mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-09 05:18:40 +02:00
Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d7b1cfd7 | ||
|
|
e15c1bcb32 | ||
|
|
826d414d9f | ||
|
|
0da6fca727 | ||
|
|
2327bc1705 | ||
|
|
3bb76cecac | ||
|
|
e3f82bb237 | ||
|
|
fcd6270f2d | ||
|
|
d5411703ca | ||
|
|
8fc2058db8 | ||
|
|
d03949f5d1 | ||
|
|
ff01786063 | ||
|
|
bb117ab4da | ||
|
|
f5ec247b47 | ||
|
|
6e6dd00523 | ||
|
|
c9fb98f033 | ||
|
|
75ecff5e6d | ||
|
|
91a9f918fb | ||
|
|
03305f90ca | ||
|
|
9428bc07c5 | ||
|
|
f38f9c3502 | ||
|
|
64a796aece | ||
|
|
63039fe8c9 | ||
|
|
3f2b723d0f | ||
|
|
ffd7fb1cf9 | ||
|
|
b049b206cf | ||
|
|
35f56dc8a9 | ||
|
|
a71cd326cc | ||
|
|
cc841d1c7c | ||
|
|
16ecdcf96e | ||
|
|
96ec56d1ad | ||
|
|
9b15bafc9f | ||
|
|
aa02f2e465 | ||
|
|
15464617ef | ||
|
|
60ecceb59d | ||
|
|
3fe943e989 | ||
|
|
5e7d039454 | ||
|
|
f928df146a | ||
|
|
6e57cb49c4 | ||
|
|
2f8494b013 | ||
|
|
6f1b93bd9f | ||
|
|
19b928d7f6 | ||
|
|
b849be6f5e | ||
|
|
86096df1a4 | ||
|
|
98a465c7b4 | ||
|
|
3315f61ae4 | ||
|
|
f977445d7a | ||
|
|
095d5e1cec | ||
|
|
69fbfa4b33 | ||
|
|
1c89d33726 | ||
|
|
acee907932 | ||
|
|
08ad216fe2 | ||
|
|
3cc7a0937c | ||
|
|
322cad5548 | ||
|
|
73d70dd480 | ||
|
|
56bc569d4a | ||
|
|
981ae58f6a | ||
|
|
b2bfa4c002 | ||
|
|
8b6003c5e3 | ||
|
|
017b6237ca | ||
|
|
d5496c2e9a | ||
|
|
5ef8961c33 | ||
|
|
e8bea48577 | ||
|
|
6cae78f5c2 | ||
|
|
d9d31a0620 | ||
|
|
3682c5fca2 | ||
|
|
14b04a14b6 | ||
|
|
f218d90bfb | ||
|
|
020f936ad4 | ||
|
|
224e5efe48 | ||
|
|
439ab2c8c0 | ||
|
|
26d0209763 | ||
|
|
29086fc783 | ||
|
|
bb656a265f | ||
|
|
b11ad9dc5b | ||
|
|
2b1667e532 | ||
|
|
228a198598 | ||
|
|
2843308fb8 |
@@ -16,10 +16,10 @@ jobs:
|
|||||||
- name: checkout sources
|
- name: checkout sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: use Node.js 16.x
|
- name: use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
|
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ jobs:
|
|||||||
- name: checkout sources
|
- name: checkout sources
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: use Node.js 16.x
|
- name: use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
|
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ android {
|
|||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 101
|
versionCode 103
|
||||||
versionName "0.9.70-beta"
|
versionName "0.9.72-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
manifestPlaceholders = [
|
manifestPlaceholders = [
|
||||||
"appAuthRedirectScheme": "com.audiobookshelf.app"
|
"appAuthRedirectScheme": "com.audiobookshelf.app"
|
||||||
@@ -64,6 +64,8 @@ repositories {
|
|||||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
}
|
}
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
// TODO: Temporarily using SNAPSHOT version of Simple Storage that resolves https://github.com/anggrayudi/SimpleStorage/issues/133
|
||||||
|
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
@@ -118,7 +120,7 @@ dependencies {
|
|||||||
implementation 'io.github.pilgr:paperdb:2.7.2'
|
implementation 'io.github.pilgr:paperdb:2.7.2'
|
||||||
|
|
||||||
// Simple Storage
|
// Simple Storage
|
||||||
implementation "com.anggrayudi:storage:1.5.4"
|
implementation "com.anggrayudi:storage:1.5.5-SNAPSHOT"
|
||||||
|
|
||||||
// OK HTTP
|
// OK HTTP
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':byteowls-capacitor-filesharer')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-browser')
|
implementation project(':capacitor-browser')
|
||||||
implementation project(':capacitor-clipboard')
|
implementation project(':capacitor-clipboard')
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"pkg": "@byteowls/capacitor-filesharer",
|
||||||
|
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/app",
|
"pkg": "@capacitor/app",
|
||||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ class MainActivity : BridgeActivity() {
|
|||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
storageHelper.onSaveInstanceState(outState)
|
storageHelper.onSaveInstanceState(outState)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
|||||||
@@ -253,8 +253,10 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
if (!DeviceManager.checkConnectivity(ctx)) {
|
if (!DeviceManager.checkConnectivity(ctx)) {
|
||||||
serverUserMediaProgress = mutableListOf()
|
serverUserMediaProgress = mutableListOf()
|
||||||
cb(false)
|
cb(false)
|
||||||
|
} else if (DeviceManager.serverConnectionConfigId == "") { // If in offline mode server connection config is unset
|
||||||
|
serverUserMediaProgress = mutableListOf()
|
||||||
|
cb(false)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
var hasValidConn = false
|
var hasValidConn = false
|
||||||
var lookupMediaProgress = true
|
var lookupMediaProgress = true
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
|
|||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
Log.d(tag, "Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}")
|
||||||
|
|
||||||
// Local library item is linked to a server library item
|
// Local library item is linked to a server library item
|
||||||
// Send sync to server also if connected to this server and local item belongs to this server
|
// Send sync to server also if connected to this server and local item belongs to this server
|
||||||
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.audiobookshelf.app.server
|
package com.audiobookshelf.app.server
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -31,7 +34,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
||||||
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>)
|
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>, val deviceInfo:DeviceInfo)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
|
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
|
||||||
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
|
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
|
||||||
@@ -322,7 +325,11 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
|
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions)))
|
@SuppressLint("HardwareIds")
|
||||||
|
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, Build.VERSION.RELEASE)
|
||||||
|
|
||||||
|
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
|
||||||
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
|
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
|
||||||
postRequest("/api/session/local-all", payload, null) {
|
postRequest("/api/session/local-all", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':byteowls-capacitor-filesharer'
|
||||||
|
project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android')
|
||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
class WrappingMarquee {
|
||||||
|
#scrollDelay = 2000
|
||||||
|
#scrollSpeed = 30
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
constructor(el) {
|
||||||
|
this.el = el
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.pEl = el?.firstElementChild
|
||||||
|
|
||||||
|
this.innerText = ''
|
||||||
|
this.isScrolling = false
|
||||||
|
|
||||||
|
/** @type {NodeJS.Timeout} */
|
||||||
|
this.timer = null
|
||||||
|
/** @type {number} */
|
||||||
|
this.animationId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transparent gradient mask shown when text is scrolling left and overflowing right
|
||||||
|
*
|
||||||
|
* @param {boolean} showLeft
|
||||||
|
*/
|
||||||
|
setMask(showLeft) {
|
||||||
|
if (!this.el) return
|
||||||
|
this.el.style.maskImage = showLeft ? 'linear-gradient(90deg, transparent 0%, #fff 10%, #000 90%, transparent)' : 'linear-gradient(90deg, #000 90%, transparent)'
|
||||||
|
}
|
||||||
|
|
||||||
|
startScroll() {
|
||||||
|
if (this.isScrolling) {
|
||||||
|
console.warn('Already scrolling')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isScrolling = true
|
||||||
|
this.setMask(true)
|
||||||
|
|
||||||
|
let textScrollAmount = this.el.scrollWidth
|
||||||
|
this.pEl.innerHTML = this.innerText + ' '.repeat(15)
|
||||||
|
let totalScrollAmount = this.el.scrollWidth
|
||||||
|
let scrollDuration = totalScrollAmount * this.#scrollSpeed
|
||||||
|
|
||||||
|
this.pEl.innerHTML = this.pEl.innerHTML + this.innerText
|
||||||
|
|
||||||
|
let done = false
|
||||||
|
let start, previousTimeStamp
|
||||||
|
|
||||||
|
const step = (timeStamp) => {
|
||||||
|
if (start === undefined) {
|
||||||
|
start = timeStamp
|
||||||
|
}
|
||||||
|
const elapsed = timeStamp - start
|
||||||
|
|
||||||
|
if (this.isScrolling && previousTimeStamp !== timeStamp) {
|
||||||
|
const amountToMove = Math.min(elapsed / scrollDuration * totalScrollAmount, totalScrollAmount)
|
||||||
|
this.pEl.style.transform = `translateX(-${amountToMove}px)`
|
||||||
|
if (amountToMove === totalScrollAmount) done = true
|
||||||
|
if (amountToMove > textScrollAmount) this.setMask(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isScrolling || done) { // canceled or done
|
||||||
|
this.isScrolling = false
|
||||||
|
this.pEl.style.transform = 'translateX(0px)'
|
||||||
|
this.pEl.innerText = this.innerText
|
||||||
|
this.setMask(false)
|
||||||
|
if (done) {
|
||||||
|
this.startTimer()
|
||||||
|
}
|
||||||
|
} else if (elapsed < scrollDuration) { // step
|
||||||
|
previousTimeStamp = timeStamp
|
||||||
|
this.animationId = window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.animationId = window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.startScroll()
|
||||||
|
}, this.#scrollDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
this.isScrolling = false
|
||||||
|
window.cancelAnimationFrame(this.animationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and start marquee if text overflows container
|
||||||
|
* resets the marquee if already active
|
||||||
|
*
|
||||||
|
* @param {string} innerText
|
||||||
|
*/
|
||||||
|
init(innerText) {
|
||||||
|
if (!this.el || !this.pEl) return
|
||||||
|
|
||||||
|
this.reset()
|
||||||
|
|
||||||
|
this.innerText = innerText
|
||||||
|
this.pEl.innerText = innerText
|
||||||
|
this.pEl.style.transform = 'translateX(0px)'
|
||||||
|
|
||||||
|
if (this.el.scrollWidth > this.el.clientWidth) {
|
||||||
|
this.setMask(false)
|
||||||
|
this.startTimer()
|
||||||
|
} else {
|
||||||
|
this.el.style.maskImage = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default WrappingMarquee
|
||||||
+1
-4
@@ -2,6 +2,7 @@
|
|||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
@import './defaultStyles.css';
|
@import './defaultStyles.css';
|
||||||
@import './absicons.css';
|
@import './absicons.css';
|
||||||
|
@import './transitions.css';
|
||||||
|
|
||||||
* {
|
* {
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
@@ -16,10 +17,6 @@ textarea {
|
|||||||
user-select: auto;
|
user-select: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #262626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-wrapper {
|
.layout-wrapper {
|
||||||
height: calc(100vh - env(safe-area-inset-top));
|
height: calc(100vh - env(safe-area-inset-top));
|
||||||
min-height: calc(100vh - env(safe-area-inset-top));
|
min-height: calc(100vh - env(safe-area-inset-top));
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
(function (global, factory) {
|
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
||||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
||||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['fast-sort'] = {}));
|
|
||||||
}(this, (function (exports) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// >>> INTERFACES <<<
|
|
||||||
// >>> HELPERS <<<
|
|
||||||
var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; };
|
|
||||||
var throwInvalidConfigErrorIfTrue = function (condition, context) {
|
|
||||||
if (condition)
|
|
||||||
throw Error("Invalid sort config: " + context);
|
|
||||||
};
|
|
||||||
var unpackObjectSorter = function (sortByObj) {
|
|
||||||
var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc;
|
|
||||||
var order = asc ? 1 : -1;
|
|
||||||
var sortBy = (asc || desc);
|
|
||||||
// Validate object config
|
|
||||||
throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property');
|
|
||||||
throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties');
|
|
||||||
var comparer = sortByObj.comparer && castComparer(sortByObj.comparer);
|
|
||||||
return { order: order, sortBy: sortBy, comparer: comparer };
|
|
||||||
};
|
|
||||||
// >>> SORTERS <<<
|
|
||||||
var multiPropertySorterProvider = function (defaultComparer) {
|
|
||||||
return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) {
|
|
||||||
var valA;
|
|
||||||
var valB;
|
|
||||||
if (typeof sortBy === 'string') {
|
|
||||||
valA = a[sortBy];
|
|
||||||
valB = b[sortBy];
|
|
||||||
}
|
|
||||||
else if (typeof sortBy === 'function') {
|
|
||||||
valA = sortBy(a);
|
|
||||||
valB = sortBy(b);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var objectSorterConfig = unpackObjectSorter(sortBy);
|
|
||||||
return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b);
|
|
||||||
}
|
|
||||||
var equality = comparer(valA, valB, order);
|
|
||||||
if ((equality === 0 || (valA == null && valB == null)) &&
|
|
||||||
sortByArr.length > depth) {
|
|
||||||
return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b);
|
|
||||||
}
|
|
||||||
return equality;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
function getSortStrategy(sortBy, comparer, order) {
|
|
||||||
// Flat array sorter
|
|
||||||
if (sortBy === undefined || sortBy === true) {
|
|
||||||
return function (a, b) { return comparer(a, b, order); };
|
|
||||||
}
|
|
||||||
// Sort list of objects by single object key
|
|
||||||
if (typeof sortBy === 'string') {
|
|
||||||
throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.');
|
|
||||||
return function (a, b) { return comparer(a[sortBy], b[sortBy], order); };
|
|
||||||
}
|
|
||||||
// Sort list of objects by single function sorter
|
|
||||||
if (typeof sortBy === 'function') {
|
|
||||||
return function (a, b) { return comparer(sortBy(a), sortBy(b), order); };
|
|
||||||
}
|
|
||||||
// Sort by multiple properties
|
|
||||||
if (Array.isArray(sortBy)) {
|
|
||||||
var multiPropSorter_1 = multiPropertySorterProvider(comparer);
|
|
||||||
return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); };
|
|
||||||
}
|
|
||||||
// Unpack object config to get actual sorter strategy
|
|
||||||
var objectSorterConfig = unpackObjectSorter(sortBy);
|
|
||||||
return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order);
|
|
||||||
}
|
|
||||||
var sortArray = function (order, ctx, sortBy, comparer) {
|
|
||||||
var _a;
|
|
||||||
if (!Array.isArray(ctx)) {
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
// Unwrap sortBy if array with only 1 value to get faster sort strategy
|
|
||||||
if (Array.isArray(sortBy) && sortBy.length < 2) {
|
|
||||||
_a = sortBy, sortBy = _a[0];
|
|
||||||
}
|
|
||||||
return ctx.sort(getSortStrategy(sortBy, comparer, order));
|
|
||||||
};
|
|
||||||
// >>> Public <<<
|
|
||||||
var createNewSortInstance = function (opts) {
|
|
||||||
var comparer = castComparer(opts.comparer);
|
|
||||||
return function (_ctx) {
|
|
||||||
var ctx = Array.isArray(_ctx) && !opts.inPlaceSorting
|
|
||||||
? _ctx.slice()
|
|
||||||
: _ctx;
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Sort array in ascending order.
|
|
||||||
* @example
|
|
||||||
* sort([3, 1, 4]).asc();
|
|
||||||
* sort(users).asc(u => u.firstName);
|
|
||||||
* sort(users).asc([
|
|
||||||
* U => u.firstName
|
|
||||||
* u => u.lastName,
|
|
||||||
* ]);
|
|
||||||
*/
|
|
||||||
asc: function (sortBy) {
|
|
||||||
return sortArray(1, ctx, sortBy, comparer);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Sort array in descending order.
|
|
||||||
* @example
|
|
||||||
* sort([3, 1, 4]).desc();
|
|
||||||
* sort(users).desc(u => u.firstName);
|
|
||||||
* sort(users).desc([
|
|
||||||
* U => u.firstName
|
|
||||||
* u => u.lastName,
|
|
||||||
* ]);
|
|
||||||
*/
|
|
||||||
desc: function (sortBy) {
|
|
||||||
return sortArray(-1, ctx, sortBy, comparer);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Sort array in ascending or descending order. It allows sorting on multiple props
|
|
||||||
* in different order for each of them.
|
|
||||||
* @example
|
|
||||||
* sort(users).by([
|
|
||||||
* { asc: u => u.score }
|
|
||||||
* { desc: u => u.age }
|
|
||||||
* ]);
|
|
||||||
*/
|
|
||||||
by: function (sortBy) {
|
|
||||||
return sortArray(1, ctx, sortBy, comparer);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var defaultComparer = function (a, b, order) {
|
|
||||||
if (a == null)
|
|
||||||
return order;
|
|
||||||
if (b == null)
|
|
||||||
return -order;
|
|
||||||
if (a < b)
|
|
||||||
return -1;
|
|
||||||
if (a === b)
|
|
||||||
return 0;
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
var sort = createNewSortInstance({
|
|
||||||
comparer: defaultComparer,
|
|
||||||
});
|
|
||||||
var inPlaceSort = createNewSortInstance({
|
|
||||||
comparer: defaultComparer,
|
|
||||||
inPlaceSorting: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.createNewSortInstance = createNewSortInstance;
|
|
||||||
exports.inPlaceSort = inPlaceSort;
|
|
||||||
exports.sort = sort;
|
|
||||||
|
|
||||||
Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
|
|
||||||
})));
|
|
||||||
+6
-2
@@ -18,7 +18,7 @@
|
|||||||
--color-track: 107 114 128;
|
--color-track: 107 114 128;
|
||||||
--color-track-buffered: 75 85 99;
|
--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-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%);
|
--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: 189 191 191;
|
||||||
--color-track-buffered: 129 131 131;
|
--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-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%);
|
--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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
|
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
|
||||||
<span class="material-icons text-3xl text-fg">arrow_back</span>
|
<span class="material-icons text-3xl text-fg">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="user && currentLibrary">
|
<div v-if="user && currentLibrary && networkConnected">
|
||||||
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||||
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
@@ -54,14 +54,17 @@ export default {
|
|||||||
this.$store.commit('setCastAvailable', val)
|
this.$store.commit('setCastAvailable', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
|
},
|
||||||
currentLibrary() {
|
currentLibrary() {
|
||||||
return this.$store.getters['libraries/getCurrentLibrary']
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
},
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.currentLibrary ? this.currentLibrary.name : ''
|
return this.currentLibrary?.name || ''
|
||||||
},
|
},
|
||||||
currentLibraryIcon() {
|
currentLibraryIcon() {
|
||||||
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
return this.currentLibrary?.icon || 'database'
|
||||||
},
|
},
|
||||||
showBack() {
|
showBack() {
|
||||||
if (!this.$route.name) return true
|
if (!this.$route.name) return true
|
||||||
@@ -71,7 +74,7 @@ export default {
|
|||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : 'err'
|
return this.user?.username || 'err'
|
||||||
},
|
},
|
||||||
isCasting() {
|
isCasting() {
|
||||||
return this.$store.state.isCasting
|
return this.$store.state.isCasting
|
||||||
|
|||||||
+156
-73
@@ -15,7 +15,7 @@
|
|||||||
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
|
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
|
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -41,7 +41,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
||||||
<p class="title-text truncate">{{ title }}</p>
|
<div ref="titlewrapper" class="overflow-hidden relative">
|
||||||
|
<p class="title-text whitespace-nowrap"></p>
|
||||||
|
</div>
|
||||||
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
|
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -66,17 +68,17 @@
|
|||||||
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
|
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
|
||||||
|
|
||||||
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
|
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
|
||||||
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
|
<div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
|
||||||
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
|
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
|
||||||
|
|
||||||
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
||||||
<widgets-spinner-icon v-else class="h-8 w-8" />
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
||||||
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@
|
|||||||
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
|
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor">
|
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': playerSettings.lockUi || !showFullscreen }" @touchstart="touchstartCursor">
|
||||||
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
|
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
||||||
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
|
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -106,6 +108,7 @@
|
|||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||||
import { FastAverageColor } from 'fast-average-color'
|
import { FastAverageColor } from 'fast-average-color'
|
||||||
|
import WrappingMarquee from '@/assets/WrappingMarquee.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -145,10 +148,12 @@ export default {
|
|||||||
onPlaybackSpeedChangedListener: null,
|
onPlaybackSpeedChangedListener: null,
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
playerSettings: {
|
||||||
useChapterTrack: false,
|
useChapterTrack: false,
|
||||||
useTotalTrack: true,
|
useTotalTrack: true,
|
||||||
lockUi: false,
|
scaleElapsedTimeBySpeed: true,
|
||||||
|
lockUi: false
|
||||||
|
},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isDraggingCursor: false,
|
isDraggingCursor: false,
|
||||||
draggingTouchStartX: 0,
|
draggingTouchStartX: 0,
|
||||||
@@ -157,16 +162,21 @@ export default {
|
|||||||
syncStatus: 0,
|
syncStatus: 0,
|
||||||
showMoreMenuDialog: false,
|
showMoreMenuDialog: false,
|
||||||
coverRgb: 'rgb(55, 56, 56)',
|
coverRgb: 'rgb(55, 56, 56)',
|
||||||
coverBgIsLight: false
|
coverBgIsLight: false,
|
||||||
|
titleMarquee: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showFullscreen(val) {
|
showFullscreen(val) {
|
||||||
this.updateScreenSize()
|
this.updateScreenSize()
|
||||||
this.$store.commit('setPlayerFullscreen', !!val)
|
this.$store.commit('setPlayerFullscreen', !!val)
|
||||||
|
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ''
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
this.updateScreenSize()
|
this.updateScreenSize()
|
||||||
|
},
|
||||||
|
title(val) {
|
||||||
|
if (this.titleMarquee) this.titleMarquee.init(val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -186,17 +196,22 @@ export default {
|
|||||||
{
|
{
|
||||||
text: this.$strings.LabelTotalTrack,
|
text: this.$strings.LabelTotalTrack,
|
||||||
value: 'total_track',
|
value: 'total_track',
|
||||||
icon: this.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
|
icon: this.playerSettings.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelChapterTrack,
|
text: this.$strings.LabelChapterTrack,
|
||||||
value: 'chapter_track',
|
value: 'chapter_track',
|
||||||
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
icon: this.playerSettings.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
|
text: this.$strings.LabelScaleElapsedTimeBySpeed,
|
||||||
|
value: 'scale_elapsed_time',
|
||||||
|
icon: this.playerSettings.scaleElapsedTimeBySpeed ? 'check_box' : 'check_box_outline_blank'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.playerSettings.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
|
||||||
value: 'lock',
|
value: 'lock',
|
||||||
icon: this.lockUi ? 'lock' : 'lock_open'
|
icon: this.playerSettings.lockUi ? 'lock' : 'lock_open'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelClosePlayer,
|
text: this.$strings.LabelClosePlayer,
|
||||||
@@ -256,22 +271,22 @@ export default {
|
|||||||
return this.mediaPlayer === 'cast-player'
|
return this.mediaPlayer === 'cast-player'
|
||||||
},
|
},
|
||||||
mediaPlayer() {
|
mediaPlayer() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaPlayer : null
|
return this.playbackSession?.mediaPlayer || null
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaType : null
|
return this.playbackSession?.mediaType || null
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaMetadata : null
|
return this.playbackSession?.mediaMetadata || null
|
||||||
},
|
},
|
||||||
libraryItem() {
|
libraryItem() {
|
||||||
return this.playbackSession ? this.playbackSession.libraryItem || null : null
|
return this.playbackSession?.libraryItem || null
|
||||||
},
|
},
|
||||||
localLibraryItem() {
|
localLibraryItem() {
|
||||||
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
return this.playbackSession?.localLibraryItem || null
|
||||||
},
|
},
|
||||||
localLibraryItemCoverSrc() {
|
localLibraryItemCoverSrc() {
|
||||||
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
||||||
@@ -279,7 +294,7 @@ export default {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
playMethod() {
|
playMethod() {
|
||||||
return this.playbackSession ? this.playbackSession.playMethod : null
|
return this.playbackSession?.playMethod || 0
|
||||||
},
|
},
|
||||||
isLocalPlayMethod() {
|
isLocalPlayMethod() {
|
||||||
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
||||||
@@ -288,19 +303,19 @@ export default {
|
|||||||
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
|
const mediaItemTitle = this.playbackSession?.displayTitle || this.mediaMetadata?.title || 'Title'
|
||||||
if (this.playbackSession) return this.playbackSession.displayTitle
|
if (this.currentChapterTitle) {
|
||||||
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
if (this.showFullscreen) return this.currentChapterTitle
|
||||||
|
return `${mediaItemTitle} | ${this.currentChapterTitle}`
|
||||||
|
}
|
||||||
|
return mediaItemTitle
|
||||||
},
|
},
|
||||||
authorName() {
|
authorName() {
|
||||||
if (this.playbackSession) return this.playbackSession.displayAuthor
|
if (this.playbackSession) return this.playbackSession.displayAuthor
|
||||||
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author'
|
return this.mediaMetadata?.authorName || 'Author'
|
||||||
},
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
if (this.playbackSession && this.playbackSession.chapters) {
|
return this.playbackSession?.chapters || []
|
||||||
return this.playbackSession.chapters
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
if (!this.chapters.length) return null
|
if (!this.chapters.length) return null
|
||||||
@@ -311,7 +326,7 @@ export default {
|
|||||||
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
|
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
|
||||||
},
|
},
|
||||||
currentChapterTitle() {
|
currentChapterTitle() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter?.title || ''
|
||||||
},
|
},
|
||||||
currentChapterDuration() {
|
currentChapterDuration() {
|
||||||
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
||||||
@@ -321,11 +336,14 @@ export default {
|
|||||||
},
|
},
|
||||||
currentTimePretty() {
|
currentTimePretty() {
|
||||||
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
|
if (this.playerSettings.scaleElapsedTimeBySpeed) {
|
||||||
|
currentTimeToUse = currentTimeToUse / this.currentPlaybackRate
|
||||||
|
}
|
||||||
|
return this.$secondsToTimestamp(currentTimeToUse)
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = currentTimeToUse - this.currentChapter.start
|
var currChapTime = currentTimeToUse - this.currentChapter.start
|
||||||
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
||||||
}
|
}
|
||||||
@@ -411,7 +429,11 @@ export default {
|
|||||||
AbsAudioPlayer.requestSession()
|
AbsAudioPlayer.requestSession()
|
||||||
},
|
},
|
||||||
clickContainer() {
|
clickContainer() {
|
||||||
|
this.expandToFullscreen()
|
||||||
|
},
|
||||||
|
expandToFullscreen() {
|
||||||
this.showFullscreen = true
|
this.showFullscreen = true
|
||||||
|
if (this.titleMarquee) this.titleMarquee.reset()
|
||||||
|
|
||||||
// Update track for total time bar if useChapterTrack is set
|
// Update track for total time bar if useChapterTrack is set
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -420,6 +442,8 @@ export default {
|
|||||||
},
|
},
|
||||||
collapseFullscreen() {
|
collapseFullscreen() {
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
|
if (this.titleMarquee) this.titleMarquee.reset()
|
||||||
|
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
},
|
},
|
||||||
async jumpNextChapter() {
|
async jumpNextChapter() {
|
||||||
@@ -493,7 +517,7 @@ export default {
|
|||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
},
|
},
|
||||||
updateReadyTrack() {
|
updateReadyTrack() {
|
||||||
if (this.useChapterTrack) {
|
if (this.playerSettings.useChapterTrack) {
|
||||||
if (this.$refs.totalReadyTrack) {
|
if (this.$refs.totalReadyTrack) {
|
||||||
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
|
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
|
||||||
}
|
}
|
||||||
@@ -510,11 +534,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
||||||
currentTime = Math.max(0, currentTime - this.currentChapter.start)
|
currentTime = Math.max(0, currentTime - this.currentChapter.start)
|
||||||
}
|
}
|
||||||
|
if (this.playerSettings.scaleElapsedTimeBySpeed) {
|
||||||
|
currentTime = currentTime / this.currentPlaybackRate
|
||||||
|
}
|
||||||
|
|
||||||
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
|
ts.innerText = this.$secondsToTimestamp(currentTime)
|
||||||
},
|
},
|
||||||
timeupdate() {
|
timeupdate() {
|
||||||
if (!this.$refs.playedTrack) {
|
if (!this.$refs.playedTrack) {
|
||||||
@@ -542,7 +569,7 @@ export default {
|
|||||||
let bufferedPercent = this.bufferedTime / this.totalDuration
|
let bufferedPercent = this.bufferedTime / this.totalDuration
|
||||||
const totalBufferedPercent = bufferedPercent
|
const totalBufferedPercent = bufferedPercent
|
||||||
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
||||||
const currChapTime = currentTimeToUse - this.currentChapter.start
|
const currChapTime = currentTimeToUse - this.currentChapter.start
|
||||||
percentDone = currChapTime / this.currentChapterDuration
|
percentDone = currChapTime / this.currentChapterDuration
|
||||||
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
||||||
@@ -556,7 +583,7 @@ export default {
|
|||||||
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
|
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.useChapterTrack) {
|
if (this.playerSettings.useChapterTrack) {
|
||||||
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
||||||
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
||||||
}
|
}
|
||||||
@@ -583,7 +610,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async touchstartCursor(e) {
|
async touchstartCursor(e) {
|
||||||
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.playerSettings.lockUi) return
|
||||||
|
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
this.isDraggingCursor = true
|
this.isDraggingCursor = true
|
||||||
@@ -624,38 +651,49 @@ export default {
|
|||||||
resetStream(startTime) {
|
resetStream(startTime) {
|
||||||
this.closePlayback()
|
this.closePlayback()
|
||||||
},
|
},
|
||||||
handleGesture() {
|
|
||||||
var touchDistance = this.touchEndY - this.touchStartY
|
|
||||||
if (touchDistance > 100) {
|
|
||||||
this.collapseFullscreen()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
touchstart(e) {
|
touchstart(e) {
|
||||||
if (!this.showFullscreen || !e.changedTouches) return
|
if (!e.changedTouches) return
|
||||||
|
const touchPosY = e.changedTouches[0].pageY
|
||||||
|
// when minimized only listen to touchstart on the player
|
||||||
|
if (!this.showFullscreen && touchPosY < window.innerHeight - 120) return
|
||||||
|
|
||||||
this.touchStartY = e.changedTouches[0].screenY
|
// for ios
|
||||||
if (this.touchStartY > window.innerHeight / 3) {
|
if (!this.showFullscreen && e.pageX < 20) {
|
||||||
// console.log('touch too low')
|
e.preventDefault()
|
||||||
return
|
e.stopImmediatePropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.touchStartY = touchPosY
|
||||||
this.touchStartTime = Date.now()
|
this.touchStartTime = Date.now()
|
||||||
},
|
},
|
||||||
touchend(e) {
|
touchend(e) {
|
||||||
if (!e.changedTouches) return
|
if (!e.changedTouches) return
|
||||||
|
const touchDuration = Date.now() - this.touchStartTime
|
||||||
|
const touchEndY = e.changedTouches[0].pageY
|
||||||
|
const touchDistanceY = touchEndY - this.touchStartY
|
||||||
|
|
||||||
|
// reset touch start data
|
||||||
|
this.touchStartTime = 0
|
||||||
|
this.touchStartY = 0
|
||||||
|
|
||||||
if (this.isDraggingCursor) {
|
if (this.isDraggingCursor) {
|
||||||
if (this.draggingCurrentTime !== this.currentTime) {
|
if (this.draggingCurrentTime !== this.currentTime) {
|
||||||
this.seek(this.draggingCurrentTime)
|
this.seek(this.draggingCurrentTime)
|
||||||
}
|
}
|
||||||
this.isDraggingCursor = false
|
this.isDraggingCursor = false
|
||||||
} else if (this.showFullscreen) {
|
} else {
|
||||||
this.touchEndY = e.changedTouches[0].screenY
|
|
||||||
var touchDuration = Date.now() - this.touchStartTime
|
|
||||||
if (touchDuration > 1200) {
|
if (touchDuration > 1200) {
|
||||||
// console.log('touch too long', touchDuration)
|
// console.log('touch too long', touchDuration)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.handleGesture()
|
if (this.showFullscreen) {
|
||||||
|
// Touch start higher than touchend
|
||||||
|
if (touchDistanceY > 100) {
|
||||||
|
this.collapseFullscreen()
|
||||||
|
}
|
||||||
|
} else if (touchDistanceY < -100) {
|
||||||
|
this.expandToFullscreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
touchmove(e) {
|
touchmove(e) {
|
||||||
@@ -665,7 +703,7 @@ export default {
|
|||||||
let duration = this.totalDuration
|
let duration = this.totalDuration
|
||||||
let minTime = 0
|
let minTime = 0
|
||||||
let maxTime = duration
|
let maxTime = duration
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
||||||
duration = this.currentChapterDuration
|
duration = this.currentChapterDuration
|
||||||
minTime = this.currentChapter.start
|
minTime = this.currentChapter.start
|
||||||
maxTime = minTime + duration
|
maxTime = minTime + duration
|
||||||
@@ -685,32 +723,42 @@ export default {
|
|||||||
if (action === 'history') {
|
if (action === 'history') {
|
||||||
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
|
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
|
} else if (action === 'scale_elapsed_time') {
|
||||||
|
this.playerSettings.scaleElapsedTimeBySpeed = !this.playerSettings.scaleElapsedTimeBySpeed
|
||||||
|
this.updateTimestamp()
|
||||||
|
this.savePlayerSettings()
|
||||||
} else if (action === 'lock') {
|
} else if (action === 'lock') {
|
||||||
this.lockUi = !this.lockUi
|
this.playerSettings.lockUi = !this.playerSettings.lockUi
|
||||||
this.$localStore.setPlayerLock(this.lockUi)
|
this.savePlayerSettings()
|
||||||
} else if (action === 'chapter_track') {
|
} else if (action === 'chapter_track') {
|
||||||
this.useChapterTrack = !this.useChapterTrack
|
this.playerSettings.useChapterTrack = !this.playerSettings.useChapterTrack
|
||||||
this.useTotalTrack = !this.useChapterTrack || this.useTotalTrack
|
this.playerSettings.useTotalTrack = !this.playerSettings.useChapterTrack || this.playerSettings.useTotalTrack
|
||||||
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
this.updateUseChapterTrack()
|
||||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
this.savePlayerSettings()
|
||||||
} else if (action === 'total_track') {
|
} else if (action === 'total_track') {
|
||||||
this.useTotalTrack = !this.useTotalTrack
|
this.playerSettings.useTotalTrack = !this.playerSettings.useTotalTrack
|
||||||
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack
|
this.playerSettings.useChapterTrack = !this.playerSettings.useTotalTrack || this.playerSettings.useChapterTrack
|
||||||
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
this.updateUseChapterTrack()
|
||||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
this.savePlayerSettings()
|
||||||
} else if (action === 'close') {
|
} else if (action === 'close') {
|
||||||
this.closePlayback()
|
this.closePlayback()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateUseChapterTrack() {
|
||||||
|
// Chapter track in NowPlaying only supported on iOS for now
|
||||||
|
if (this.$platform === 'ios') {
|
||||||
|
AbsAudioPlayer.setChapterTrack({ enabled: this.playerSettings.useChapterTrack })
|
||||||
|
}
|
||||||
|
},
|
||||||
forceCloseDropdownMenu() {
|
forceCloseDropdownMenu() {
|
||||||
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
||||||
this.$refs.dropdownMenu.closeMenu()
|
this.$refs.dropdownMenu.closeMenu()
|
||||||
@@ -727,6 +775,30 @@ export default {
|
|||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.playbackSession = null
|
this.playbackSession = null
|
||||||
},
|
},
|
||||||
|
async loadPlayerSettings() {
|
||||||
|
const savedPlayerSettings = await this.$localStore.getPlayerSettings()
|
||||||
|
if (!savedPlayerSettings) {
|
||||||
|
// In 0.9.72-beta 'useChapterTrack', 'useTotalTrack' and 'playerLock' was replaced with 'playerSettings' JSON object
|
||||||
|
// Check if this old key was set and if so migrate them over to 'playerSettings'
|
||||||
|
const chapterTrackPref = await this.$localStore.getPreferenceByKey('useChapterTrack')
|
||||||
|
if (chapterTrackPref) {
|
||||||
|
this.playerSettings.useChapterTrack = chapterTrackPref === '1'
|
||||||
|
const totalTrackPref = await this.$localStore.getPreferenceByKey('useTotalTrack')
|
||||||
|
this.playerSettings.useTotalTrack = totalTrackPref === '1'
|
||||||
|
const playerLockPref = await this.$localStore.getPreferenceByKey('playerLock')
|
||||||
|
this.playerSettings.lockUi = playerLockPref === '1'
|
||||||
|
}
|
||||||
|
this.savePlayerSettings()
|
||||||
|
} else {
|
||||||
|
this.playerSettings.useChapterTrack = !!savedPlayerSettings.useChapterTrack
|
||||||
|
this.playerSettings.useTotalTrack = !!savedPlayerSettings.useTotalTrack
|
||||||
|
this.playerSettings.lockUi = !!savedPlayerSettings.lockUi
|
||||||
|
this.playerSettings.scaleElapsedTimeBySpeed = !!savedPlayerSettings.scaleElapsedTimeBySpeed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
savePlayerSettings() {
|
||||||
|
return this.$localStore.setPlayerSettings({ ...this.playerSettings })
|
||||||
|
},
|
||||||
//
|
//
|
||||||
// Listeners from audio AbsAudioPlayer
|
// Listeners from audio AbsAudioPlayer
|
||||||
//
|
//
|
||||||
@@ -771,6 +843,10 @@ export default {
|
|||||||
|
|
||||||
// Set track width
|
// Set track width
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
if (this.titleMarquee) this.titleMarquee.reset()
|
||||||
|
this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper)
|
||||||
|
this.titleMarquee.init(this.title)
|
||||||
|
|
||||||
if (this.$refs.track) {
|
if (this.$refs.track) {
|
||||||
this.trackWidth = this.$refs.track.clientWidth
|
this.trackWidth = this.$refs.track.clientWidth
|
||||||
} else {
|
} else {
|
||||||
@@ -793,9 +869,7 @@ export default {
|
|||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
await this.loadPlayerSettings()
|
||||||
this.useTotalTrack = await this.$localStore.getUseTotalTrack()
|
|
||||||
this.lockUi = await this.$localStore.getPlayerLock()
|
|
||||||
|
|
||||||
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
||||||
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
||||||
@@ -817,15 +891,23 @@ export default {
|
|||||||
}, 50)
|
}, 50)
|
||||||
},
|
},
|
||||||
updateScreenSize() {
|
updateScreenSize() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.titleMarquee) this.titleMarquee.init(this.title)
|
||||||
|
}, 500)
|
||||||
|
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
this.windowWidth = window.innerWidth
|
this.windowWidth = window.innerWidth
|
||||||
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
||||||
const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio
|
const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio
|
||||||
|
const titleAuthorLeftOffsetCollapsed = 30 + coverImageWidthCollapsed
|
||||||
|
const titleAuthorWidthCollapsed = this.windowWidth - 128 - titleAuthorLeftOffsetCollapsed - 10
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
|
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
||||||
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px')
|
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', titleAuthorLeftOffsetCollapsed + 'px')
|
||||||
|
document.documentElement.style.setProperty('--title-author-width-collapsed', titleAuthorWidthCollapsed + 'px')
|
||||||
},
|
},
|
||||||
minimizePlayerEvt() {
|
minimizePlayerEvt() {
|
||||||
this.collapseFullscreen()
|
this.collapseFullscreen()
|
||||||
@@ -848,7 +930,7 @@ export default {
|
|||||||
window.addEventListener('resize', this.screenOrientationChange)
|
window.addEventListener('resize', this.screenOrientationChange)
|
||||||
|
|
||||||
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
|
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('touchend', this.touchend)
|
||||||
document.body.addEventListener('touchmove', this.touchmove)
|
document.body.addEventListener('touchmove', this.touchmove)
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
@@ -893,6 +975,7 @@ export default {
|
|||||||
--cover-image-width-collapsed: 46px;
|
--cover-image-width-collapsed: 46px;
|
||||||
--cover-image-height-collapsed: 46px;
|
--cover-image-height-collapsed: 46px;
|
||||||
--title-author-left-offset-collapsed: 80px;
|
--title-author-left-offset-collapsed: 80px;
|
||||||
|
--title-author-width-collapsed: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerContainer {
|
.playerContainer {
|
||||||
@@ -940,7 +1023,7 @@ export default {
|
|||||||
transition-property: left, bottom, width, height;
|
transition-property: left, bottom, width, height;
|
||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
|
||||||
width: 40%;
|
width: var(--title-author-width-collapsed);
|
||||||
bottom: 76px;
|
bottom: 76px;
|
||||||
left: var(--title-author-left-offset-collapsed);
|
left: var(--title-author-left-offset-collapsed);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -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>
|
<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 -->
|
<!-- 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>
|
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -493,7 +493,12 @@ export default {
|
|||||||
if (router) {
|
if (router) {
|
||||||
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`)
|
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`)
|
||||||
else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`)
|
else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`)
|
||||||
else router.push(`/item/${this.libraryItemId}`)
|
else if (this.localLibraryItem) {
|
||||||
|
// Pass local library item id to server page to allow falling back to offline page
|
||||||
|
router.push(`/item/${this.libraryItemId}?localLibraryItemId=${this.localLibraryItemId}`)
|
||||||
|
} else {
|
||||||
|
router.push(`/item/${this.libraryItemId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
<p class="whitespace-normal line-clamp-2" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
||||||
<span v-if="seriesSequence">#{{ seriesSequence }} </span>{{ displayTitle }}
|
<span v-if="seriesSequence">#{{ seriesSequence }} </span>{{ displayTitle }}
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="200" height="100%">
|
<modals-modal v-model="show" :width="200" height="100%">
|
||||||
<template #outer>
|
<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>
|
<p class="text-white text-2xl truncate capitalize">{{ $strings.LabelAutoRewindTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="
|
||||||
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
|
|
||||||
@click="
|
|
||||||
show = false
|
show = false
|
||||||
manualTimerModal = 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 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 v-if="manualTimerModal" class="p-4">
|
||||||
<div class="flex mb-4" @click="manualTimerModal = false">
|
<div class="flex mb-4" @click="manualTimerModal = false">
|
||||||
<span class="material-icons text-3xl">arrow_back</span>
|
<span class="material-icons text-3xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex my-2 justify-between">
|
<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>
|
<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>
|
</div>
|
||||||
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
|
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="400" height="100%">
|
<modals-modal v-model="show" :width="400" height="100%">
|
||||||
<template #outer>
|
<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>
|
<p class="text-white text-2xl truncate">{{ $strings.LabelYourBookmarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,9 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
|
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="text-xl font-mono">
|
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
|
||||||
{{ this.$secondsToTimestamp(currentTime) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
|
<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">
|
<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>
|
<span class="material-icons">add</span>
|
||||||
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
|
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
|
||||||
<p class="text-sm font-mono">
|
<p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
|
||||||
{{ this.$secondsToTimestamp(currentTime) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
default: '16px'
|
default: '16px'
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: [String, Number],
|
||||||
default: 300
|
default: 300
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" width="90%" height="100%">
|
<modals-modal v-model="show" width="90%" height="100%">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div v-show="selected !== 'all'" class="absolute top-10 left-4 z-40">
|
<div v-show="selected !== 'all'" class="absolute top-12 left-4 z-40">
|
||||||
<ui-btn class="text-lg border-yellow-400 border-opacity-40" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
|
<ui-btn class="text-lg border-yellow-400 border-opacity-40 h-10" :padding-y="0" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop>
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20 mt-8" style="max-height: 75%" @click.stop>
|
||||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
<div ref="content" class="relative text-fg h-full w-full bg-bg">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="400" height="100%">
|
<modals-modal v-model="show" :width="400" height="100%">
|
||||||
<template #outer>
|
<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>
|
<p class="text-white text-2xl truncate">{{ $strings.HeaderDetails }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="300" :processing="processing" height="100%">
|
<modals-modal v-model="show" :width="300" :processing="processing" height="100%">
|
||||||
<template #outer>
|
<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>
|
<p class="text-white text-2xl truncate">{{ $strings.HeaderLibraries }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
|
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
|
||||||
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
|
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
|
||||||
|
|
||||||
<div class="absolute z-40 top-10 right-4 h-10 w-10 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
<div class="absolute z-40 top-11 right-4 h-10 w-10 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
||||||
<span class="material-icons text-4xl">close</span>
|
<span class="material-icons text-4xl">close</span>
|
||||||
</div>
|
</div>
|
||||||
<slot name="outer" />
|
<slot name="outer" />
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<!-- ereader settings modal -->
|
<!-- ereader settings modal -->
|
||||||
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen>
|
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen>
|
||||||
<div style="box-shadow: 0px -8px 8px #11111155">
|
<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>
|
<h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1>
|
||||||
<button class="flex" @click="showSettingsModal = false">
|
<button class="flex" @click="showSettingsModal = false">
|
||||||
<span class="material-icons">close</span>
|
<span class="material-icons">close</span>
|
||||||
@@ -64,24 +64,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
|
<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="w-full h-full px-4">
|
||||||
<div class="flex items-center mb-8">
|
<div class="flex items-center mb-6">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<p class="text-base">{{ $strings.LabelTheme }}:</p>
|
<p class="text-base">{{ $strings.LabelTheme }}:</p>
|
||||||
</div>
|
</div>
|
||||||
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
|
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-8">
|
<div class="flex items-center mb-6">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<p class="text-base">{{ $strings.LabelFontScale }}:</p>
|
<p class="text-base">{{ $strings.LabelFontScale }}:</p>
|
||||||
</div>
|
</div>
|
||||||
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-8">
|
<div class="flex items-center mb-6">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p>
|
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p>
|
||||||
</div>
|
</div>
|
||||||
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +115,8 @@ export default {
|
|||||||
ereaderSettings: {
|
ereaderSettings: {
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
fontScale: 100,
|
fontScale: 100,
|
||||||
lineSpacing: 115
|
lineSpacing: 115,
|
||||||
|
spread: 'auto'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -154,6 +161,18 @@ export default {
|
|||||||
if (this.isEpub) return this.ereaderSettings.theme
|
if (this.isEpub) return this.ereaderSettings.theme
|
||||||
return document.documentElement.dataset.theme || 'dark'
|
return document.documentElement.dataset.theme || 'dark'
|
||||||
},
|
},
|
||||||
|
spreadItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLayoutSinglePage,
|
||||||
|
value: 'none'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLayoutAuto,
|
||||||
|
value: 'auto'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
themeItems() {
|
themeItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -350,7 +369,12 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const settings = localStorage.getItem('ereaderSettings')
|
const settings = localStorage.getItem('ereaderSettings')
|
||||||
if (settings) {
|
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()
|
this.settingsUpdated()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</div>
|
||||||
|
<img v-else-if="dataUrl" :src="dataUrl" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { FileSharer } from '@byteowls/capacitor-filesharer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
year: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvas: null,
|
||||||
|
dataUrl: null,
|
||||||
|
yearStats: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initCanvas() {
|
||||||
|
if (!this.yearStats) return
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 600
|
||||||
|
canvas.height = 200
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const createRoundedRect = (x, y, w, h) => {
|
||||||
|
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
|
||||||
|
grd1.addColorStop(0, '#44444455')
|
||||||
|
grd1.addColorStop(1, '#ffffff11')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.strokeStyle = '#C0C0C088'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.roundRect(x, y, w, h, [20])
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
|
||||||
|
ctx.letterSpacing = letterSpacing
|
||||||
|
|
||||||
|
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
|
||||||
|
if (maxWidth) {
|
||||||
|
let txtWidth = ctx.measureText(text).width
|
||||||
|
while (txtWidth > maxWidth) {
|
||||||
|
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
|
||||||
|
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
|
||||||
|
else text = text.slice(0, -3) // First check remove last 3 chars
|
||||||
|
text += '...'
|
||||||
|
txtWidth = ctx.measureText(text).width
|
||||||
|
console.log(`Checking text "${text}" (width:${txtWidth})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addIcon = (icon, color, fontSize, x, y) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontSize} Material Icons Outlined`
|
||||||
|
ctx.fillText(icon, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bg color
|
||||||
|
ctx.fillStyle = '#232323'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Cover image tiles
|
||||||
|
const bookCovers = this.yearStats.finishedBooksWithCovers
|
||||||
|
bookCovers.push(...this.yearStats.booksWithCovers)
|
||||||
|
|
||||||
|
if (bookCovers.length) {
|
||||||
|
let index = 0
|
||||||
|
ctx.globalAlpha = 0.25
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(canvas.width / 2, canvas.height / 2)
|
||||||
|
ctx.rotate((-Math.PI / 180) * 25)
|
||||||
|
ctx.translate(-canvas.width / 2, -canvas.height / 2)
|
||||||
|
ctx.translate(-10, -90)
|
||||||
|
for (let x = 0; x < 4; x++) {
|
||||||
|
for (let y = 0; y < 3; y++) {
|
||||||
|
const coverIndex = index % bookCovers.length
|
||||||
|
let libraryItemId = bookCovers[coverIndex]
|
||||||
|
index++
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
let sw = img.width
|
||||||
|
if (img.width > img.height) {
|
||||||
|
sw = img.height
|
||||||
|
}
|
||||||
|
let sx = -(sw - img.width) / 2
|
||||||
|
let sy = -(sw - img.height) / 2
|
||||||
|
ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
// Create gradient
|
||||||
|
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
|
||||||
|
grd1.addColorStop(0, '#000000aa')
|
||||||
|
grd1.addColorStop(1, '#cd9d49aa')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Top Abs icon
|
||||||
|
let tanColor = '#ffdb70'
|
||||||
|
ctx.fillStyle = tanColor
|
||||||
|
ctx.font = '42px absicons'
|
||||||
|
ctx.fillText('\ue900', 15, 36)
|
||||||
|
|
||||||
|
// Top text
|
||||||
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
|
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
|
// Top left box
|
||||||
|
createRoundedRect(15, 75, 280, 110)
|
||||||
|
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
|
||||||
|
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
|
||||||
|
const readIconPath = new Path2D()
|
||||||
|
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.fill(readIconPath)
|
||||||
|
|
||||||
|
createRoundedRect(305, 75, 280, 110)
|
||||||
|
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
|
||||||
|
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
|
||||||
|
addIcon('local_library', 'white', '42px', 345, 130)
|
||||||
|
|
||||||
|
this.canvas = canvas
|
||||||
|
this.dataUrl = canvas.toDataURL('png')
|
||||||
|
},
|
||||||
|
share() {
|
||||||
|
const base64Data = this.dataUrl.split(';base64,').pop()
|
||||||
|
FileSharer.share({
|
||||||
|
filename: `audiobookshelf_my_${this.year}_short.png`,
|
||||||
|
contentType: 'image/png',
|
||||||
|
base64Data
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message !== 'USER_CANCELLED') {
|
||||||
|
console.error('Failed to share', error.message)
|
||||||
|
this.$toast.error('Failed to share: ' + error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refresh() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.$emit('update:processing', true)
|
||||||
|
this.$nativeHttp
|
||||||
|
.get(`/api/me/stats/year/${this.year}`)
|
||||||
|
.then((data) => {
|
||||||
|
this.yearStats = data || []
|
||||||
|
return this.initCanvas()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to load year stats')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('update:processing', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
paddingX: Number,
|
paddingX: Number,
|
||||||
|
paddingY: Number,
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
@@ -48,14 +49,17 @@ export default {
|
|||||||
if (this.small) {
|
if (this.small) {
|
||||||
list.push('text-sm')
|
list.push('text-sm')
|
||||||
if (this.paddingX === undefined) list.push('px-4')
|
if (this.paddingX === undefined) list.push('px-4')
|
||||||
list.push('py-1')
|
if (this.paddingY === undefined) list.push('py-1')
|
||||||
} else {
|
} else {
|
||||||
if (this.paddingX === undefined) list.push('px-8')
|
if (this.paddingX === undefined) list.push('px-8')
|
||||||
list.push('py-2')
|
if (this.paddingY === undefined) list.push('py-2')
|
||||||
}
|
}
|
||||||
if (this.paddingX !== undefined) {
|
if (this.paddingX !== undefined) {
|
||||||
list.push(`px-${this.paddingX}`)
|
list.push(`px-${this.paddingX}`)
|
||||||
}
|
}
|
||||||
|
if (this.paddingY !== undefined) {
|
||||||
|
list.push(`py-${this.paddingY}`)
|
||||||
|
}
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
list.push('cursor-not-allowed')
|
list.push('cursor-not-allowed')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -728,12 +728,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
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\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -752,12 +752,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 27;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
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_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
|
|
||||||
let configuration = Realm.Configuration(
|
let configuration = Realm.Configuration(
|
||||||
schemaVersion: 15,
|
schemaVersion: 16,
|
||||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||||
if (oldSchemaVersion < 1) {
|
if (oldSchemaVersion < 1) {
|
||||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
||||||
@@ -48,6 +48,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
newObject?["languageCode"] = "en-us"
|
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
|
Realm.Configuration.defaultConfiguration = configuration
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
|
|||||||
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(setChapterTrack, CAPPluginReturnPromise);
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||||
|
|||||||
@@ -120,6 +120,17 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
call.resolve()
|
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) {
|
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||||
PlayerHandler.paused = false
|
PlayerHandler.paused = false
|
||||||
call.resolve()
|
call.resolve()
|
||||||
|
|||||||
@@ -147,14 +147,15 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
|
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
|
||||||
logger.log("syncLocalSessionsWithServer: Starting")
|
let isFirstSync = call.getBool("isFirstSync", false)
|
||||||
|
logger.log("syncLocalSessionsWithServer: Starting (First sync: \(isFirstSync))")
|
||||||
guard Store.serverConfig != nil else {
|
guard Store.serverConfig != nil else {
|
||||||
call.reject("syncLocalSessionsWithServer not connected to server")
|
call.reject("syncLocalSessionsWithServer not connected to server")
|
||||||
return call.resolve()
|
return call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await ApiClient.syncLocalSessionsWithServer()
|
await ApiClient.syncLocalSessionsWithServer(isFirstSync: isFirstSync)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'ByteowlsCapacitorFilesharer', :path => '../../node_modules/@byteowls/capacitor-filesharer'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
||||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.6.4)
|
- Alamofire (5.6.4)
|
||||||
|
- ByteowlsCapacitorFilesharer (5.0.0):
|
||||||
|
- Capacitor
|
||||||
- Capacitor (5.4.0):
|
- Capacitor (5.4.0):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorApp (5.0.6):
|
- CapacitorApp (5.0.6):
|
||||||
@@ -29,6 +31,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire (~> 5.5)
|
- Alamofire (~> 5.5)
|
||||||
|
- "ByteowlsCapacitorFilesharer (from `../../node_modules/@byteowls/capacitor-filesharer`)"
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||||
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
|
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
|
||||||
@@ -49,6 +52,8 @@ SPEC REPOS:
|
|||||||
- RealmSwift
|
- RealmSwift
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
ByteowlsCapacitorFilesharer:
|
||||||
|
:path: "../../node_modules/@byteowls/capacitor-filesharer"
|
||||||
Capacitor:
|
Capacitor:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorApp:
|
CapacitorApp:
|
||||||
@@ -74,6 +79,7 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c
|
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c
|
||||||
|
ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176
|
||||||
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
|
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
|
||||||
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
|
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
|
||||||
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6
|
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6
|
||||||
@@ -88,6 +94,6 @@ SPEC CHECKSUMS:
|
|||||||
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5
|
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5
|
||||||
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716
|
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7a8fc177ef0646dd60a1ee8aa387964975fcc1e3
|
PODFILE CHECKSUM: 02e6ffe2f51a453ce222ee9af0e55e9448d8514c
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
COCOAPODS: 1.12.1
|
||||||
|
|||||||
@@ -160,3 +160,11 @@ class PlaybackSession: Object, Codable, Deletable {
|
|||||||
try container.encode(localMediaProgressId, forKey: .localMediaProgressId)
|
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
|
// 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
|
// 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 playbackRate: Float = 1.0
|
||||||
|
@Persisted var chapterTrack: Bool = true
|
||||||
|
|
||||||
|
|
||||||
// Singleton pattern for Realm objects
|
// Singleton pattern for Realm objects
|
||||||
static func main() -> PlayerSettings {
|
static func main() -> PlayerSettings {
|
||||||
|
|||||||
@@ -39,3 +39,13 @@ class Chapter: EmbeddedObject, Codable {
|
|||||||
try container.encode(title, forKey: .title)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ class AudioPlayer: NSObject {
|
|||||||
// Update the UI
|
// Update the UI
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
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? {
|
public func getCurrentTime() -> Double? {
|
||||||
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
||||||
let currentTrackTime = self.audioPlayer.currentTime().seconds
|
let currentTrackTime = self.audioPlayer.currentTime().seconds
|
||||||
@@ -528,14 +534,24 @@ class AudioPlayer: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch type {
|
// When interruption is from the app suspending then don't resume playback
|
||||||
case .ended:
|
if #available(iOS 14.5, *) {
|
||||||
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0
|
||||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
|
||||||
if options.contains(.shouldResume) {
|
if (reason == .appWasSuspended) {
|
||||||
self.play(allowSeekBack: true)
|
logger.log("AVAudioSession was suspended")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
default: ()
|
}
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .ended:
|
||||||
|
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
||||||
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||||
|
if options.contains(.shouldResume) {
|
||||||
|
self.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
|
default: ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,12 +599,34 @@ class AudioPlayer: NSObject {
|
|||||||
|
|
||||||
commandCenter.playCommand.isEnabled = true
|
commandCenter.playCommand.isEnabled = true
|
||||||
commandCenter.playCommand.addTarget { [weak self] event in
|
commandCenter.playCommand.addTarget { [weak self] event in
|
||||||
self?.play(allowSeekBack: true)
|
guard let strongSelf = self else { return .commandFailed }
|
||||||
|
if strongSelf.isPlaying() {
|
||||||
|
strongSelf.pause()
|
||||||
|
} else {
|
||||||
|
strongSelf.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
commandCenter.pauseCommand.isEnabled = true
|
commandCenter.pauseCommand.isEnabled = true
|
||||||
commandCenter.pauseCommand.addTarget { [weak self] event in
|
commandCenter.pauseCommand.addTarget { [weak self] event in
|
||||||
self?.pause()
|
guard let strongSelf = self else { return .commandFailed }
|
||||||
|
if strongSelf.isPlaying() {
|
||||||
|
strongSelf.pause()
|
||||||
|
} else {
|
||||||
|
strongSelf.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
commandCenter.togglePlayPauseCommand.isEnabled = true
|
||||||
|
commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
|
||||||
|
guard let strongSelf = self else { return .commandFailed }
|
||||||
|
if strongSelf.isPlaying() {
|
||||||
|
strongSelf.pause()
|
||||||
|
} else {
|
||||||
|
strongSelf.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +678,14 @@ class AudioPlayer: NSObject {
|
|||||||
return .noSuchContent
|
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
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,7 +702,16 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
private func updateNowPlaying() {
|
private func updateNowPlaying() {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
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)
|
NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ class PlayerHandler {
|
|||||||
self.player?.setPlaybackRate(speed)
|
self.player?.setPlaybackRate(speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func setChapterTrack() {
|
||||||
|
self.player?.setChapterTrack()
|
||||||
|
}
|
||||||
|
|
||||||
public static func getSleepTimeRemaining() -> Double? {
|
public static func getSleepTimeRemaining() -> Double? {
|
||||||
return self.player?.getSleepTimeRemaining()
|
return self.player?.getSleepTimeRemaining()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,10 +190,10 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool {
|
public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool {
|
||||||
return await postResource(endpoint: "api/session/local-all", parameters: LocalPlaybackSessionSyncAllPayload(sessions: sessions))
|
return await postResource(endpoint: "api/session/local-all", parameters: LocalPlaybackSessionSyncAllPayload(sessions: sessions, deviceInfo: sessions.first?.deviceInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func syncLocalSessionsWithServer() async {
|
public static func syncLocalSessionsWithServer(isFirstSync: Bool) async {
|
||||||
do {
|
do {
|
||||||
// Sync server progress with local media progress
|
// Sync server progress with local media progress
|
||||||
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
|
||||||
@@ -232,13 +232,15 @@ class ApiClient {
|
|||||||
let playbackSessions = Database.shared.getAllPlaybackSessions().filter {
|
let playbackSessions = Database.shared.getAllPlaybackSessions().filter {
|
||||||
$0.serverConnectionConfigId == Store.serverConfig?.id
|
$0.serverConnectionConfigId == Store.serverConfig?.id
|
||||||
}.map { $0.freeze() }
|
}.map { $0.freeze() }
|
||||||
logger.log("syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server")
|
logger.log("syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server (first sync: \(isFirstSync))")
|
||||||
if (!playbackSessions.isEmpty) {
|
if (!playbackSessions.isEmpty) {
|
||||||
let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions)
|
let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions)
|
||||||
if (success) {
|
if (success) {
|
||||||
// Remove sessions from db
|
// Remove sessions from db
|
||||||
try playbackSessions.forEach { session in
|
try playbackSessions.forEach { session in
|
||||||
if (!session.isActiveSession) {
|
logger.log("syncLocalSessionsWithServer: Handling \(session.displayTitle ?? "") (\(session.id)) \(session.isActiveSession)")
|
||||||
|
// On first sync then remove all sessions
|
||||||
|
if (!session.isActiveSession || isFirstSync) {
|
||||||
if let session = session.thaw() {
|
if let session = session.thaw() {
|
||||||
try session.delete()
|
try session.delete()
|
||||||
}
|
}
|
||||||
@@ -327,6 +329,7 @@ struct LocalMediaProgressSyncResultsPayload: Codable {
|
|||||||
|
|
||||||
struct LocalPlaybackSessionSyncAllPayload: Codable {
|
struct LocalPlaybackSessionSyncAllPayload: Codable {
|
||||||
var sessions: [PlaybackSession]
|
var sessions: [PlaybackSession]
|
||||||
|
var deviceInfo: [String: String?]?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Connectivity {
|
struct Connectivity {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class NowPlayingInfo {
|
|||||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
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
|
// Update on the main to prevent access collisions
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
if let self = self {
|
if let self = self {
|
||||||
@@ -62,6 +62,18 @@ class NowPlayingInfo {
|
|||||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||||
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
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
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +101,7 @@ class NowPlayingInfo {
|
|||||||
|
|
||||||
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title
|
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title
|
||||||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.title
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shouldFetchCover(id: String) -> Bool {
|
private func shouldFetchCover(id: String) -> Bool {
|
||||||
|
|||||||
+5
-7
@@ -45,7 +45,7 @@ export default {
|
|||||||
if (timeSinceDisconnect > 5000) {
|
if (timeSinceDisconnect > 5000) {
|
||||||
console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server')
|
console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.syncLocalSessions()
|
this.syncLocalSessions(false)
|
||||||
}, 4000)
|
}, 4000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ export default {
|
|||||||
|
|
||||||
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
|
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
|
||||||
|
|
||||||
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 10000).catch((error) => {
|
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 6000).catch((error) => {
|
||||||
console.error('[default] Server auth failed', error)
|
console.error('[default] Server auth failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -195,14 +195,14 @@ export default {
|
|||||||
this.$eventBus.$emit('library-changed')
|
this.$eventBus.$emit('library-changed')
|
||||||
this.inittingLibraries = false
|
this.inittingLibraries = false
|
||||||
},
|
},
|
||||||
async syncLocalSessions() {
|
async syncLocalSessions(isFirstSync) {
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
console.log('[default] No need to sync local sessions - not connected to server')
|
console.log('[default] No need to sync local sessions - not connected to server')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[default] Calling syncLocalSessions')
|
console.log('[default] Calling syncLocalSessions')
|
||||||
const response = await this.$db.syncLocalSessionsWithServer()
|
const response = await this.$db.syncLocalSessionsWithServer(isFirstSync)
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
console.error('[default] Failed to sync local sessions', response.error)
|
console.error('[default] Failed to sync local sessions', response.error)
|
||||||
} else {
|
} else {
|
||||||
@@ -326,8 +326,6 @@ export default {
|
|||||||
|
|
||||||
await this.$store.dispatch('setupNetworkListener')
|
await this.$store.dispatch('setupNetworkListener')
|
||||||
|
|
||||||
await this.$store.dispatch('globals/loadLocalMediaProgress')
|
|
||||||
|
|
||||||
if (this.$store.state.user.serverConnectionConfig) {
|
if (this.$store.state.user.serverConnectionConfig) {
|
||||||
await this.initLibraries()
|
await this.initLibraries()
|
||||||
} else {
|
} else {
|
||||||
@@ -335,7 +333,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
||||||
await this.syncLocalSessions()
|
await this.syncLocalSessions(true)
|
||||||
|
|
||||||
this.hasMounted = true
|
this.hasMounted = true
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -2,7 +2,8 @@ const pkg = require('./package.json')
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
target: 'static',
|
||||||
|
telemetry: false,
|
||||||
env: {
|
env: {
|
||||||
PROD: '1',
|
PROD: '1',
|
||||||
ANDROID_APP_URL: 'https://play.google.com/store/apps/details?id=com.audiobookshelf.app',
|
ANDROID_APP_URL: 'https://play.google.com/store/apps/details?id=com.audiobookshelf.app',
|
||||||
@@ -35,6 +36,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
|
'@/assets/tailwind.css',
|
||||||
'@/assets/app.css'
|
'@/assets/app.css'
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -55,10 +57,6 @@ export default {
|
|||||||
|
|
||||||
components: true,
|
components: true,
|
||||||
|
|
||||||
buildModules: [
|
|
||||||
'@nuxtjs/tailwindcss',
|
|
||||||
],
|
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxtjs/axios'
|
'@nuxtjs/axios'
|
||||||
],
|
],
|
||||||
@@ -66,6 +64,12 @@ export default {
|
|||||||
axios: {},
|
axios: {},
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
|
postcss: {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
babel: {
|
babel: {
|
||||||
plugins: [['@babel/plugin-proposal-private-property-in-object', { loose: true }]],
|
plugins: [['@babel/plugin-proposal-private-property-in-object', { loose: true }]],
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2374
-17835
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.70-beta",
|
"version": "0.9.72-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"ionic:serve": "npm run start"
|
"ionic:serve": "npm run start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@byteowls/capacitor-filesharer": "^5.0.0",
|
||||||
"@capacitor/android": "^5.0.0",
|
"@capacitor/android": "^5.0.0",
|
||||||
"@capacitor/app": "^5.0.6",
|
"@capacitor/app": "^5.0.6",
|
||||||
"@capacitor/browser": "^5.1.0",
|
"@capacitor/browser": "^5.1.0",
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
"@babel/core": "7.13.15",
|
"@babel/core": "7.13.15",
|
||||||
"@babel/preset-env": "7.13.15",
|
"@babel/preset-env": "7.13.15",
|
||||||
"@capacitor/cli": "^5.0.0",
|
"@capacitor/cli": "^5.0.0",
|
||||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.3.5",
|
"postcss": "^8.4.33",
|
||||||
"tailwindcss": "^3.3.2"
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<div id="bookshelf" class="w-full h-full p-4 overflow-y-auto">
|
<div id="bookshelf" class="w-full h-full p-4 overflow-y-auto">
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
<template v-for="author in authors">
|
<template v-for="author in authors">
|
||||||
<cards-author-card :key="author.id" :author="author" :width="96" :height="120" class="p-2" />
|
<cards-author-card :key="author.id" :author="author" :width="cardWidth" :height="cardHeight" class="p-2" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,16 +16,21 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
authors: [],
|
authors: [],
|
||||||
loadedLibraryId: null
|
loadedLibraryId: null,
|
||||||
|
cardWidth: 200
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
cardHeight() {
|
||||||
|
return this.cardWidth * 1.25
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async init() {
|
||||||
|
this.cardWidth = (window.innerWidth - 64) / 2
|
||||||
if (!this.currentLibraryId) {
|
if (!this.currentLibraryId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export default {
|
|||||||
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
||||||
|
|
||||||
if (isConnectedToServerWithInternet) {
|
if (isConnectedToServerWithInternet) {
|
||||||
const categories = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1&include=rssfeed,numEpisodesIncomplete`).catch((error) => {
|
const categories = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1&include=rssfeed,numEpisodesIncomplete`, { connectTimeout: 10000 }).catch((error) => {
|
||||||
console.error('[categories] Failed to fetch categories', error)
|
console.error('[categories] Failed to fetch categories', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -327,8 +327,13 @@ export default {
|
|||||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
|
if (this.$route.query.error) {
|
||||||
|
this.$toast.error(this.$route.query.error)
|
||||||
|
}
|
||||||
|
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
|
await this.$store.dispatch('globals/loadLocalMediaProgress')
|
||||||
console.log(`[categories] mounted so fetching categories`)
|
console.log(`[categories] mounted so fetching categories`)
|
||||||
this.fetchCategories()
|
this.fetchCategories()
|
||||||
},
|
},
|
||||||
|
|||||||
+238
-183
@@ -1,149 +1,163 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="item-page" class="w-full h-full px-3 pb-4 overflow-y-auto overflow-x-hidden relative bg-bg">
|
<div v-if="!libraryItem" class="w-full h-full relative flex items-center justify-center bg-bg">
|
||||||
<div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10">
|
<ui-loading-indicator />
|
||||||
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
|
</div>
|
||||||
<div class="w-full h-full absolute top-0 left-0" style="background: var(--gradient-item-page)" />
|
<div v-else id="item-page" class="w-full h-full overflow-y-auto overflow-x-hidden relative bg-bg">
|
||||||
</div>
|
<!-- cover -->
|
||||||
|
<div class="w-full flex justify-center relative">
|
||||||
<div class="z-10 relative">
|
<div style="width: 0; transform: translateX(-50vw); overflow: visible">
|
||||||
<!-- cover -->
|
<div style="width: 150vw; overflow: hidden">
|
||||||
<div class="w-full flex justify-center relative mb-4">
|
<div id="coverBg" style="filter: blur(5vw)">
|
||||||
<div style="width: 0; transform: translateX(-50vw); overflow: visible">
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
|
||||||
<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>
|
||||||
</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>
|
</div>
|
||||||
|
<div class="relative" @click="showFullscreenCover = true">
|
||||||
<!-- title -->
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw @imageLoaded="coverImageLoaded" />
|
||||||
<div class="text-center mb-2">
|
<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>
|
||||||
<h1 class="text-xl font-semibold">{{ title }}</h1>
|
|
||||||
<p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p>
|
|
||||||
</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>
|
||||||
|
|
||||||
|
<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">
|
<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 />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
@@ -156,43 +170,37 @@ import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
|
|||||||
import { FastAverageColor } from 'fast-average-color'
|
import { FastAverageColor } from 'fast-average-color'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, app }) {
|
async asyncData({ store, params, redirect, app, query }) {
|
||||||
const libraryItemId = params.id
|
const libraryItemId = params.id
|
||||||
let libraryItem = null
|
let libraryItem = null
|
||||||
|
|
||||||
if (libraryItemId.startsWith('local')) {
|
if (libraryItemId.startsWith('local')) {
|
||||||
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
|
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
|
||||||
console.log('Got lli', libraryItemId)
|
if (!libraryItem) {
|
||||||
|
return redirect('/?error=Failed to get downloaded library item')
|
||||||
|
}
|
||||||
|
|
||||||
// If library item is linked to the currently connected server then redirect to the page using the server library item id
|
// If library item is linked to the currently connected server then redirect to the page using the server library item id
|
||||||
if (libraryItem?.libraryItemId?.startsWith('li_')) {
|
if (libraryItem?.libraryItemId?.startsWith('li_')) {
|
||||||
// Detect old library item id
|
// Detect old library item id
|
||||||
console.error('Local library item has old server library item id', libraryItem.libraryItemId)
|
console.error('Local library item has old server library item id', libraryItem.libraryItemId)
|
||||||
} else if (libraryItem?.libraryItemId && libraryItem?.serverAddress === store.getters['user/getServerAddress'] && store.state.networkConnected) {
|
} else if (query.noredirect !== '1' && libraryItem?.libraryItemId && libraryItem?.serverAddress === store.getters['user/getServerAddress'] && store.state.networkConnected) {
|
||||||
let query = ''
|
const queryParams = new URLSearchParams()
|
||||||
if (libraryItem.mediaType === 'podcast') query = '?episodefilter=downloaded' // Filter by downloaded when redirecting from the local copy
|
queryParams.set('localLibraryItemId', libraryItemId)
|
||||||
return redirect(`/item/${libraryItem.libraryItemId}${query}`)
|
if (libraryItem.mediaType === 'podcast') {
|
||||||
}
|
// Filter by downloaded when redirecting from the local copy
|
||||||
} else if (store.state.user.serverConnectionConfig) {
|
queryParams.set('episodefilter', 'downloaded')
|
||||||
libraryItem = await app.$nativeHttp.get(`/api/items/${libraryItemId}?expanded=1&include=rssfeed`).catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (libraryItem) {
|
|
||||||
const localLibraryItem = await app.$db.getLocalLibraryItemByLId(libraryItemId)
|
|
||||||
if (localLibraryItem) {
|
|
||||||
console.log('Library item has local library item also', localLibraryItem.id)
|
|
||||||
libraryItem.localLibraryItem = localLibraryItem
|
|
||||||
}
|
}
|
||||||
|
return redirect(`/item/${libraryItem.libraryItemId}?${queryParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
} else if (!store.state.user.serverConnectionConfig) {
|
||||||
|
// Not connected to server
|
||||||
|
return redirect('/?error=No server connection to get library item')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!libraryItem) {
|
|
||||||
console.error('No item...', params.id)
|
|
||||||
return redirect('/')
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
libraryItem,
|
libraryItem,
|
||||||
rssFeed: libraryItem.rssFeed || null
|
libraryItemId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -201,7 +209,7 @@ export default {
|
|||||||
showSelectLocalFolder: false,
|
showSelectLocalFolder: false,
|
||||||
showMoreMenu: false,
|
showMoreMenu: false,
|
||||||
showFullscreenCover: false,
|
showFullscreenCover: false,
|
||||||
coverRgb: 'rgb(55, 56, 56)',
|
coverRgb: null,
|
||||||
coverBgIsLight: false,
|
coverBgIsLight: false,
|
||||||
windowWidth: 0,
|
windowWidth: 0,
|
||||||
descriptionClamped: false,
|
descriptionClamped: false,
|
||||||
@@ -285,8 +293,8 @@ export default {
|
|||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
rssFeed() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem?.rssFeed
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.libraryItem.mediaType
|
return this.libraryItem.mediaType
|
||||||
@@ -297,6 +305,9 @@ export default {
|
|||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
|
tags() {
|
||||||
|
return this.media.tags || []
|
||||||
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
@@ -464,6 +475,9 @@ export default {
|
|||||||
|
|
||||||
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
|
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
|
||||||
return width
|
return width
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.coverWidth * this.bookCoverAspectRatio
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -705,27 +719,59 @@ export default {
|
|||||||
if (!this.libraryItem.libraryId) return
|
if (!this.libraryItem.libraryId) return
|
||||||
await this.$store.dispatch('libraries/fetch', this.libraryItem.libraryId)
|
await this.$store.dispatch('libraries/fetch', this.libraryItem.libraryId)
|
||||||
this.$localStore.setLastLibraryId(this.libraryItem.libraryId)
|
this.$localStore.setLastLibraryId(this.libraryItem.libraryId)
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
// If library of this item is different from current library then switch libraries
|
||||||
|
if (this.$store.state.libraries.currentLibraryId !== this.libraryItem.libraryId) {
|
||||||
|
this.setLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
window.addEventListener('resize', this.windowResized)
|
||||||
|
this.$eventBus.$on('library-changed', this.libraryChanged)
|
||||||
|
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
||||||
|
this.$socket.$on('item_updated', this.itemUpdated)
|
||||||
|
this.$socket.$on('rss_feed_open', this.rssFeedOpen)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadServerLibraryItem() {
|
||||||
|
console.log(`Fetching library item "${this.libraryItemId}" from server`)
|
||||||
|
const libraryItem = await this.$nativeHttp.get(`/api/items/${this.libraryItemId}?expanded=1&include=rssfeed`, { connectTimeout: 5000 }).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (libraryItem) {
|
||||||
|
const localLibraryItem = await this.$db.getLocalLibraryItemByLId(this.libraryItemId)
|
||||||
|
if (localLibraryItem) {
|
||||||
|
console.log('Library item has local library item also', localLibraryItem.id)
|
||||||
|
libraryItem.localLibraryItem = localLibraryItem
|
||||||
|
}
|
||||||
|
this.libraryItem = libraryItem
|
||||||
|
} else if (this.$route.query.localLibraryItemId) {
|
||||||
|
// Failed to get server library item but is local library item so redirect
|
||||||
|
return this.$router.replace(`/item/${this.$route.query.localLibraryItemId}?noredirect=1`)
|
||||||
|
} else {
|
||||||
|
this.$toast.error('Failed to get library item from server')
|
||||||
|
return this.$router.replace('/bookshelf')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
// If library of this item is different from current library then switch libraries
|
if (!this.libraryItem) {
|
||||||
if (this.$store.state.libraries.currentLibraryId !== this.libraryItem.libraryId) {
|
await this.loadServerLibraryItem()
|
||||||
this.setLibrary()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.windowWidth = window.innerWidth
|
|
||||||
window.addEventListener('resize', this.windowResized)
|
|
||||||
this.$eventBus.$on('library-changed', this.libraryChanged)
|
|
||||||
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
|
||||||
this.$socket.$on('item_updated', this.itemUpdated)
|
|
||||||
this.$socket.$on('rss_feed_open', this.rssFeedOpen)
|
|
||||||
this.$socket.$on('rss_feed_closed', this.rssFeedClosed)
|
|
||||||
this.checkDescriptionClamped()
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('resize', this.windowResized)
|
window.removeEventListener('resize', this.windowResized)
|
||||||
@@ -744,6 +790,15 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<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 {
|
.title-container {
|
||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
max-width: calc(100% - 64px);
|
max-width: calc(100% - 64px);
|
||||||
|
|||||||
+23
-23
@@ -2,16 +2,16 @@
|
|||||||
<div class="w-full h-full px-4 py-8 overflow-y-auto">
|
<div class="w-full h-full px-4 py-8 overflow-y-auto">
|
||||||
<!-- Display settings -->
|
<!-- Display settings -->
|
||||||
<p class="uppercase text-xs font-semibold text-fg-muted mb-2">{{ $strings.HeaderUserInterfaceSettings }}</p>
|
<p class="uppercase text-xs font-semibold text-fg-muted mb-2">{{ $strings.HeaderUserInterfaceSettings }}</p>
|
||||||
<div class="flex items-center py-3" @click="toggleEnableAltView">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleEnableAltView">
|
||||||
<ui-toggle-switch v-model="enableBookshelfView" @input="saveSettings" />
|
<ui-toggle-switch v-model="enableBookshelfView" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelUseBookshelfView }}</p>
|
<p class="pl-4">{{ $strings.LabelUseBookshelfView }}</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- screen.orientation.lock not supported on iOS webview -->
|
<!-- screen.orientation.lock not supported on iOS webview -->
|
||||||
<div v-if="!isiOS" class="flex items-center py-3" @click.stop="toggleLockOrientation">
|
<div v-if="!isiOS" class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center pointer-events-none">
|
<div class="w-10 flex justify-center" @click.stop="toggleLockOrientation">
|
||||||
<ui-toggle-switch v-model="lockCurrentOrientation" />
|
<ui-toggle-switch v-model="lockCurrentOrientation" class="pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelLockOrientation }}</p>
|
<p class="pl-4">{{ $strings.LabelLockOrientation }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,26 +36,26 @@
|
|||||||
|
|
||||||
<!-- Playback settings -->
|
<!-- Playback settings -->
|
||||||
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderPlaybackSettings }}</p>
|
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderPlaybackSettings }}</p>
|
||||||
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleDisableAutoRewind">
|
<div v-if="!isiOS" class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleDisableAutoRewind">
|
||||||
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelDisableAutoRewind }}</p>
|
<p class="pl-4">{{ $strings.LabelDisableAutoRewind }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center py-3" @click="toggleJumpBackwards">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleJumpBackwards">
|
||||||
<span class="material-icons text-4xl">{{ currentJumpBackwardsTimeIcon }}</span>
|
<span class="material-icons text-4xl">{{ currentJumpBackwardsTimeIcon }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelJumpBackwardsTime }}</p>
|
<p class="pl-4">{{ $strings.LabelJumpBackwardsTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center py-3" @click="toggleJumpForward">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleJumpForward">
|
||||||
<span class="material-icons text-4xl">{{ currentJumpForwardTimeIcon }}</span>
|
<span class="material-icons text-4xl">{{ currentJumpForwardTimeIcon }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelJumpForwardsTime }}</p>
|
<p class="pl-4">{{ $strings.LabelJumpForwardsTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleEnableMp3IndexSeeking">
|
<div v-if="!isiOS" class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleEnableMp3IndexSeeking">
|
||||||
<ui-toggle-switch v-model="settings.enableMp3IndexSeeking" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.enableMp3IndexSeeking" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelEnableMp3IndexSeeking }}</p>
|
<p class="pl-4">{{ $strings.LabelEnableMp3IndexSeeking }}</p>
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
<!-- Sleep timer settings -->
|
<!-- Sleep timer settings -->
|
||||||
<template v-if="!isiOS">
|
<template v-if="!isiOS">
|
||||||
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderSleepTimerSettings }}</p>
|
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderSleepTimerSettings }}</p>
|
||||||
<div class="flex items-center py-3" @click="toggleDisableShakeToResetSleepTimer">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleDisableShakeToResetSleepTimer">
|
||||||
<ui-toggle-switch v-model="settings.disableShakeToResetSleepTimer" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.disableShakeToResetSleepTimer" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelDisableShakeToReset }}</p>
|
<p class="pl-4">{{ $strings.LabelDisableShakeToReset }}</p>
|
||||||
@@ -78,22 +78,22 @@
|
|||||||
<ui-text-input :value="shakeSensitivityOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
|
<ui-text-input :value="shakeSensitivityOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center py-3" @click="toggleDisableSleepTimerFadeOut">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleDisableSleepTimerFadeOut">
|
||||||
<ui-toggle-switch v-model="settings.disableSleepTimerFadeOut" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.disableSleepTimerFadeOut" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelDisableAudioFadeOut }}</p>
|
<p class="pl-4">{{ $strings.LabelDisableAudioFadeOut }}</p>
|
||||||
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerFadeOut')">info</span>
|
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerFadeOut')">info</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center py-3" @click="toggleDisableSleepTimerResetFeedback">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleDisableSleepTimerResetFeedback">
|
||||||
<ui-toggle-switch v-model="settings.disableSleepTimerResetFeedback" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.disableSleepTimerResetFeedback" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelDisableVibrateOnReset }}</p>
|
<p class="pl-4">{{ $strings.LabelDisableVibrateOnReset }}</p>
|
||||||
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerResetFeedback')">info</span>
|
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerResetFeedback')">info</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center py-3" @click="toggleAutoSleepTimer">
|
<div class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleAutoSleepTimer">
|
||||||
<ui-toggle-switch v-model="settings.autoSleepTimer" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.autoSleepTimer" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelAutoSleepTimer }}</p>
|
<p class="pl-4">{{ $strings.LabelAutoSleepTimer }}</p>
|
||||||
@@ -115,8 +115,8 @@
|
|||||||
<ui-text-input :value="sleepTimerLengthOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
|
<ui-text-input :value="sleepTimerLengthOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="settings.autoSleepTimer" class="flex items-center py-3" @click="toggleAutoSleepTimerAutoRewind">
|
<div v-if="settings.autoSleepTimer" class="flex items-center py-3">
|
||||||
<div class="w-10 flex justify-center">
|
<div class="w-10 flex justify-center" @click="toggleAutoSleepTimerAutoRewind">
|
||||||
<ui-toggle-switch v-model="settings.autoSleepTimerAutoRewind" @input="saveSettings" />
|
<ui-toggle-switch v-model="settings.autoSleepTimerAutoRewind" @input="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
<p class="pl-4">{{ $strings.LabelAutoSleepTimerAutoRewind }}</p>
|
<p class="pl-4">{{ $strings.LabelAutoSleepTimerAutoRewind }}</p>
|
||||||
|
|||||||
+14
-2
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-0 py-4 overflow-y-auto">
|
<div class="w-full h-full px-0 py-4 overflow-y-auto">
|
||||||
|
<!-- Year in review banner shown at the top in December and January -->
|
||||||
|
<stats-year-in-review-banner v-if="showYearInReviewBanner" />
|
||||||
|
|
||||||
<h1 class="text-xl px-4">
|
<h1 class="text-xl px-4">
|
||||||
{{ $strings.HeaderYourStats }}
|
{{ $strings.HeaderYourStats }}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -50,6 +53,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Year in review banner shown at the bottom Feb - Nov -->
|
||||||
|
<stats-year-in-review-banner v-if="!showYearInReviewBanner" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -58,7 +64,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listeningStats: null,
|
listeningStats: null,
|
||||||
windowWidth: 0
|
windowWidth: 0,
|
||||||
|
showYearInReviewBanner: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -103,7 +110,12 @@ export default {
|
|||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Loaded user listening data', this.listeningStats)
|
|
||||||
|
let month = new Date().getMonth()
|
||||||
|
// January and December show year in review banner
|
||||||
|
if (month === 11 || month === 0) {
|
||||||
|
this.showYearInReviewBanner = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
|||||||
if (this.player) this.player.playbackRate = value
|
if (this.player) this.player.playbackRate = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PluginMethod
|
||||||
|
setChapterTrack({ enabled }) {
|
||||||
|
this.useChapterTrack = enabled
|
||||||
|
}
|
||||||
|
|
||||||
// PluginMethod
|
// PluginMethod
|
||||||
async getCurrentTime() {
|
async getCurrentTime() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncLocalSessionsWithServer() {
|
async syncLocalSessionsWithServer({ isFirstSync }) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -70,8 +70,8 @@ class DbService {
|
|||||||
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
|
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
|
||||||
}
|
}
|
||||||
|
|
||||||
syncLocalSessionsWithServer() {
|
syncLocalSessionsWithServer(isFirstSync) {
|
||||||
return AbsDatabase.syncLocalSessionsWithServer()
|
return AbsDatabase.syncLocalSessionsWithServer({ isFirstSync })
|
||||||
}
|
}
|
||||||
|
|
||||||
syncServerMediaProgressWithLocalMediaProgress(payload) {
|
syncServerMediaProgressWithLocalMediaProgress(payload) {
|
||||||
|
|||||||
+11
-6
@@ -94,26 +94,31 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
|
|||||||
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = true) => {
|
||||||
if (isNaN(seconds) || seconds === null) return ''
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
seconds = Math.round(seconds)
|
seconds = Math.round(seconds)
|
||||||
|
|
||||||
var minutes = Math.floor(seconds / 60)
|
let minutes = Math.floor(seconds / 60)
|
||||||
seconds -= minutes * 60
|
seconds -= minutes * 60
|
||||||
var hours = Math.floor(minutes / 60)
|
let hours = Math.floor(minutes / 60)
|
||||||
minutes -= hours * 60
|
minutes -= hours * 60
|
||||||
|
|
||||||
var days = 0
|
let days = 0
|
||||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||||
days = Math.floor(hours / 24)
|
days = Math.floor(hours / 24)
|
||||||
hours -= days * 24
|
hours -= days * 24
|
||||||
}
|
}
|
||||||
|
|
||||||
var strs = []
|
// If not showing seconds then round minutes up
|
||||||
|
if (minutes && seconds && !showSeconds) {
|
||||||
|
if (seconds >= 30) minutes++
|
||||||
|
}
|
||||||
|
|
||||||
|
const strs = []
|
||||||
if (days) strs.push(`${days}d`)
|
if (days) strs.push(`${days}d`)
|
||||||
if (hours) strs.push(`${hours}h`)
|
if (hours) strs.push(`${hours}h`)
|
||||||
if (minutes) strs.push(`${minutes}m`)
|
if (minutes) strs.push(`${minutes}m`)
|
||||||
if (seconds) strs.push(`${seconds}s`)
|
if (seconds && showSeconds) strs.push(`${seconds}s`)
|
||||||
return strs.join(' ')
|
return strs.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-43
@@ -42,56 +42,20 @@ class LocalStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUseChapterTrack(useChapterTrack) {
|
async setPlayerSettings(playerSettings) {
|
||||||
try {
|
try {
|
||||||
await Preferences.set({ key: 'useChapterTrack', value: useChapterTrack ? '1' : '0' })
|
await Preferences.set({ key: 'playerSettings', value: JSON.stringify(playerSettings) })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to set use chapter track', error)
|
console.error('[LocalStorage] Failed to set player settings', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUseChapterTrack() {
|
async getPlayerSettings() {
|
||||||
try {
|
try {
|
||||||
var obj = await Preferences.get({ key: 'useChapterTrack' }) || {}
|
const playerSettingsObj = await Preferences.get({ key: 'playerSettings' }) || {}
|
||||||
return obj.value === '1'
|
return playerSettingsObj.value ? JSON.parse(playerSettingsObj.value) : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to get use chapter track', error)
|
console.error('[LocalStorage] Failed to get player settings', error)
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setUseTotalTrack(useTotalTrack) {
|
|
||||||
try {
|
|
||||||
await Preferences.set({ key: 'useTotalTrack', value: useTotalTrack ? '1' : '0' })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LocalStorage] Failed to set use total track', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUseTotalTrack() {
|
|
||||||
try {
|
|
||||||
var obj = await Preferences.get({ key: 'useTotalTrack' }) || {}
|
|
||||||
return obj.value === '1'
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LocalStorage] Failed to get use total track', error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPlayerLock(lock) {
|
|
||||||
try {
|
|
||||||
await Preferences.set({ key: 'playerLock', value: lock ? '1' : '0' })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LocalStorage] Failed to set player lock', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPlayerLock() {
|
|
||||||
try {
|
|
||||||
var obj = await Preferences.get({ key: 'playerLock' }) || {}
|
|
||||||
return obj.value === '1'
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LocalStorage] Failed to get player lock', error)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +143,22 @@ class LocalStorage {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get preference value by key
|
||||||
|
*
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async getPreferenceByKey(key) {
|
||||||
|
try {
|
||||||
|
const obj = await Preferences.get({ key }) || {}
|
||||||
|
return obj.value || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[LocalStorage] Failed to get preference "${key}"`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ Audiobookshelf is a self-hosted audiobook and podcast server.
|
|||||||
Get the Android app on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
Get the Android app on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
||||||
|
|
||||||
### iOS (early beta)
|
### iOS (early beta)
|
||||||
|
**Beta is currently full. Apple has a hard limit of 10k beta testers. Updates will be posted in Discord/Matrix.**
|
||||||
|
|
||||||
Available to beta testers through Test Flight
|
Using Test Flight: https://testflight.apple.com/join/wiic7QIW ***(beta is full)***
|
||||||
|
|
||||||
Join the beta testers and install the iOS app: https://testflight.apple.com/join/wiic7QIW
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ This application is built using [NuxtJS](https://nuxtjs.org/) and [Capacitor](ht
|
|||||||
Required Software:
|
Required Software:
|
||||||
|
|
||||||
* [Git](https://git-scm.com/downloads)
|
* [Git](https://git-scm.com/downloads)
|
||||||
* [Node.js](https://nodejs.org/en/)
|
* [Node.js](https://nodejs.org/en/) (version 20)
|
||||||
* Code editor of choice([VSCode](https://code.visualstudio.com/download), etc)
|
* Code editor of choice([VSCode](https://code.visualstudio.com/download), etc)
|
||||||
* [Android Studio](https://developer.android.com/studio)
|
* [Android Studio](https://developer.android.com/studio)
|
||||||
* [Android SDK](https://developer.android.com/studio)
|
* [Android SDK](https://developer.android.com/studio)
|
||||||
@@ -46,7 +45,7 @@ Note: This requires a PowerShell prompt with winget installed. You should be ab
|
|||||||
winget install -e --id Git.Git; `
|
winget install -e --id Git.Git; `
|
||||||
winget install -e --id Microsoft.VisualStudioCode; `
|
winget install -e --id Microsoft.VisualStudioCode; `
|
||||||
winget install -e --id Google.AndroidStudio; `
|
winget install -e --id Google.AndroidStudio; `
|
||||||
winget install -e --id OpenJS.NodeJS --version 16.12.0; #v17 has issues with openssl
|
winget install -e --id OpenJS.NodeJS --version 20.11.0;
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
@@ -62,7 +61,7 @@ Your Windows environment should now be set up and ready to proceed!
|
|||||||
Required Software:
|
Required Software:
|
||||||
|
|
||||||
* [Android Studio](https://developer.android.com/studio)
|
* [Android Studio](https://developer.android.com/studio)
|
||||||
* [Node.js](https://nodejs.org/en/)
|
* [Node.js](https://nodejs.org/en/) (version 20)
|
||||||
* [Cocoapods](https://guides.cocoapods.org/using/getting-started.html#installation)
|
* [Cocoapods](https://guides.cocoapods.org/using/getting-started.html#installation)
|
||||||
* [Android SDK](https://developer.android.com/studio)
|
* [Android SDK](https://developer.android.com/studio)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -64,15 +64,15 @@ export const getters = {
|
|||||||
return state.serverSettings[key]
|
return state.serverSettings[key]
|
||||||
},
|
},
|
||||||
getJumpForwardTime: state => {
|
getJumpForwardTime: state => {
|
||||||
if (!state.deviceData || !state.deviceData.deviceSettings) return 10
|
if (!state.deviceData?.deviceSettings) return 10
|
||||||
return state.deviceData.deviceSettings.jumpForwardTime || 10
|
return state.deviceData.deviceSettings.jumpForwardTime || 10
|
||||||
},
|
},
|
||||||
getJumpBackwardsTime: state => {
|
getJumpBackwardsTime: state => {
|
||||||
if (!state.deviceData || !state.deviceData.deviceSettings) return 10
|
if (!state.deviceData?.deviceSettings) return 10
|
||||||
return state.deviceData.deviceSettings.jumpBackwardsTime || 10
|
return state.deviceData.deviceSettings.jumpBackwardsTime || 10
|
||||||
},
|
},
|
||||||
getAltViewEnabled: state => {
|
getAltViewEnabled: state => {
|
||||||
if (!state.deviceData || !state.deviceData.deviceSettings) return false
|
if (!state.deviceData?.deviceSettings) return true
|
||||||
return state.deviceData.deviceSettings.enableAltView
|
return state.deviceData.deviceSettings.enableAltView
|
||||||
},
|
},
|
||||||
getOrientationLockSetting: state => {
|
getOrientationLockSetting: state => {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Popis",
|
"LabelDescription": "Popis",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Jazyk",
|
"LabelLanguage": "Jazyk",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Řádkování",
|
"LabelLineSpacing": "Řádkování",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Interpret",
|
"LabelNarrator": "Interpret",
|
||||||
"LabelNarrators": "Interpreti",
|
"LabelNarrators": "Interpreti",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nedokončeno",
|
"LabelNotFinished": "Nedokončeno",
|
||||||
"LabelNotStarted": "Nezahájeno",
|
"LabelNotStarted": "Nezahájeno",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Datum vydání",
|
"LabelPubDate": "Datum vydání",
|
||||||
"LabelPublishYear": "Rok vydání",
|
"LabelPublishYear": "Rok vydání",
|
||||||
"LabelRead": "Číst",
|
"LabelRead": "Číst",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
||||||
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
|
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
|
||||||
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
|
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
|
||||||
"LabelRSSFeedSlug": "RSS kanál Slug",
|
"LabelRSSFeedSlug": "RSS kanál Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Sezóna",
|
"LabelSeason": "Sezóna",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Série",
|
"LabelSeries": "Série",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beskrivelse",
|
"LabelDescription": "Beskrivelse",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Sprog",
|
"LabelLanguage": "Sprog",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Linjeafstand",
|
"LabelLineSpacing": "Linjeafstand",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Fortæller",
|
"LabelNarrator": "Fortæller",
|
||||||
"LabelNarrators": "Fortællere",
|
"LabelNarrators": "Fortællere",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Ikke færdig",
|
"LabelNotFinished": "Ikke færdig",
|
||||||
"LabelNotStarted": "Ikke påbegyndt",
|
"LabelNotStarted": "Ikke påbegyndt",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Udgivelsesdato",
|
"LabelPubDate": "Udgivelsesdato",
|
||||||
"LabelPublishYear": "Udgivelsesår",
|
"LabelPublishYear": "Udgivelsesår",
|
||||||
"LabelRead": "Læst",
|
"LabelRead": "Læst",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
|
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
|
||||||
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
|
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
|
||||||
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
|
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
|
||||||
"LabelRSSFeedSlug": "RSS-feed-slug",
|
"LabelRSSFeedSlug": "RSS-feed-slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Sæson",
|
"LabelSeason": "Sæson",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
|
|||||||
+59
-52
@@ -17,36 +17,36 @@
|
|||||||
"ButtonDeleteLocalEpisode": "Lösche lokale Episode",
|
"ButtonDeleteLocalEpisode": "Lösche lokale Episode",
|
||||||
"ButtonDeleteLocalFile": "Lösche lokale Datei",
|
"ButtonDeleteLocalFile": "Lösche lokale Datei",
|
||||||
"ButtonDeleteLocalItem": "Lösche lokales Element",
|
"ButtonDeleteLocalItem": "Lösche lokales Element",
|
||||||
"ButtonDisableAutoTimer": "Deaktiviere Automatischer-Timer",
|
"ButtonDisableAutoTimer": "Deaktiviere automatischen Timer",
|
||||||
"ButtonDisconnect": "Trennen",
|
"ButtonDisconnect": "Trennen",
|
||||||
"ButtonHistory": "Historie",
|
"ButtonHistory": "Historie",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
"ButtonIssues": "Probleme",
|
"ButtonIssues": "Probleme",
|
||||||
"ButtonLatest": "Neuste",
|
"ButtonLatest": "Neueste",
|
||||||
"ButtonLibrary": "Bibliothek",
|
"ButtonLibrary": "Bibliothek",
|
||||||
"ButtonLocalMedia": "Lokale Medien",
|
"ButtonLocalMedia": "Lokale Medien",
|
||||||
"ButtonManageLocalFiles": "Verwalte lokale Dateien",
|
"ButtonManageLocalFiles": "Verwalte lokale Dateien",
|
||||||
"ButtonNewFolder": "Neuer Ordner",
|
"ButtonNewFolder": "Neuer Ordner",
|
||||||
"ButtonNextEpisode": "Nächste Episode",
|
"ButtonNextEpisode": "Nächste Episode",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
"ButtonOverride": "Überschreieben",
|
"ButtonOverride": "Überschreiben",
|
||||||
"ButtonPause": "Pause",
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Abspielen",
|
"ButtonPlay": "Abspielen",
|
||||||
"ButtonPlaying": "Spielt",
|
"ButtonPlaying": "Spielt",
|
||||||
"ButtonPlaylists": "Wiedergabelisten",
|
"ButtonPlaylists": "Wiedergabelisten",
|
||||||
"ButtonRead": "Lese",
|
"ButtonRead": "Lesen",
|
||||||
"ButtonRemove": "Löschen",
|
"ButtonRemove": "Löschen",
|
||||||
"ButtonRemoveFromServer": "Vom Server entfernen",
|
"ButtonRemoveFromServer": "Vom Server entfernen",
|
||||||
"ButtonSave": "Speichern",
|
"ButtonSave": "Speichern",
|
||||||
"ButtonSaveOrder": "Speicher Reihenfolge",
|
"ButtonSaveOrder": "Reihenfolge speichern",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSendEbookToDevice": "Sende Ebook an Gerät",
|
"ButtonSendEbookToDevice": "Sende E-Book an Gerät",
|
||||||
"ButtonSeries": "Serien",
|
"ButtonSeries": "Serien",
|
||||||
"ButtonSetTimer": "Setze Timer",
|
"ButtonSetTimer": "Setze Timer",
|
||||||
"ButtonStream": "Stream",
|
"ButtonStream": "Stream",
|
||||||
"ButtonSubmit": "Ok",
|
"ButtonSubmit": "Ok",
|
||||||
"ButtonSwitchServerUser": "Wechsle Server/Benutzer",
|
"ButtonSwitchServerUser": "Wechsle Server/Benutzer",
|
||||||
"ButtonUserStats": "Nutzer Statistiken",
|
"ButtonUserStats": "Nutzerstatistiken",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
"HeaderAdvanced": "Erweitert",
|
"HeaderAdvanced": "Erweitert",
|
||||||
@@ -54,13 +54,13 @@
|
|||||||
"HeaderChapters": "Kapitel",
|
"HeaderChapters": "Kapitel",
|
||||||
"HeaderCollection": "Sammlungen",
|
"HeaderCollection": "Sammlungen",
|
||||||
"HeaderCollectionItems": "Sammlungseinträge",
|
"HeaderCollectionItems": "Sammlungseinträge",
|
||||||
"HeaderConnectionStatus": "Verbindungs Status",
|
"HeaderConnectionStatus": "Verbindungsstatus",
|
||||||
"HeaderDetails": "Einzelheiten",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloads": "Downloads",
|
"HeaderDownloads": "Downloads",
|
||||||
"HeaderEbookFiles": "E-Book Dateien",
|
"HeaderEbookFiles": "E-Book Dateien",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderEreaderSettings": "Ereader Einstellungen",
|
"HeaderEreaderSettings": "E-Reader Einstellungen",
|
||||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
"HeaderLatestEpisodes": "Neueste Episoden",
|
||||||
"HeaderLibraries": "Bibliotheken",
|
"HeaderLibraries": "Bibliotheken",
|
||||||
"HeaderLocalFolders": "Lokale Ordner",
|
"HeaderLocalFolders": "Lokale Ordner",
|
||||||
"HeaderLocalLibraryItems": "Lokale Bibliothek Elemente",
|
"HeaderLocalLibraryItems": "Lokale Bibliothek Elemente",
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||||
"HeaderSelectDownloadLocation": "Wähle Download-Speicherort",
|
"HeaderSelectDownloadLocation": "Wähle Download-Speicherort",
|
||||||
"HeaderSettings": "Einstellungen",
|
"HeaderSettings": "Einstellungen",
|
||||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
"HeaderSleepTimer": "Sleep-Timer",
|
||||||
"HeaderSleepTimerSettings": "Einschlaf-Timer Einstellungen",
|
"HeaderSleepTimerSettings": "Sleep-Timer Einstellungen",
|
||||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
"HeaderTableOfContents": "Inhaltsverzeichnis",
|
"HeaderTableOfContents": "Inhaltsverzeichnis",
|
||||||
@@ -91,10 +91,10 @@
|
|||||||
"LabelAuthors": "Autoren",
|
"LabelAuthors": "Autoren",
|
||||||
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
|
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
|
||||||
"LabelAutoRewindTime": "Automatische Rückspulzeit",
|
"LabelAutoRewindTime": "Automatische Rückspulzeit",
|
||||||
"LabelAutoSleepTimer": "Automatischer Einschlaf-Timer",
|
"LabelAutoSleepTimer": "Automatischer Sleep-Timer",
|
||||||
"LabelAutoSleepTimerAutoRewind": "Automatischer Einschlaf-Timer automatische Rückspulzeit",
|
"LabelAutoSleepTimerAutoRewind": "Automatischer Sleep-Timer automatische Rückspulzeit",
|
||||||
"LabelAutoSleepTimerAutoRewindHelp": "Wenn die Einschlaf-Timer abgelaufen ist, wird bei der erneuten Wiedergabe des Titels die Position automatisch zurückgespult.",
|
"LabelAutoSleepTimerAutoRewindHelp": "Wenn der Sleep-Timer abgelaufen ist, wird bei der erneuten Wiedergabe des Titels die Position automatisch zurückgespult.",
|
||||||
"LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch ein Einschlaf-Timer gestartet.",
|
"LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch ein Sleep-Timer gestartet.",
|
||||||
"LabelBooks": "Bücher",
|
"LabelBooks": "Bücher",
|
||||||
"LabelChapters": "Kapitel",
|
"LabelChapters": "Kapitel",
|
||||||
"LabelChapterTrack": "Kapitel Spur",
|
"LabelChapterTrack": "Kapitel Spur",
|
||||||
@@ -104,25 +104,26 @@
|
|||||||
"LabelContinueBooks": "Bücher fortfahren",
|
"LabelContinueBooks": "Bücher fortfahren",
|
||||||
"LabelContinueEpisodes": "Episoden fortfahren",
|
"LabelContinueEpisodes": "Episoden fortfahren",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
|
"LabelContinueReading": "Weiterlesen",
|
||||||
"LabelContinueSeries": "Serie fortsetzen",
|
"LabelContinueSeries": "Serie fortsetzen",
|
||||||
"LabelCustomTime": "Benutzerdefinierte zeit",
|
"LabelCustomTime": "Benutzerdefinierte Zeit",
|
||||||
"LabelDescription": "Beschreibung",
|
"LabelDescription": "Beschreibung",
|
||||||
"LabelDisableAudioFadeOut": "Deaktiviere Audio abklingen",
|
"LabelDisableAudioFadeOut": "Deaktiviere Audio abklingen",
|
||||||
"LabelDisableAudioFadeOutHelp": "Die Audiolautstärke wird verringert, wenn weniger als 1 Minute für den Einschlaf-Timer verbleibt. Aktivieren Sie diese Einstellung, um nicht abzuklingen.",
|
"LabelDisableAudioFadeOutHelp": "Die Audiolautstärke wird verringert, wenn weniger als 1 Minute für den Sleep-Timer verbleibt. Aktiviere diese Einstellung, um nicht abzuklingen.",
|
||||||
"LabelDisableAutoRewind": "Deaktiviere automatisches Rückspulen",
|
"LabelDisableAutoRewind": "Deaktiviere automatisches Zurückspulen",
|
||||||
"LabelDisableShakeToReset": "Deaktiviere Schütteln zum Zurücksetzen",
|
"LabelDisableShakeToReset": "Deaktiviere Schütteln zum Zurücksetzen",
|
||||||
"LabelDisableShakeToResetHelp": "Wenn Sie Ihr Gerät schütteln, während der Timer läuft ODER innerhalb von 2 Minuten nach Ablauf des Timers, wird der Sleep-Timer zurückgesetzt. Aktivieren Sie diese Einstellung, um das Schütteln zum Zurücksetzen zu deaktivieren.",
|
"LabelDisableShakeToResetHelp": "Wenn du dein Gerät schüttelst, während der Timer läuft ODER innerhalb von 2 Minuten nach Ablauf des Timers, wird der Sleep-Timer zurückgesetzt. Aktiviere diese Einstellung, um das Schütteln zum Zurücksetzen zu deaktivieren.",
|
||||||
"LabelDisableVibrateOnReset": "Deaktiviere vibrieren beim Zurücksetzen",
|
"LabelDisableVibrateOnReset": "Deaktiviere Vibrieren beim Zurücksetzen",
|
||||||
"LabelDisableVibrateOnResetHelp": "Wenn der Einschlaf-Timer zurückgesetzt wird, vibriert Ihr Gerät. Aktivieren Sie diese Einstellung, um nicht zu vibrieren, wenn der Einschlaf-Timer zurückgesetzt wird.",
|
"LabelDisableVibrateOnResetHelp": "Wenn der Sleep-Timer zurückgesetzt wird, vibriert dein Gerät. Aktiviere diese Einstellung, um nicht zu vibrieren, wenn der Sleep-Timer zurückgesetzt wird.",
|
||||||
"LabelDiscover": "Entdecken",
|
"LabelDiscover": "Entdecken",
|
||||||
"LabelDownload": "Herunterladen",
|
"LabelDownload": "Herunterladen",
|
||||||
"LabelDownloaded": "Heruntergeladen",
|
"LabelDownloaded": "Heruntergeladen",
|
||||||
"LabelDuration": "Laufzeit",
|
"LabelDuration": "Laufzeit",
|
||||||
"LabelEbook": "E-Book",
|
"LabelEbook": "E-Book",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "E-Books",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnableMp3IndexSeeking": "Aktiviere MP3-Indexsuche",
|
"LabelEnableMp3IndexSeeking": "MP3-Indexsuche",
|
||||||
"LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn Sie MP3-Dateien haben, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.",
|
"LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn du MP3-Dateien hast, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
"LabelEndOfChapter": "Kapitelende",
|
"LabelEndOfChapter": "Kapitelende",
|
||||||
"LabelEndTime": "Endzeit",
|
"LabelEndTime": "Endzeit",
|
||||||
@@ -132,15 +133,15 @@
|
|||||||
"LabelFileBirthtime": "Datei erstellt",
|
"LabelFileBirthtime": "Datei erstellt",
|
||||||
"LabelFileModified": "Datei geändert",
|
"LabelFileModified": "Datei geändert",
|
||||||
"LabelFilename": "Dateiname",
|
"LabelFilename": "Dateiname",
|
||||||
"LabelFinished": "beendet",
|
"LabelFinished": "Beendet",
|
||||||
"LabelFolder": "Ordner",
|
"LabelFolder": "Ordner",
|
||||||
"LabelFontScale": "Schriftgröße",
|
"LabelFontScale": "Schriftgröße",
|
||||||
"LabelGenre": "Kategorie",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
"LabelHapticFeedback": "Haptische Rückmeldung",
|
"LabelHapticFeedback": "Haptische Rückmeldung",
|
||||||
"LabelHasEbook": "Hat Ebook",
|
"LabelHasEbook": "E-Book verfügbar",
|
||||||
"LabelHasSupplementaryEbook": "Hat zusätzliches Ebook",
|
"LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar",
|
||||||
"LabelHeavy": "Schwer",
|
"LabelHeavy": "Stark",
|
||||||
"LabelHigh": "Hoch",
|
"LabelHigh": "Hoch",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelIncomplete": "Unvollständig",
|
"LabelIncomplete": "Unvollständig",
|
||||||
@@ -149,24 +150,28 @@
|
|||||||
"LabelJumpBackwardsTime": "Rückspulzeit",
|
"LabelJumpBackwardsTime": "Rückspulzeit",
|
||||||
"LabelJumpForwardsTime": "Vorwärtsspulzeit",
|
"LabelJumpForwardsTime": "Vorwärtsspulzeit",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLight": "Licht",
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Einzelne Seite",
|
||||||
|
"LabelLight": "Leicht",
|
||||||
"LabelLineSpacing": "Zeilenabstand",
|
"LabelLineSpacing": "Zeilenabstand",
|
||||||
"LabelListenAgain": "Erneut anhören",
|
"LabelListenAgain": "Erneut anhören",
|
||||||
"LabelLocalBooks": "Lokale Bücher",
|
"LabelLocalBooks": "Lokale Bücher",
|
||||||
"LabelLocalPodcasts": "Lokale Podcasts",
|
"LabelLocalPodcasts": "Lokale Podcasts",
|
||||||
"LabelLockOrientation": "Ausrichtung sperren",
|
"LabelLockOrientation": "Automatische Bildschirmausrichtung sperren",
|
||||||
"LabelLockPlayer": "Mediaplayer sperren",
|
"LabelLockPlayer": "Mediaplayer sperren",
|
||||||
"LabelLow": "Niedrig",
|
"LabelLow": "Wenig",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
"LabelMedium": "Mittel",
|
"LabelMedium": "Mittel",
|
||||||
"LabelMore": "Mehr",
|
"LabelMore": "Mehr",
|
||||||
"LabelMoreInfo": "Mehr Info",
|
"LabelMoreInfo": "Mehr Infos",
|
||||||
"LabelName": "Name",
|
"LabelName": "Name",
|
||||||
"LabelNarrator": "Erzähler",
|
"LabelNarrator": "Erzähler",
|
||||||
"LabelNarrators": "Erzähler",
|
"LabelNarrators": "Erzähler",
|
||||||
"LabelNewestAuthors": "Neueste Autoren",
|
"LabelNewestAuthors": "Neueste Autoren",
|
||||||
|
"LabelNewestEpisodes": "Neueste Episoden",
|
||||||
"LabelNo": "Nein",
|
"LabelNo": "Nein",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "Nicht beendet",
|
||||||
"LabelNotStarted": "Nicht begonnen",
|
"LabelNotStarted": "Nicht begonnen",
|
||||||
"LabelOff": "Aus",
|
"LabelOff": "Aus",
|
||||||
"LabelPassword": "Passwort",
|
"LabelPassword": "Passwort",
|
||||||
@@ -182,29 +187,31 @@
|
|||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRead": "Lesen",
|
"LabelRead": "Lesen",
|
||||||
|
"LabelReadAgain": "Erneut lesen",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
|
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
|
||||||
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
|
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
|
||||||
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
|
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Vergangene Zeit anhand der Geschwindigkeit skalieren",
|
||||||
"LabelSeason": "Staffel",
|
"LabelSeason": "Staffel",
|
||||||
"LabelSelectADevice": "Wähle ein Gerät",
|
"LabelSelectADevice": "Wähle ein Gerät",
|
||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelServerAddress": "Server Adresse",
|
"LabelServerAddress": "Server Adresse",
|
||||||
"LabelSetEbookAsPrimary": "Setzen als Hauptbuch",
|
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
|
||||||
"LabelSetEbookAsSupplementary": "Setzen als Ergänzung",
|
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
|
||||||
"LabelShakeSensitivity": "Schüttel-Empfindlichkeit",
|
"LabelShakeSensitivity": "Schüttel-Empfindlichkeit",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Sleep-Timer",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStartTime": "Startzeit",
|
"LabelStartTime": "Startzeit",
|
||||||
"LabelStatsBestDay": "Bester Tag",
|
"LabelStatsBestDay": "Bester Tag",
|
||||||
"LabelStatsDailyAverage": "Tagesdurchschnitt",
|
"LabelStatsDailyAverage": "Tagesdurchschnitt",
|
||||||
"LabelStatsDays": "Tage",
|
"LabelStatsDays": "Tage",
|
||||||
"LabelStatsDaysListened": "Gehörte Tage",
|
"LabelStatsDaysListened": "Gehörte Tage",
|
||||||
"LabelStatsInARow": "nacheinander",
|
"LabelStatsInARow": "Nacheinander",
|
||||||
"LabelStatsItemsFinished": "Gehörte Medien",
|
"LabelStatsItemsFinished": "Gehörte Medien",
|
||||||
"LabelStatsMinutes": "Minuten",
|
"LabelStatsMinutes": "Minuten",
|
||||||
"LabelStatsMinutesListening": "Gehörte Minuten",
|
"LabelStatsMinutesListening": "Gehörte Minuten",
|
||||||
@@ -213,11 +220,11 @@
|
|||||||
"LabelTags": "Schlagwörter",
|
"LabelTags": "Schlagwörter",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dunkel",
|
"LabelThemeDark": "Dunkel",
|
||||||
"LabelThemeLight": "Licht",
|
"LabelThemeLight": "Hell",
|
||||||
"LabelTimeRemaining": "{0} verbleibend",
|
"LabelTimeRemaining": "{0} verbleibend",
|
||||||
"LabelTitle": "Titel",
|
"LabelTitle": "Titel",
|
||||||
"LabelTotalTrack": "Total Track",
|
"LabelTotalTrack": "Gesamtanzahl Titel",
|
||||||
"LabelTracks": "Dateien",
|
"LabelTracks": "Titel",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnlockPlayer": "Mediaplayer entsperren",
|
"LabelUnlockPlayer": "Mediaplayer entsperren",
|
||||||
"LabelUseBookshelfView": "Benutze Bücherregalansicht",
|
"LabelUseBookshelfView": "Benutze Bücherregalansicht",
|
||||||
@@ -230,16 +237,16 @@
|
|||||||
"MessageAndroid10Downloads": "Bei Android 10 und darunter wird der interne App-Speicher für Downloads verwendet.",
|
"MessageAndroid10Downloads": "Bei Android 10 und darunter wird der interne App-Speicher für Downloads verwendet.",
|
||||||
"MessageAttemptingServerConnection": "Versuch einer Serververbindung...",
|
"MessageAttemptingServerConnection": "Versuch einer Serververbindung...",
|
||||||
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf Server nicht verbunden",
|
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf Server nicht verbunden",
|
||||||
"MessageAudiobookshelfServerRequired": "<strong>Wichtig!</strong> Diese App wurde entwickelt, um mit einem Audiobookshelf-Server zu arbeiten, den Sie oder jemand, den Sie kennen, hostet. Diese App stellt keine Inhalte zur Verfügung.",
|
"MessageAudiobookshelfServerRequired": "<strong>Wichtig!</strong> Diese App wurde entwickelt, um mit einem Audiobookshelf-Server zu arbeiten, den du oder jemand, den du kennst, hostet. Diese App stellt keine Inhalte zur Verfügung.",
|
||||||
"MessageBookshelfEmpty": "Bücherregal leer",
|
"MessageBookshelfEmpty": "Bücherregal leer",
|
||||||
"MessageConfirmDeleteLocalEpisode": "Entferne lokale Episode \"{0}\" von Ihrem Gerät? Die Datei auf dem Server bleibt davon unberührt.",
|
"MessageConfirmDeleteLocalEpisode": "Soll die lokale Episode \"{0}\" von deinem Gerät entfernt werden? Die Datei auf dem Server bleibt davon unberührt.",
|
||||||
"MessageConfirmDeleteLocalFiles": "Entfernen Sie lokale Dateien dieses Elements von Ihrem Gerät? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.",
|
"MessageConfirmDeleteLocalFiles": "Sollen lokale Dateien dieses Elements von deinem Gerät entfernt werden? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.",
|
||||||
"MessageConfirmDiscardProgress": "Sind Sie sicher, dass Sie Ihren Fortschritt zurücksetzen wollen?",
|
"MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?",
|
||||||
"MessageConfirmMarkAsFinished": "Sind Sie sicher, dass Sie diesen Artikel als beendet markieren wollen?",
|
"MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?",
|
||||||
"MessageConfirmRemoveBookmark": "Sind Sie sicher, dass Sie das Lesezeichen entfernen möchten?",
|
"MessageConfirmRemoveBookmark": "Bist du sicher, dass du das Lesezeichen entfernen willst?",
|
||||||
"MessageDiscardProgress": "Verwerfe Fortschritt",
|
"MessageDiscardProgress": "Fortschritt verwerfen",
|
||||||
"MessageDownloadCompleteProcessing": "Download abgeschlossen. Verarbeite...",
|
"MessageDownloadCompleteProcessing": "Download abgeschlossen. Verarbeite...",
|
||||||
"MessageDownloading": "Downloaded...",
|
"MessageDownloading": "Herunterladen...",
|
||||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
||||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||||
@@ -264,14 +271,14 @@
|
|||||||
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
|
||||||
"MessageSocketConnectedOverMeteredCellular": "Socket verbunden über getaktetes Mobilfunknetz",
|
"MessageSocketConnectedOverMeteredCellular": "Socket verbunden über getaktetes Mobilfunknetz",
|
||||||
"MessageSocketConnectedOverMeteredWifi": "Socket verbunden über getaktetes WLAN",
|
"MessageSocketConnectedOverMeteredWifi": "Socket verbunden über getaktetes WLAN",
|
||||||
"MessageSocketConnectedOverUnmeteredCellular": "Socket verbunden über ungetaktetes Mobilfunknetz",
|
"MessageSocketConnectedOverUnmeteredCellular": "Socket verbunden über ungetaktetes Mobilfunknetz",
|
||||||
"MessageSocketConnectedOverUnmeteredWifi": "Socket verbunden über ungetaktetes Mobilfunknetz",
|
"MessageSocketConnectedOverUnmeteredWifi": "Socket verbunden über ungetaktetes Mobilfunknetz",
|
||||||
"MessageSocketNotConnected": "Socket nicht verbunden",
|
"MessageSocketNotConnected": "Socket nicht verbunden",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere deiner Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
||||||
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
|
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
|
||||||
"ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht gelöscht werden",
|
"ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht gelöscht werden",
|
||||||
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLineSpacing": "Line spacing",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Narrators",
|
"LabelNarrators": "Narrators",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Descripción",
|
"LabelDescription": "Descripción",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Lenguaje",
|
"LabelLanguage": "Lenguaje",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Interlineado",
|
"LabelLineSpacing": "Interlineado",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Narrador",
|
"LabelNarrator": "Narrador",
|
||||||
"LabelNarrators": "Narradores",
|
"LabelNarrators": "Narradores",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "No Terminado",
|
"LabelNotFinished": "No Terminado",
|
||||||
"LabelNotStarted": "Sin Iniciar",
|
"LabelNotStarted": "Sin Iniciar",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Fecha de Publicación",
|
"LabelPubDate": "Fecha de Publicación",
|
||||||
"LabelPublishYear": "Año de Publicación",
|
"LabelPublishYear": "Año de Publicación",
|
||||||
"LabelRead": "Leído",
|
"LabelRead": "Leído",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
|
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevenir Indexado",
|
"LabelRSSFeedPreventIndexing": "Prevenir Indexado",
|
||||||
"LabelRSSFeedSlug": "Fuente RSS Slug",
|
"LabelRSSFeedSlug": "Fuente RSS Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Temporada",
|
"LabelSeason": "Temporada",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Langue",
|
"LabelLanguage": "Langue",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Interligne",
|
"LabelLineSpacing": "Interligne",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Narrateur",
|
"LabelNarrator": "Narrateur",
|
||||||
"LabelNarrators": "Narrateurs",
|
"LabelNarrators": "Narrateurs",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Non terminé(e)",
|
"LabelNotFinished": "Non terminé(e)",
|
||||||
"LabelNotStarted": "Non Démarré(e)",
|
"LabelNotStarted": "Non Démarré(e)",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublishYear": "Année d’édition",
|
"LabelPublishYear": "Année d’édition",
|
||||||
"LabelRead": "Lire",
|
"LabelRead": "Lire",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
||||||
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
||||||
"LabelRSSFeedSlug": "Identificateur d’adresse du Flux RSS ",
|
"LabelRSSFeedSlug": "Identificateur d’adresse du Flux RSS ",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Saison",
|
"LabelSeason": "Saison",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLineSpacing": "Line spacing",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Narrators",
|
"LabelNarrators": "Narrators",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLineSpacing": "Line spacing",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Narrators",
|
"LabelNarrators": "Narrators",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Jezik",
|
"LabelLanguage": "Jezik",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLineSpacing": "Line spacing",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Naratori",
|
"LabelNarrators": "Naratori",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nedovršeno",
|
"LabelNotFinished": "Nedovršeno",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Datam izdavanja",
|
"LabelPubDate": "Datam izdavanja",
|
||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Sezona",
|
"LabelSeason": "Sezona",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Serije",
|
"LabelSeries": "Serije",
|
||||||
|
|||||||
+114
-107
@@ -2,7 +2,7 @@
|
|||||||
"ButtonAdd": "Aggiungi",
|
"ButtonAdd": "Aggiungi",
|
||||||
"ButtonAddNewServer": "Aggiungi Nuovo Server",
|
"ButtonAddNewServer": "Aggiungi Nuovo Server",
|
||||||
"ButtonAuthors": "Autori",
|
"ButtonAuthors": "Autori",
|
||||||
"ButtonBack": "Back",
|
"ButtonBack": "Indietro",
|
||||||
"ButtonCancel": "Cancella",
|
"ButtonCancel": "Cancella",
|
||||||
"ButtonCancelTimer": "Cancella Timer",
|
"ButtonCancelTimer": "Cancella Timer",
|
||||||
"ButtonClearFilter": "Elimina Filtri",
|
"ButtonClearFilter": "Elimina Filtri",
|
||||||
@@ -15,38 +15,38 @@
|
|||||||
"ButtonCreateNewPlaylist": "Crea Nuova Playlist",
|
"ButtonCreateNewPlaylist": "Crea Nuova Playlist",
|
||||||
"ButtonDelete": "Elimina",
|
"ButtonDelete": "Elimina",
|
||||||
"ButtonDeleteLocalEpisode": "Cancella episodio locale",
|
"ButtonDeleteLocalEpisode": "Cancella episodio locale",
|
||||||
"ButtonDeleteLocalFile": "Delete local file",
|
"ButtonDeleteLocalFile": "Elimina File locale",
|
||||||
"ButtonDeleteLocalItem": "Delete local item",
|
"ButtonDeleteLocalItem": "Elimina oggetto locale",
|
||||||
"ButtonDisableAutoTimer": "Disable Auto Timer",
|
"ButtonDisableAutoTimer": "Disattiva Auto Timer",
|
||||||
"ButtonDisconnect": "Disconnect",
|
"ButtonDisconnect": "Disconnetti",
|
||||||
"ButtonHistory": "History",
|
"ButtonHistory": "Cronologia",
|
||||||
"ButtonHome": "Home",
|
"ButtonHome": "Home",
|
||||||
"ButtonIssues": "Errori",
|
"ButtonIssues": "Errori",
|
||||||
"ButtonLatest": "Ultimi",
|
"ButtonLatest": "Ultimi",
|
||||||
"ButtonLibrary": "Libreria",
|
"ButtonLibrary": "Libreria",
|
||||||
"ButtonLocalMedia": "Local Media",
|
"ButtonLocalMedia": "Media Locali",
|
||||||
"ButtonManageLocalFiles": "Manage Local Files",
|
"ButtonManageLocalFiles": "Gestisci file Locali",
|
||||||
"ButtonNewFolder": "New Folder",
|
"ButtonNewFolder": "Nuova Cartella",
|
||||||
"ButtonNextEpisode": "Next Episode",
|
"ButtonNextEpisode": "Prossimo Episodio",
|
||||||
"ButtonOpenFeed": "Apri Feed",
|
"ButtonOpenFeed": "Apri Feed",
|
||||||
"ButtonOverride": "Override",
|
"ButtonOverride": "Oltrepassa",
|
||||||
"ButtonPause": "Pause",
|
"ButtonPause": "Pausa",
|
||||||
"ButtonPlay": "Play",
|
"ButtonPlay": "Play",
|
||||||
"ButtonPlaying": "In Riproduzione",
|
"ButtonPlaying": "In Riproduzione",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlists",
|
||||||
"ButtonRead": "Leggi",
|
"ButtonRead": "Leggi",
|
||||||
"ButtonRemove": "Rimuovi",
|
"ButtonRemove": "Rimuovi",
|
||||||
"ButtonRemoveFromServer": "Remove from Server",
|
"ButtonRemoveFromServer": "Rimuovi dal server",
|
||||||
"ButtonSave": "Salva",
|
"ButtonSave": "Salva",
|
||||||
"ButtonSaveOrder": "Save Order",
|
"ButtonSaveOrder": "Salva Ordine",
|
||||||
"ButtonSearch": "Cerca",
|
"ButtonSearch": "Cerca",
|
||||||
"ButtonSendEbookToDevice": "Send Ebook to Device",
|
"ButtonSendEbookToDevice": "Invia Ebook al Device",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
"ButtonSetTimer": "Set Timer",
|
"ButtonSetTimer": "Imposta Timer",
|
||||||
"ButtonStream": "Stream",
|
"ButtonStream": "Stream",
|
||||||
"ButtonSubmit": "Invia",
|
"ButtonSubmit": "Invia",
|
||||||
"ButtonSwitchServerUser": "Switch Server/User",
|
"ButtonSwitchServerUser": "Cambia Server/Utente",
|
||||||
"ButtonUserStats": "User Stats",
|
"ButtonUserStats": "Statistiche Utente",
|
||||||
"ButtonYes": "Si",
|
"ButtonYes": "Si",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
"HeaderAdvanced": "Avanzate",
|
"HeaderAdvanced": "Avanzate",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"HeaderChapters": "Capitoli",
|
"HeaderChapters": "Capitoli",
|
||||||
"HeaderCollection": "Raccolta",
|
"HeaderCollection": "Raccolta",
|
||||||
"HeaderCollectionItems": "Elementi della Raccolta",
|
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||||
"HeaderConnectionStatus": "Connection Status",
|
"HeaderConnectionStatus": "Stato Connessione",
|
||||||
"HeaderDetails": "Dettagli",
|
"HeaderDetails": "Dettagli",
|
||||||
"HeaderDownloads": "Downloads",
|
"HeaderDownloads": "Downloads",
|
||||||
"HeaderEbookFiles": "Ebook File",
|
"HeaderEbookFiles": "Ebook File",
|
||||||
@@ -62,24 +62,24 @@
|
|||||||
"HeaderEreaderSettings": "Impostazioni Ereader",
|
"HeaderEreaderSettings": "Impostazioni Ereader",
|
||||||
"HeaderLatestEpisodes": "Ultimi Episodi",
|
"HeaderLatestEpisodes": "Ultimi Episodi",
|
||||||
"HeaderLibraries": "Librerie",
|
"HeaderLibraries": "Librerie",
|
||||||
"HeaderLocalFolders": "Local Folders",
|
"HeaderLocalFolders": "Cartelle Locali",
|
||||||
"HeaderLocalLibraryItems": "Local Library Items",
|
"HeaderLocalLibraryItems": "Oggetti Libreria Locale",
|
||||||
"HeaderNewPlaylist": "New Playlist",
|
"HeaderNewPlaylist": "Nuova Playlist",
|
||||||
"HeaderOpenRSSFeed": "Apri RSS Feed",
|
"HeaderOpenRSSFeed": "Apri RSS Feed",
|
||||||
"HeaderPlaybackSettings": "Playback Settings",
|
"HeaderPlaybackSettings": "Impostazioni Playback",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Playlist",
|
||||||
"HeaderPlaylistItems": "Elementi della playlist",
|
"HeaderPlaylistItems": "Elementi della playlist",
|
||||||
"HeaderRSSFeed": "RSS Feed",
|
"HeaderRSSFeed": "RSS Feed",
|
||||||
"HeaderRSSFeedGeneral": "RSS Details",
|
"HeaderRSSFeedGeneral": "RSS Dettagli",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
||||||
"HeaderSelectDownloadLocation": "Select Download Location",
|
"HeaderSelectDownloadLocation": "Selezione Posizione Download",
|
||||||
"HeaderSettings": "Impostazioni",
|
"HeaderSettings": "Impostazioni",
|
||||||
"HeaderSleepTimer": "Sveglia",
|
"HeaderSleepTimer": "Sveglia",
|
||||||
"HeaderSleepTimerSettings": "Sleep Timer Settings",
|
"HeaderSleepTimerSettings": "Impostazione tempo Sleep",
|
||||||
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
||||||
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
||||||
"HeaderTableOfContents": "Tabella dei Contenuti",
|
"HeaderTableOfContents": "Tabella dei Contenuti",
|
||||||
"HeaderUserInterfaceSettings": "User Interface Settings",
|
"HeaderUserInterfaceSettings": "Impostazioni Interfaccia Utente",
|
||||||
"HeaderYourStats": "Statistiche Personali",
|
"HeaderYourStats": "Statistiche Personali",
|
||||||
"LabelAdded": "Aggiunto",
|
"LabelAdded": "Aggiunto",
|
||||||
"LabelAddedAt": "Aggiunto il",
|
"LabelAddedAt": "Aggiunto il",
|
||||||
@@ -90,42 +90,43 @@
|
|||||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||||
"LabelAuthors": "Autori",
|
"LabelAuthors": "Autori",
|
||||||
"LabelAutoDownloadEpisodes": "Auto Download Episodi",
|
"LabelAutoDownloadEpisodes": "Auto Download Episodi",
|
||||||
"LabelAutoRewindTime": "Auto rewind time",
|
"LabelAutoRewindTime": "Auto Riavvolgimento",
|
||||||
"LabelAutoSleepTimer": "Auto sleep timer",
|
"LabelAutoSleepTimer": "Auto Ibernazione",
|
||||||
"LabelAutoSleepTimerAutoRewind": "Auto sleep timer auto rewind",
|
"LabelAutoSleepTimerAutoRewind": "Auto Ibernazione e riavvolgimento",
|
||||||
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
|
"LabelAutoSleepTimerAutoRewindHelp": "Al termine del timer di spegnimento automatico, la riproduzione dell'elemento riavvolgerà automaticamente la tua posizione.",
|
||||||
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
|
"LabelAutoSleepTimerHelp": "Durante la riproduzione di contenuti multimediali tra l'ora di inizio e quella di fine specificate, verrà avviato automaticamente un timer di spegnimento.",
|
||||||
"LabelBooks": "Libri",
|
"LabelBooks": "Libri",
|
||||||
"LabelChapters": "Capitoli",
|
"LabelChapters": "Capitoli",
|
||||||
"LabelChapterTrack": "Chapter Track",
|
"LabelChapterTrack": "Traccia Capitolo",
|
||||||
"LabelClosePlayer": "Chiudi player",
|
"LabelClosePlayer": "Chiudi player",
|
||||||
"LabelCollapseSeries": "Comprimi Serie",
|
"LabelCollapseSeries": "Comprimi Serie",
|
||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continua Libri",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continua Episodi",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continua Ascolto",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueReading": "Continua Lettura",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelContinueSeries": "Continua Serie",
|
||||||
|
"LabelCustomTime": "Personalizza tempo",
|
||||||
"LabelDescription": "Descrizione",
|
"LabelDescription": "Descrizione",
|
||||||
"LabelDisableAudioFadeOut": "Disable audio fade out",
|
"LabelDisableAudioFadeOut": "Disabilita audio fade out",
|
||||||
"LabelDisableAudioFadeOutHelp": "Audio volume will start decreasing when there is less than 1 minute remaining on the sleep timer. Enable this setting to not fade out.",
|
"LabelDisableAudioFadeOutHelp": "Il volume dell'audio inizierà a diminuire quando rimane meno di 1 minuto sul timer di spegnimento. Abilita questa impostazione per non diminuirlo.",
|
||||||
"LabelDisableAutoRewind": "Disable auto rewind",
|
"LabelDisableAutoRewind": "Disabilita auto riavvolgimento",
|
||||||
"LabelDisableShakeToReset": "Disable shake to reset",
|
"LabelDisableShakeToReset": "Disabilita shake to reset",
|
||||||
"LabelDisableShakeToResetHelp": "Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.",
|
"LabelDisableShakeToResetHelp": "Scuotendo il dispositivo mentre il timer è in esecuzione O entro 2 minuti dalla scadenza del timer si ripristinerà il timer di spegnimento. Abilita questa impostazione per disabilitare lo scuotimento per ripristinare.",
|
||||||
"LabelDisableVibrateOnReset": "Disable vibrate on reset",
|
"LabelDisableVibrateOnReset": "Disabilita vibrazione reset",
|
||||||
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.",
|
"LabelDisableVibrateOnResetHelp": "Quando il timer di spegnimento viene reimpostato, il dispositivo vibrerà. Abilita questa impostazione per non vibrare quando il timer di spegnimento viene reimpostato.",
|
||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "Scopri",
|
||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDownloaded": "Downloaded",
|
"LabelDownloaded": "Scaricati",
|
||||||
"LabelDuration": "Durata",
|
"LabelDuration": "Durata",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEnable": "Abilita",
|
"LabelEnable": "Abilita",
|
||||||
"LabelEnableMp3IndexSeeking": "Enable mp3 index seeking",
|
"LabelEnableMp3IndexSeeking": "Abilita la ricerca dell'indice mp3",
|
||||||
"LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.",
|
"LabelEnableMp3IndexSeekingHelp": "Questa impostazione dovrebbe essere abilitata solo se hai file mp3 che non vengono ricercati correttamente. La ricerca imprecisa è molto probabilmente dovuta ai file MP3 a birate variabile (VBR). Questa impostazione imporrà la ricerca dell'indice, in cui viene creata una mappatura tempo-byte durante la lettura del file. In alcuni casi, con file MP3 di grandi dimensioni, si verificherà un ritardo durante la ricerca verso la fine del file.",
|
||||||
"LabelEnd": "Fine",
|
"LabelEnd": "Fine",
|
||||||
"LabelEndOfChapter": "End of Chapter",
|
"LabelEndOfChapter": "End del capitolo",
|
||||||
"LabelEndTime": "End time",
|
"LabelEndTime": "Tempo alla fine",
|
||||||
"LabelEpisode": "Episodio",
|
"LabelEpisode": "Episodio",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "File",
|
"LabelFile": "File",
|
||||||
@@ -137,44 +138,48 @@
|
|||||||
"LabelFontScale": "Dimensione Font",
|
"LabelFontScale": "Dimensione Font",
|
||||||
"LabelGenre": "Genere",
|
"LabelGenre": "Genere",
|
||||||
"LabelGenres": "Generi",
|
"LabelGenres": "Generi",
|
||||||
"LabelHapticFeedback": "Haptic feedback",
|
"LabelHapticFeedback": "Feedback tattile",
|
||||||
"LabelHasEbook": "Has ebook",
|
"LabelHasEbook": "Ha l'ebook",
|
||||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
"LabelHasSupplementaryEbook": "Ha un ebook supplementale",
|
||||||
"LabelHeavy": "Heavy",
|
"LabelHeavy": "Heavy",
|
||||||
"LabelHigh": "High",
|
"LabelHigh": "Alto",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelIncomplete": "Incompleta",
|
"LabelIncomplete": "Incompleta",
|
||||||
"LabelInProgress": "In Corso",
|
"LabelInProgress": "In Corso",
|
||||||
"LabelInternalAppStorage": "Internal App Storage",
|
"LabelInternalAppStorage": "Archiviazione interna delle app",
|
||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Vai indietro nel tempo",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Vai avanti nel tempo",
|
||||||
"LabelLanguage": "Lingua",
|
"LabelLanguage": "Lingua",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Pagina singola",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Interlinea",
|
"LabelLineSpacing": "Interlinea",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Ascolta ancora",
|
||||||
"LabelLocalBooks": "Local Books",
|
"LabelLocalBooks": "Libri Locali",
|
||||||
"LabelLocalPodcasts": "Local Podcasts",
|
"LabelLocalPodcasts": "Podcasts Locali",
|
||||||
"LabelLockOrientation": "Lock orientation",
|
"LabelLockOrientation": "Blocca orientamento",
|
||||||
"LabelLockPlayer": "Lock Player",
|
"LabelLockPlayer": "Blocca Player",
|
||||||
"LabelLow": "Low",
|
"LabelLow": "Basso",
|
||||||
"LabelMediaType": "Tipo Media",
|
"LabelMediaType": "Tipo Media",
|
||||||
"LabelMedium": "Medium",
|
"LabelMedium": "Medio",
|
||||||
"LabelMore": "Molto",
|
"LabelMore": "Molto",
|
||||||
"LabelMoreInfo": "Più Info",
|
"LabelMoreInfo": "Più Info",
|
||||||
"LabelName": "Nome",
|
"LabelName": "Nome",
|
||||||
"LabelNarrator": "Narratore",
|
"LabelNarrator": "Narratore",
|
||||||
"LabelNarrators": "Narratori",
|
"LabelNarrators": "Narratori",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Nuovi Autori",
|
||||||
|
"LabelNewestEpisodes": "Nuovi Episodi",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Da Completare",
|
"LabelNotFinished": "Da Completare",
|
||||||
"LabelNotStarted": "Non iniziato",
|
"LabelNotStarted": "Non iniziato",
|
||||||
"LabelOff": "Off",
|
"LabelOff": "Off",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Percorso",
|
"LabelPath": "Percorso",
|
||||||
"LabelPlaybackDirect": "Direct",
|
"LabelPlaybackDirect": "Diretto",
|
||||||
"LabelPlaybackLocal": "Local",
|
"LabelPlaybackLocal": "Local",
|
||||||
"LabelPlaybackSpeed": "Playback Speed",
|
"LabelPlaybackSpeed": "Velocità Playback",
|
||||||
"LabelPlaybackTranscode": "Transcode",
|
"LabelPlaybackTranscode": "Transcodifica",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
||||||
@@ -182,19 +187,21 @@
|
|||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRead": "Leggi",
|
"LabelRead": "Leggi",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelReadAgain": "Leggi Ancora",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
|
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
|
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
|
||||||
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
|
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scala il tempo trascorso in base alla velocità",
|
||||||
"LabelSeason": "Stagione",
|
"LabelSeason": "Stagione",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Seletiona dispositivo",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelServerAddress": "Server address",
|
"LabelServerAddress": "Indiritto Server",
|
||||||
"LabelSetEbookAsPrimary": "Immposta come Primario",
|
"LabelSetEbookAsPrimary": "Imposta come Primario",
|
||||||
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
|
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
|
||||||
"LabelShakeSensitivity": "Shake sensitivity",
|
"LabelShakeSensitivity": "Sensibilita Shake",
|
||||||
"LabelShowAll": "Mostra Tutto",
|
"LabelShowAll": "Mostra Tutto",
|
||||||
"LabelSize": "Dimensione",
|
"LabelSize": "Dimensione",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
@@ -216,60 +223,60 @@
|
|||||||
"LabelThemeLight": "Chiaro",
|
"LabelThemeLight": "Chiaro",
|
||||||
"LabelTimeRemaining": "{0} rimanente",
|
"LabelTimeRemaining": "{0} rimanente",
|
||||||
"LabelTitle": "Titolo",
|
"LabelTitle": "Titolo",
|
||||||
"LabelTotalTrack": "Total Track",
|
"LabelTotalTrack": "Traccia Totale",
|
||||||
"LabelTracks": "Traccia",
|
"LabelTracks": "Traccia",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnlockPlayer": "Unlock Player",
|
"LabelUnlockPlayer": "Sblocca Player",
|
||||||
"LabelUseBookshelfView": "Use bookshelf view",
|
"LabelUseBookshelfView": "Usa visualizzazione libreria",
|
||||||
"LabelUser": "Utente",
|
"LabelUser": "Utente",
|
||||||
"LabelUsername": "Username",
|
"LabelUsername": "Username",
|
||||||
"LabelVeryHigh": "Very High",
|
"LabelVeryHigh": "Molto Alto",
|
||||||
"LabelVeryLow": "Very Low",
|
"LabelVeryLow": "Molto Basso",
|
||||||
"LabelYourBookmarks": "I tuoi Preferiti",
|
"LabelYourBookmarks": "I tuoi Preferiti",
|
||||||
"LabelYourProgress": "Completato al",
|
"LabelYourProgress": "Completato al",
|
||||||
"MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.",
|
"MessageAndroid10Downloads": "Android 10 e versioni precedenti utilizzeranno la memoria interna dell'app per i download.",
|
||||||
"MessageAttemptingServerConnection": "Attempting server connection...",
|
"MessageAttemptingServerConnection": "Tentativo di connessione al server...",
|
||||||
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected",
|
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server non connesso",
|
||||||
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.",
|
"MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Questa app è progettata per funzionare con un server Audiobookshelf ospitato da te o da qualcuno che conosci. Questa app non fornisce alcun contenuto.",
|
||||||
"MessageBookshelfEmpty": "Bookshelf empty",
|
"MessageBookshelfEmpty": "Bookshelf vuoto",
|
||||||
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
|
"MessageConfirmDeleteLocalEpisode": "Rimuovi episodi locali \"{0}\" dal tuo dispositivo? i file sul server non verranno toccati.",
|
||||||
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
|
"MessageConfirmDeleteLocalFiles": "Remove i file locali dell'oggetto? i file sul server e i progressi non verranno toccati.",
|
||||||
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
|
"MessageConfirmDiscardProgress": "Sei sicuro di voler resettare i tuoi progressi?",
|
||||||
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
|
"MessageConfirmMarkAsFinished": "Sei sicuro di voler contrassegnare questo elemento come finito?",
|
||||||
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
|
"MessageConfirmRemoveBookmark": "Sei sicuro di voler rimuovere il segnalibro?",
|
||||||
"MessageDiscardProgress": "Discard Progress",
|
"MessageDiscardProgress": "Elimina Progressi",
|
||||||
"MessageDownloadCompleteProcessing": "Download complete. Processing...",
|
"MessageDownloadCompleteProcessing": "Download completato. Elaboratione...",
|
||||||
"MessageDownloading": "Downloading...",
|
"MessageDownloading": "Downloading...",
|
||||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episodio(i) in coda per il Download",
|
"MessageEpisodesQueuedForDownload": "{0} Episodio(i) in coda per il Download",
|
||||||
"MessageFeedURLWillBe": "Feed URL Saranno {0}",
|
"MessageFeedURLWillBe": "Feed URL Saranno {0}",
|
||||||
"MessageFetching": "Recupero Info...",
|
"MessageFetching": "Recupero Info...",
|
||||||
"MessageFollowTheProjectOnGithub": "Follow the project on Github",
|
"MessageFollowTheProjectOnGithub": "Segui il progetto su Github",
|
||||||
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item",
|
"MessageItemDownloadCompleteFailedToCreate": "Download dell'elemento completato ma impossibile creare l'elemento della libreria",
|
||||||
"MessageLoading": "Caricamento...",
|
"MessageLoading": "Caricamento...",
|
||||||
"MessageLoadingServerData": "Loading server data...",
|
"MessageLoadingServerData": "Caricamento dati dal server...",
|
||||||
"MessageMarkAsFinished": "Segna come finito",
|
"MessageMarkAsFinished": "Segna come finito",
|
||||||
"MessageMediaLinkedToADifferentServer": "Media is linked to an Audiobookshelf server on a different address ({0}). Progress will be synced when connected to this server address.",
|
"MessageMediaLinkedToADifferentServer": "Il supporto è collegato a un server Audiobookshelf su un indirizzo diverso ({0}). I progressi verranno sincronizzati quando ci si connette a questo indirizzo del server.",
|
||||||
"MessageMediaLinkedToADifferentUser": "Media is linked to this server but was downloaded by a different user. Progress will only be synced to the user that downloaded it.",
|
"MessageMediaLinkedToADifferentUser": "Il contenuto multimediale è collegato a questo server ma è stato scaricato da un utente diverso. I progressi verranno sincronizzati solo con l'utente che lo ha scaricato.",
|
||||||
"MessageMediaLinkedToServer": "Linked to server {0}",
|
"MessageMediaLinkedToServer": "Collegato al server {0}",
|
||||||
"MessageMediaLinkedToThisServer": "Downloaded media is linked to this server",
|
"MessageMediaLinkedToThisServer": "Il supporto scaricato è collegato a questo server",
|
||||||
"MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.",
|
"MessageMediaNotLinkedToServer": "I contenuti multimediali non sono collegati a un server Audiobookshelf. Nessun progresso verrà sincronizzato.",
|
||||||
"MessageNoBookmarks": "Nessun Preferito",
|
"MessageNoBookmarks": "Nessun Preferito",
|
||||||
"MessageNoChapters": "Nessun Capitolo",
|
"MessageNoChapters": "Nessun Capitolo",
|
||||||
"MessageNoItems": "Nessun Oggetto",
|
"MessageNoItems": "Nessun Oggetto",
|
||||||
"MessageNoItemsFound": "Nessun Oggetto trovato",
|
"MessageNoItemsFound": "Nessun Oggetto trovato",
|
||||||
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
|
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
|
||||||
"MessageNoMediaFolders": "No Media Folders",
|
"MessageNoMediaFolders": "Nessuna Cartella Media",
|
||||||
"MessageNoNetworkConnection": "No network connection",
|
"MessageNoNetworkConnection": "Nessuna connessione di rete",
|
||||||
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
||||||
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
||||||
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular",
|
"MessageSocketConnectedOverMeteredCellular": "Collegato tramite Rete cellulare a consumo",
|
||||||
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi",
|
"MessageSocketConnectedOverMeteredWifi": "Collegato tramite rete wifi a consumo",
|
||||||
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
|
"MessageSocketConnectedOverUnmeteredCellular": "Collegato tramite Rete cellularer",
|
||||||
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi",
|
"MessageSocketConnectedOverUnmeteredWifi": "Collegato tramite rete wif",
|
||||||
"MessageSocketNotConnected": "Socket not connected",
|
"MessageSocketNotConnected": "Non connesso",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Avviso: la maggior parte delle app di podcast richiede che l'URL del feed RSS utilizzi HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Avviso: la maggior parte delle app di podcast richiede che l'URL del feed RSS utilizzi HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Avviso: 1 o più delle tue puntate non hanno una data di pubblicazione. Alcune app di podcast lo richiedono.",
|
"NoteRSSFeedPodcastAppsPubDate": "Avviso: 1 o più delle tue puntate non hanno una data di pubblicazione. Alcune app di podcast lo richiedono.",
|
||||||
"ToastBookmarkCreateFailed": "Creazione segnalibro fallita",
|
"ToastBookmarkCreateFailed": "Creazione segnalibro fallita",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Aprašymas",
|
"LabelDescription": "Aprašymas",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Kalba",
|
"LabelLanguage": "Kalba",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Tarpas tarp eilučių",
|
"LabelLineSpacing": "Tarpas tarp eilučių",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Skaitytojas",
|
"LabelNarrator": "Skaitytojas",
|
||||||
"LabelNarrators": "Skaitytojai",
|
"LabelNarrators": "Skaitytojai",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nebaigta",
|
"LabelNotFinished": "Nebaigta",
|
||||||
"LabelNotStarted": "Nepasileista",
|
"LabelNotStarted": "Nepasileista",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Publikavimo data",
|
"LabelPubDate": "Publikavimo data",
|
||||||
"LabelPublishYear": "Leidimo metai",
|
"LabelPublishYear": "Leidimo metai",
|
||||||
"LabelRead": "Skaityta",
|
"LabelRead": "Skaityta",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
|
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
|
||||||
"LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas",
|
"LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas",
|
||||||
"LabelRSSFeedPreventIndexing": "Neleisti indeksuoti",
|
"LabelRSSFeedPreventIndexing": "Neleisti indeksuoti",
|
||||||
"LabelRSSFeedSlug": "RSS srauto identifikatorius",
|
"LabelRSSFeedSlug": "RSS srauto identifikatorius",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Sezonas",
|
"LabelSeason": "Sezonas",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Serija",
|
"LabelSeries": "Serija",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beschrijving",
|
"LabelDescription": "Beschrijving",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Taal",
|
"LabelLanguage": "Taal",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Regelruimte",
|
"LabelLineSpacing": "Regelruimte",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Verteller",
|
"LabelNarrator": "Verteller",
|
||||||
"LabelNarrators": "Vertellers",
|
"LabelNarrators": "Vertellers",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Niet Voltooid",
|
"LabelNotFinished": "Niet Voltooid",
|
||||||
"LabelNotStarted": "Niet Gestart",
|
"LabelNotStarted": "Niet Gestart",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Publicatiedatum",
|
"LabelPubDate": "Publicatiedatum",
|
||||||
"LabelPublishYear": "Jaar van uitgave",
|
"LabelPublishYear": "Jaar van uitgave",
|
||||||
"LabelRead": "Lees",
|
"LabelRead": "Lees",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
|
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
|
||||||
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
|
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
|
||||||
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
|
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
|
||||||
"LabelRSSFeedSlug": "RSS-feed slug",
|
"LabelRSSFeedSlug": "RSS-feed slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Seizoen",
|
"LabelSeason": "Seizoen",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beskrivelse",
|
"LabelDescription": "Beskrivelse",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Språk",
|
"LabelLanguage": "Språk",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Linjemellomrom",
|
"LabelLineSpacing": "Linjemellomrom",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Forteller",
|
"LabelNarrator": "Forteller",
|
||||||
"LabelNarrators": "Fortellere",
|
"LabelNarrators": "Fortellere",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Ikke fullført",
|
"LabelNotFinished": "Ikke fullført",
|
||||||
"LabelNotStarted": "Ikke startet",
|
"LabelNotStarted": "Ikke startet",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Publiseringsdato",
|
"LabelPubDate": "Publiseringsdato",
|
||||||
"LabelPublishYear": "Publikasjonsår",
|
"LabelPublishYear": "Publikasjonsår",
|
||||||
"LabelRead": "Les",
|
"LabelRead": "Les",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost",
|
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost",
|
||||||
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
|
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
|
||||||
"LabelRSSFeedPreventIndexing": "Forhindre indeksering",
|
"LabelRSSFeedPreventIndexing": "Forhindre indeksering",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Sesong",
|
"LabelSeason": "Sesong",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Serier",
|
"LabelSeries": "Serier",
|
||||||
|
|||||||
+158
-151
@@ -1,52 +1,52 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Dodaj",
|
"ButtonAdd": "Dodaj",
|
||||||
"ButtonAddNewServer": "Add New Server",
|
"ButtonAddNewServer": "Dodaj nowy serwer",
|
||||||
"ButtonAuthors": "Autorzy",
|
"ButtonAuthors": "Autorzy",
|
||||||
"ButtonBack": "Back",
|
"ButtonBack": "Powrót",
|
||||||
"ButtonCancel": "Anuluj",
|
"ButtonCancel": "Anuluj",
|
||||||
"ButtonCancelTimer": "Cancel Timer",
|
"ButtonCancelTimer": "Anuluj wyłącznik",
|
||||||
"ButtonClearFilter": "Wyczyść filtr",
|
"ButtonClearFilter": "Wyczyść filtr",
|
||||||
"ButtonCloseFeed": "Zamknij kanał",
|
"ButtonCloseFeed": "Zamknij kanał",
|
||||||
"ButtonCollections": "Kolekcje",
|
"ButtonCollections": "Kolekcje",
|
||||||
"ButtonConnect": "Connect",
|
"ButtonConnect": "Połącz",
|
||||||
"ButtonConnectToServer": "Connect to Server",
|
"ButtonConnectToServer": "Połącz z serwerem",
|
||||||
"ButtonCreate": "Utwórz",
|
"ButtonCreate": "Utwórz",
|
||||||
"ButtonCreateBookmark": "Create Bookmark",
|
"ButtonCreateBookmark": "Utwórz zakładkę",
|
||||||
"ButtonCreateNewPlaylist": "Create New Playlist",
|
"ButtonCreateNewPlaylist": "Utwórz nową playlistę",
|
||||||
"ButtonDelete": "Usuń",
|
"ButtonDelete": "Usuń",
|
||||||
"ButtonDeleteLocalEpisode": "Delete local episode",
|
"ButtonDeleteLocalEpisode": "Usuń lokalny odcinek",
|
||||||
"ButtonDeleteLocalFile": "Delete local file",
|
"ButtonDeleteLocalFile": "Usuń lokalny plik",
|
||||||
"ButtonDeleteLocalItem": "Delete local item",
|
"ButtonDeleteLocalItem": "Usuń lokalny element",
|
||||||
"ButtonDisableAutoTimer": "Disable Auto Timer",
|
"ButtonDisableAutoTimer": "Wyłącz automatyczny wyłącznik",
|
||||||
"ButtonDisconnect": "Disconnect",
|
"ButtonDisconnect": "Rozłącz",
|
||||||
"ButtonHistory": "History",
|
"ButtonHistory": "Historia",
|
||||||
"ButtonHome": "Strona główna",
|
"ButtonHome": "Strona główna",
|
||||||
"ButtonIssues": "Błędy",
|
"ButtonIssues": "Błędy",
|
||||||
"ButtonLatest": "Aktualna wersja:",
|
"ButtonLatest": "Aktualna wersja:",
|
||||||
"ButtonLibrary": "Biblioteka",
|
"ButtonLibrary": "Biblioteka",
|
||||||
"ButtonLocalMedia": "Local Media",
|
"ButtonLocalMedia": "Media lokalne",
|
||||||
"ButtonManageLocalFiles": "Manage Local Files",
|
"ButtonManageLocalFiles": "Zarządzaj lokalnymi plikami",
|
||||||
"ButtonNewFolder": "New Folder",
|
"ButtonNewFolder": "Nowy folder",
|
||||||
"ButtonNextEpisode": "Next Episode",
|
"ButtonNextEpisode": "Następny odcinek",
|
||||||
"ButtonOpenFeed": "Otwórz feed",
|
"ButtonOpenFeed": "Otwórz feed",
|
||||||
"ButtonOverride": "Override",
|
"ButtonOverride": "Nadpisz",
|
||||||
"ButtonPause": "Pause",
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Odtwarzaj",
|
"ButtonPlay": "Odtwarzaj",
|
||||||
"ButtonPlaying": "Odtwarzane",
|
"ButtonPlaying": "Odtwarzane",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlisty",
|
||||||
"ButtonRead": "Czytaj",
|
"ButtonRead": "Czytaj",
|
||||||
"ButtonRemove": "Usuń",
|
"ButtonRemove": "Usuń",
|
||||||
"ButtonRemoveFromServer": "Remove from Server",
|
"ButtonRemoveFromServer": "Usuń z serwera",
|
||||||
"ButtonSave": "Zapisz",
|
"ButtonSave": "Zapisz",
|
||||||
"ButtonSaveOrder": "Save Order",
|
"ButtonSaveOrder": "Zapisz kolejność",
|
||||||
"ButtonSearch": "Szukaj",
|
"ButtonSearch": "Szukaj",
|
||||||
"ButtonSendEbookToDevice": "Send Ebook to Device",
|
"ButtonSendEbookToDevice": "Wyślij ebook do urządzenia",
|
||||||
"ButtonSeries": "Seria",
|
"ButtonSeries": "Seria",
|
||||||
"ButtonSetTimer": "Set Timer",
|
"ButtonSetTimer": "Ustaw wyłącznik",
|
||||||
"ButtonStream": "Stream",
|
"ButtonStream": "Strumieniuj",
|
||||||
"ButtonSubmit": "Zaloguj",
|
"ButtonSubmit": "Zaloguj",
|
||||||
"ButtonSwitchServerUser": "Switch Server/User",
|
"ButtonSwitchServerUser": "Przełącz serwer/użytkownika",
|
||||||
"ButtonUserStats": "User Stats",
|
"ButtonUserStats": "Statystyki użytkownika",
|
||||||
"ButtonYes": "Tak",
|
"ButtonYes": "Tak",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
"HeaderAdvanced": "Zaawansowane",
|
"HeaderAdvanced": "Zaawansowane",
|
||||||
@@ -54,78 +54,79 @@
|
|||||||
"HeaderChapters": "Rozdziały",
|
"HeaderChapters": "Rozdziały",
|
||||||
"HeaderCollection": "Kolekcja",
|
"HeaderCollection": "Kolekcja",
|
||||||
"HeaderCollectionItems": "Elementy kolekcji",
|
"HeaderCollectionItems": "Elementy kolekcji",
|
||||||
"HeaderConnectionStatus": "Connection Status",
|
"HeaderConnectionStatus": "Status połączenia",
|
||||||
"HeaderDetails": "Szczegóły",
|
"HeaderDetails": "Szczegóły",
|
||||||
"HeaderDownloads": "Downloads",
|
"HeaderDownloads": "Pobrane",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Pliki ebook",
|
||||||
"HeaderEpisodes": "Rozdziały",
|
"HeaderEpisodes": "Odcinki",
|
||||||
"HeaderEreaderSettings": "Ereader Settings",
|
"HeaderEreaderSettings": "Ustawienia Ereader",
|
||||||
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
||||||
"HeaderLibraries": "Biblioteki",
|
"HeaderLibraries": "Biblioteki",
|
||||||
"HeaderLocalFolders": "Local Folders",
|
"HeaderLocalFolders": "Lokalne foldery",
|
||||||
"HeaderLocalLibraryItems": "Local Library Items",
|
"HeaderLocalLibraryItems": "Elementy biblioteki lokalnej",
|
||||||
"HeaderNewPlaylist": "New Playlist",
|
"HeaderNewPlaylist": "Nowa playlista",
|
||||||
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
||||||
"HeaderPlaybackSettings": "Playback Settings",
|
"HeaderPlaybackSettings": "Ustawienia odtwarzania",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Playlista",
|
||||||
"HeaderPlaylistItems": "Playlist Items",
|
"HeaderPlaylistItems": "Elementy playlisty",
|
||||||
"HeaderRSSFeed": "RSS Feed",
|
"HeaderRSSFeed": "Kanał RSS",
|
||||||
"HeaderRSSFeedGeneral": "RSS Details",
|
"HeaderRSSFeedGeneral": "Szczegóły RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
|
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
|
||||||
"HeaderSelectDownloadLocation": "Select Download Location",
|
"HeaderSelectDownloadLocation": "Wybierz miejsce pobierania",
|
||||||
"HeaderSettings": "Ustawienia",
|
"HeaderSettings": "Ustawienia",
|
||||||
"HeaderSleepTimer": "Wyłącznik czasowy",
|
"HeaderSleepTimer": "Wyłącznik czasowy",
|
||||||
"HeaderSleepTimerSettings": "Sleep Timer Settings",
|
"HeaderSleepTimerSettings": "Ustawienia wyłącznika czasowego",
|
||||||
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
|
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
|
||||||
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
||||||
"HeaderTableOfContents": "Table of Contents",
|
"HeaderTableOfContents": "Spis treści",
|
||||||
"HeaderUserInterfaceSettings": "User Interface Settings",
|
"HeaderUserInterfaceSettings": "Ustawienia inferfejsu użytkownika",
|
||||||
"HeaderYourStats": "Twoje statystyki",
|
"HeaderYourStats": "Twoje statystyki",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Dodano",
|
||||||
"LabelAddedAt": "Dodano",
|
"LabelAddedAt": "Dodano w",
|
||||||
"LabelAddToPlaylist": "Add to Playlist",
|
"LabelAddToPlaylist": "Dodaj do playlisty",
|
||||||
"LabelAll": "All",
|
"LabelAll": "Wszystkie",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
||||||
"LabelAuthorLastFirst": "Author (Malejąco)",
|
"LabelAuthorLastFirst": "Author (Malejąco)",
|
||||||
"LabelAuthors": "Autorzy",
|
"LabelAuthors": "Autorzy",
|
||||||
"LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków",
|
"LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków",
|
||||||
"LabelAutoRewindTime": "Auto rewind time",
|
"LabelAutoRewindTime": "Automatyczne cofnięcie",
|
||||||
"LabelAutoSleepTimer": "Auto sleep timer",
|
"LabelAutoSleepTimer": "Automatyczny wył. czasowy",
|
||||||
"LabelAutoSleepTimerAutoRewind": "Auto sleep timer auto rewind",
|
"LabelAutoSleepTimerAutoRewind": "Automatyczny wył.czasowy z cofnięciem",
|
||||||
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
|
"LabelAutoSleepTimerAutoRewindHelp": "Po zakończeniu automatycznego wyłącznika czasowego ponowne odtworzenie elementu spowoduje automatyczne przewinięcie pozycji do tyłu.",
|
||||||
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
|
"LabelAutoSleepTimerHelp": "Podczas odtwarzania multimediów między określonym czasem rozpoczęcia i zakończenia automatycznie uruchomi się wyłącznik czasowy.",
|
||||||
"LabelBooks": "Książki",
|
"LabelBooks": "Książki",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Rozdziały",
|
||||||
"LabelChapterTrack": "Chapter Track",
|
"LabelChapterTrack": "Postęp rozdziału",
|
||||||
"LabelClosePlayer": "Zamknij odtwarzacz",
|
"LabelClosePlayer": "Zamknij odtwarzacz",
|
||||||
"LabelCollapseSeries": "Podsumuj serię",
|
"LabelCollapseSeries": "Podsumuj serię",
|
||||||
"LabelComplete": "Ukończone",
|
"LabelComplete": "Ukończone",
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Kontynuuj książki",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Kontynuuj odcinki",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Kontynuuj odtwarzanie",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueReading": "Kontynuuj czytanie",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelContinueSeries": "Kontynuuj serię",
|
||||||
|
"LabelCustomTime": "Własny czas",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
"LabelDisableAudioFadeOut": "Disable audio fade out",
|
"LabelDisableAudioFadeOut": "Wyłącz zanikanie dźwięku",
|
||||||
"LabelDisableAudioFadeOutHelp": "Audio volume will start decreasing when there is less than 1 minute remaining on the sleep timer. Enable this setting to not fade out.",
|
"LabelDisableAudioFadeOutHelp": "Głośność dźwięku zacznie się zmniejszać, gdy na wyłączniku czasowym pozostanie mniej niż 1 minuta. Włącz to ustawienie, jeśli nie chcesz korzystać z zanikania dźwięku.",
|
||||||
"LabelDisableAutoRewind": "Disable auto rewind",
|
"LabelDisableAutoRewind": "Wyłącz automatyczne cofanie",
|
||||||
"LabelDisableShakeToReset": "Disable shake to reset",
|
"LabelDisableShakeToReset": "Wyłącz potrząsanie, aby zresetować",
|
||||||
"LabelDisableShakeToResetHelp": "Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.",
|
"LabelDisableShakeToResetHelp": "Potrząsanie urządzeniem podczas pracy minutnika LUB w ciągu 2 minut od jego wygaśnięcia spowoduje zresetowanie wyłącznika czasowego. Włącz to ustawienie, aby wyłączyć potrząsanie w celu zresetowania.",
|
||||||
"LabelDisableVibrateOnReset": "Disable vibrate on reset",
|
"LabelDisableVibrateOnReset": "Wyłącz wibracje przy resetowaniu",
|
||||||
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.",
|
"LabelDisableVibrateOnResetHelp": "Gdy wyłącznik czasowy zostanie zresetowany, urządzenie zacznie wibrować. Włącz to ustawienie, aby nie wibrować po zresetowaniu wyłącznika czasowego.",
|
||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "Odkrywaj",
|
||||||
"LabelDownload": "Pobierz",
|
"LabelDownload": "Pobierz",
|
||||||
"LabelDownloaded": "Downloaded",
|
"LabelDownloaded": "Pobrane",
|
||||||
"LabelDuration": "Czas trwania",
|
"LabelDuration": "Czas trwania",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooki",
|
||||||
"LabelEnable": "Włącz",
|
"LabelEnable": "Włącz",
|
||||||
"LabelEnableMp3IndexSeeking": "Enable mp3 index seeking",
|
"LabelEnableMp3IndexSeeking": "Włącz wyszukiwanie indeksu mp3",
|
||||||
"LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.",
|
"LabelEnableMp3IndexSeekingHelp": "To ustawienie powinno być włączone tylko wtedy, gdy masz pliki mp3, które nie są poprawnie wyszukiwane. Niedokładne wyszukiwanie jest najprawdopodobniej spowodowane plikami MP3 o zmiennym birate (VBR). To ustawienie wymusi wyszukiwanie indeksu, w którym mapowanie czasu do bajtu jest kompilowane podczas odczytywania pliku. W niektórych przypadkach w przypadku dużych plików MP3 wystąpi opóźnienie podczas wyszukiwania w kierunku końca pliku.",
|
||||||
"LabelEnd": "Zakończ",
|
"LabelEnd": "Zakończ",
|
||||||
"LabelEndOfChapter": "End of Chapter",
|
"LabelEndOfChapter": "Koniec rozdziału",
|
||||||
"LabelEndTime": "End time",
|
"LabelEndTime": "Koniec czasu",
|
||||||
"LabelEpisode": "Odcinek",
|
"LabelEpisode": "Odcinek",
|
||||||
"LabelFeedURL": "URL kanału",
|
"LabelFeedURL": "URL kanału",
|
||||||
"LabelFile": "Plik",
|
"LabelFile": "Plik",
|
||||||
@@ -134,67 +135,73 @@
|
|||||||
"LabelFilename": "Nazwa pliku",
|
"LabelFilename": "Nazwa pliku",
|
||||||
"LabelFinished": "Zakończone",
|
"LabelFinished": "Zakończone",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Rozmiar czcionki",
|
||||||
"LabelGenre": "Gatunek",
|
"LabelGenre": "Gatunek",
|
||||||
"LabelGenres": "Gatunki",
|
"LabelGenres": "Gatunki",
|
||||||
"LabelHapticFeedback": "Haptic feedback",
|
"LabelHapticFeedback": "Dotykowe sprzężenie zwrotne",
|
||||||
"LabelHasEbook": "Has ebook",
|
"LabelHasEbook": "Zawiera wersję ebook",
|
||||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
"LabelHasSupplementaryEbook": "Posiada dodatkowy ebook",
|
||||||
"LabelHeavy": "Heavy",
|
"LabelHeavy": "Ciężko",
|
||||||
"LabelHigh": "High",
|
"LabelHigh": "Wysoko",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Dostawca",
|
||||||
"LabelIncomplete": "Nieukończone",
|
"LabelIncomplete": "Nieukończone",
|
||||||
"LabelInProgress": "W trakcie",
|
"LabelInProgress": "W toku",
|
||||||
"LabelInternalAppStorage": "Internal App Storage",
|
"LabelInternalAppStorage": "Pamięć wewnętrzna aplikacji",
|
||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Przeskok wstecz",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Przeskok w przód",
|
||||||
"LabelLanguage": "Język",
|
"LabelLanguage": "Język",
|
||||||
"LabelLight": "Light",
|
"LabelLayout": "Układ",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLayoutAuto": "Auto",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelLayoutSinglePage": "Pojedyncza strona",
|
||||||
"LabelLocalBooks": "Local Books",
|
"LabelLight": "Lekko",
|
||||||
"LabelLocalPodcasts": "Local Podcasts",
|
"LabelLineSpacing": "Interlinia",
|
||||||
"LabelLockOrientation": "Lock orientation",
|
"LabelListenAgain": "Słuchaj ponownie",
|
||||||
"LabelLockPlayer": "Lock Player",
|
"LabelLocalBooks": "Książki lokalne",
|
||||||
"LabelLow": "Low",
|
"LabelLocalPodcasts": "Podcasty lokalne",
|
||||||
|
"LabelLockOrientation": "Zablokuj orientację",
|
||||||
|
"LabelLockPlayer": "Zablokuj odtwarzacz",
|
||||||
|
"LabelLow": "Nisko",
|
||||||
"LabelMediaType": "Typ mediów",
|
"LabelMediaType": "Typ mediów",
|
||||||
"LabelMedium": "Medium",
|
"LabelMedium": "Średnio",
|
||||||
"LabelMore": "Więcej",
|
"LabelMore": "Więcej",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Więcej informacji",
|
||||||
"LabelName": "Nazwa",
|
"LabelName": "Nazwa",
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Lektorzy",
|
"LabelNarrators": "Lektorzy",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Najnowsi autorzy",
|
||||||
|
"LabelNewestEpisodes": "Najnosze odcinki",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nieukończone",
|
"LabelNotFinished": "Nieukończone",
|
||||||
"LabelNotStarted": "Nie rozpoęczto",
|
"LabelNotStarted": "Nie rozpoczęto",
|
||||||
"LabelOff": "Off",
|
"LabelOff": "Wyłącz",
|
||||||
"LabelPassword": "Hasło",
|
"LabelPassword": "Hasło",
|
||||||
"LabelPath": "Ścieżka",
|
"LabelPath": "Ścieżka",
|
||||||
"LabelPlaybackDirect": "Direct",
|
"LabelPlaybackDirect": "Bezpośrednio",
|
||||||
"LabelPlaybackLocal": "Local",
|
"LabelPlaybackLocal": "Lokalnie",
|
||||||
"LabelPlaybackSpeed": "Playback Speed",
|
"LabelPlaybackSpeed": "Prędkość odtwarzania",
|
||||||
"LabelPlaybackTranscode": "Transcode",
|
"LabelPlaybackTranscode": "Transkoduj",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasty",
|
"LabelPodcasts": "Podcasty",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Zapobiegaj indeksowaniu kanału przez katalogi podcastów iTunes i Google",
|
||||||
"LabelProgress": "Postęp",
|
"LabelProgress": "Postęp",
|
||||||
"LabelPubDate": "Data publikacji",
|
"LabelPubDate": "Data publikacji",
|
||||||
"LabelPublishYear": "Rok publikacji",
|
"LabelPublishYear": "Rok publikacji",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Czytaj",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelReadAgain": "Czytaj ponownie",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentlyAdded": "Ostatnio dodane",
|
||||||
|
"LabelRecentSeries": "Najnowsze serie",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Skaluj czas, który upłynął według prędkości",
|
||||||
"LabelSeason": "Sezon",
|
"LabelSeason": "Sezon",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Wybierz urządzenie",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelServerAddress": "Server address",
|
"LabelServerAddress": "Adres serwera",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Ustaw jako podstawowy ebook",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Ustaw jako dodatkowy ebook",
|
||||||
"LabelShakeSensitivity": "Shake sensitivity",
|
"LabelShakeSensitivity": "Czułość potrząsania",
|
||||||
"LabelShowAll": "Pokaż wszystko",
|
"LabelShowAll": "Pokaż wszystko",
|
||||||
"LabelSize": "Rozmiar",
|
"LabelSize": "Rozmiar",
|
||||||
"LabelSleepTimer": "Wyłącznik czasowy",
|
"LabelSleepTimer": "Wyłącznik czasowy",
|
||||||
@@ -211,65 +218,65 @@
|
|||||||
"LabelStatsWeekListening": "Tydzień odtwarzania",
|
"LabelStatsWeekListening": "Tydzień odtwarzania",
|
||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tagi",
|
"LabelTags": "Tagi",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Motyw",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Ciemny",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Jasny",
|
||||||
"LabelTimeRemaining": "Pozostało {0}",
|
"LabelTimeRemaining": "Pozostało {0}",
|
||||||
"LabelTitle": "Tytuł",
|
"LabelTitle": "Tytuł",
|
||||||
"LabelTotalTrack": "Total Track",
|
"LabelTotalTrack": "Postęp całkowity",
|
||||||
"LabelTracks": "Tracks",
|
"LabelTracks": "Postępy",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnlockPlayer": "Unlock Player",
|
"LabelUnlockPlayer": "Odblokuj odtwarzacz",
|
||||||
"LabelUseBookshelfView": "Use bookshelf view",
|
"LabelUseBookshelfView": "Użyj widoku półki",
|
||||||
"LabelUser": "Użytkownik",
|
"LabelUser": "Użytkownik",
|
||||||
"LabelUsername": "Nazwa użytkownika",
|
"LabelUsername": "Nazwa użytkownika",
|
||||||
"LabelVeryHigh": "Very High",
|
"LabelVeryHigh": "Bardzo wysoko",
|
||||||
"LabelVeryLow": "Very Low",
|
"LabelVeryLow": "Bardzo nisko",
|
||||||
"LabelYourBookmarks": "Twoje zakładki",
|
"LabelYourBookmarks": "Twoje zakładki",
|
||||||
"LabelYourProgress": "Twój postęp",
|
"LabelYourProgress": "Twój postęp",
|
||||||
"MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.",
|
"MessageAndroid10Downloads": "Android 10 i starsze będą używać wewnętrznej pamięci aplikacji do pobierania.",
|
||||||
"MessageAttemptingServerConnection": "Attempting server connection...",
|
"MessageAttemptingServerConnection": "Próba połączenia z serwerem...",
|
||||||
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected",
|
"MessageAudiobookshelfServerNotConnected": "Brak połączenia z serwerem Audiobookshelf",
|
||||||
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.",
|
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Ta aplikacja jest przeznaczona do współpracy z serwerem Audiobookshelf, który dostarczasz Ty lub ktoś, kogo znasz. Ta aplikacja nie dostarcza żadnych treści..",
|
||||||
"MessageBookshelfEmpty": "Bookshelf empty",
|
"MessageBookshelfEmpty": "Półka pusta",
|
||||||
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
|
"MessageConfirmDeleteLocalEpisode": "Usunąć lokalny odcinek \"{0}\" ze swojego urządzenia? Nie będzie to miało wpływu na plik na serwerze.",
|
||||||
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
|
"MessageConfirmDeleteLocalFiles": "Usunąć lokalne pliki tego elementu ze swojego urządzenia? Nie będzie to miało wpływu na pliki na serwerze i Twoje postępy.",
|
||||||
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
|
"MessageConfirmDiscardProgress": "Na pewno chcesz zresetować postęp?",
|
||||||
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
|
"MessageConfirmMarkAsFinished": "Na pewno chcesz oznaczyć ten element jako ukończony?",
|
||||||
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
|
"MessageConfirmRemoveBookmark": "Na pewno chcesz usunąć zakładkę?",
|
||||||
"MessageDiscardProgress": "Discard Progress",
|
"MessageDiscardProgress": "Zresetuj postęp",
|
||||||
"MessageDownloadCompleteProcessing": "Download complete. Processing...",
|
"MessageDownloadCompleteProcessing": "Pobieranie ukończone. Przetwarzanie...",
|
||||||
"MessageDownloading": "Downloading...",
|
"MessageDownloading": "Pobieranie...",
|
||||||
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
|
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
|
||||||
"MessageFeedURLWillBe": "URL kanału: {0}",
|
"MessageFeedURLWillBe": "URL kanału: {0}",
|
||||||
"MessageFetching": "Pobieranie...",
|
"MessageFetching": "Pobieranie...",
|
||||||
"MessageFollowTheProjectOnGithub": "Follow the project on Github",
|
"MessageFollowTheProjectOnGithub": "Śledź projekt na Githubie",
|
||||||
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item",
|
"MessageItemDownloadCompleteFailedToCreate": "Pobieranie elementu zostało zakończone, ale nie udało się utworzyć elementu biblioteki",
|
||||||
"MessageLoading": "Ładowanie...",
|
"MessageLoading": "Ładowanie...",
|
||||||
"MessageLoadingServerData": "Loading server data...",
|
"MessageLoadingServerData": "Ładowanie danych serwera...",
|
||||||
"MessageMarkAsFinished": "Oznacz jako ukończone",
|
"MessageMarkAsFinished": "Oznacz jako ukończone",
|
||||||
"MessageMediaLinkedToADifferentServer": "Media is linked to an Audiobookshelf server on a different address ({0}). Progress will be synced when connected to this server address.",
|
"MessageMediaLinkedToADifferentServer": "Multimedia są połączone z serwerem Audiobookshelf pod innym adresem ({0}). Postęp zostanie zsynchronizowany po nawiązaniu połączenia z tym adresem serwera.",
|
||||||
"MessageMediaLinkedToADifferentUser": "Media is linked to this server but was downloaded by a different user. Progress will only be synced to the user that downloaded it.",
|
"MessageMediaLinkedToADifferentUser": "Multimedia są połączone z tym serwerem, ale zostały pobrane przez innego użytkownika. Postęp zostanie zsynchronizowany tylko z użytkownikiem, który je pobrał.",
|
||||||
"MessageMediaLinkedToServer": "Linked to server {0}",
|
"MessageMediaLinkedToServer": "Połączone z serwerem {0}",
|
||||||
"MessageMediaLinkedToThisServer": "Downloaded media is linked to this server",
|
"MessageMediaLinkedToThisServer": "Pobrane multimedia są połączone z tym serwerem",
|
||||||
"MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.",
|
"MessageMediaNotLinkedToServer": "Multimedia nie są połączone z serwerem Audiobookshelf. Żaden postęp nie zostanie zsynchronizowany.",
|
||||||
"MessageNoBookmarks": "Brak zakładek",
|
"MessageNoBookmarks": "Brak zakładek",
|
||||||
"MessageNoChapters": "Brak rozdziałów",
|
"MessageNoChapters": "Brak rozdziałów",
|
||||||
"MessageNoItems": "Brak elementów",
|
"MessageNoItems": "Brak elementów",
|
||||||
"MessageNoItemsFound": "Nie znaleziono żadnych elemntów",
|
"MessageNoItemsFound": "Nie znaleziono żadnych elementów",
|
||||||
"MessageNoListeningSessions": "Brak sesji odtwarzania",
|
"MessageNoListeningSessions": "Brak sesji odtwarzania",
|
||||||
"MessageNoMediaFolders": "No Media Folders",
|
"MessageNoMediaFolders": "Brak folderów z mediami",
|
||||||
"MessageNoNetworkConnection": "No network connection",
|
"MessageNoNetworkConnection": "Brak połączenia sieciowego",
|
||||||
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
|
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
|
||||||
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "Nie posiadasz playlist",
|
||||||
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
|
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
|
||||||
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular",
|
"MessageSocketConnectedOverMeteredCellular": "Gniazdo podłączone przez sieć komórkową z limitem danych",
|
||||||
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi",
|
"MessageSocketConnectedOverMeteredWifi": "Gniazdo podłączone przez Wi-Fi z limitem danych",
|
||||||
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
|
"MessageSocketConnectedOverUnmeteredCellular": "Gniazdo podłączone przez sieć komórkową bez limitu danych",
|
||||||
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi",
|
"MessageSocketConnectedOverUnmeteredWifi": "Gniazdo podłączone przez Wi-Fi bez limitu danych",
|
||||||
"MessageSocketNotConnected": "Socket not connected",
|
"MessageSocketNotConnected": "Gniazdo nie jest podłączone",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Ostrzeżenie: Większość aplikacji do obsługi podcastów wymaga, aby adres URL kanału RSS korzystał z protokołu HTTPS.",
|
"NoteRSSFeedPodcastAppsHttps": "Ostrzeżenie: Większość aplikacji do obsługi podcastów wymaga, aby adres URL kanału RSS korzystał z protokołu HTTPS.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Ostrzeżenie: 1 lub więcej odcinków nie ma daty publikacji. Niektóre aplikacje do słuchania podcastów tego wymagają.",
|
"NoteRSSFeedPodcastAppsPubDate": "Ostrzeżenie: 1 lub więcej odcinków nie ma daty publikacji. Niektóre aplikacje do słuchania podcastów tego wymagają.",
|
||||||
"ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki",
|
"ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki",
|
||||||
@@ -277,7 +284,7 @@
|
|||||||
"ToastBookmarkUpdateFailed": "Nie udało się zaktualizować zakładki",
|
"ToastBookmarkUpdateFailed": "Nie udało się zaktualizować zakładki",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako zakończone",
|
"ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako zakończone",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Oznaczenie pozycji jako ukończonej nie powiodło się",
|
"ToastItemMarkedAsNotFinishedFailed": "Oznaczenie pozycji jako ukończonej nie powiodło się",
|
||||||
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
|
||||||
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
|
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
|
||||||
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
|
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
|
||||||
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Продолжить",
|
"LabelContinueBooks": "Продолжить",
|
||||||
"LabelContinueEpisodes": "Продолжить эпизоды",
|
"LabelContinueEpisodes": "Продолжить эпизоды",
|
||||||
"LabelContinueListening": "Продолжить слушать",
|
"LabelContinueListening": "Продолжить слушать",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Продолжить серии",
|
"LabelContinueSeries": "Продолжить серии",
|
||||||
"LabelCustomTime": "Пользовательское время",
|
"LabelCustomTime": "Пользовательское время",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Перемотка назад",
|
"LabelJumpBackwardsTime": "Перемотка назад",
|
||||||
"LabelJumpForwardsTime": "Перемотка вперед",
|
"LabelJumpForwardsTime": "Перемотка вперед",
|
||||||
"LabelLanguage": "Язык",
|
"LabelLanguage": "Язык",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Подсветка",
|
"LabelLight": "Подсветка",
|
||||||
"LabelLineSpacing": "Межстрочный интервал",
|
"LabelLineSpacing": "Межстрочный интервал",
|
||||||
"LabelListenAgain": "Слушать снова",
|
"LabelListenAgain": "Слушать снова",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Читает",
|
"LabelNarrator": "Читает",
|
||||||
"LabelNarrators": "Чтецы",
|
"LabelNarrators": "Чтецы",
|
||||||
"LabelNewestAuthors": "Новые авторы",
|
"LabelNewestAuthors": "Новые авторы",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "Нет",
|
"LabelNo": "Нет",
|
||||||
"LabelNotFinished": "Не завершено",
|
"LabelNotFinished": "Не завершено",
|
||||||
"LabelNotStarted": "Не запущено",
|
"LabelNotStarted": "Не запущено",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Дата публикации",
|
"LabelPubDate": "Дата публикации",
|
||||||
"LabelPublishYear": "Год публикации",
|
"LabelPublishYear": "Год публикации",
|
||||||
"LabelRead": "Читать",
|
"LabelRead": "Читать",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Недавно добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Недавние серии",
|
"LabelRecentSeries": "Недавние серии",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
|
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
|
||||||
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
|
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
|
||||||
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
|
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
|
||||||
"LabelRSSFeedSlug": "Встроить RSS-канал",
|
"LabelRSSFeedSlug": "Встроить RSS-канал",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Сезон",
|
"LabelSeason": "Сезон",
|
||||||
"LabelSelectADevice": "Выбор девайса",
|
"LabelSelectADevice": "Выбор девайса",
|
||||||
"LabelSeries": "Серия",
|
"LabelSeries": "Серия",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beskrivning",
|
"LabelDescription": "Beskrivning",
|
||||||
@@ -149,6 +150,9 @@
|
|||||||
"LabelJumpBackwardsTime": "Jump backwards time",
|
"LabelJumpBackwardsTime": "Jump backwards time",
|
||||||
"LabelJumpForwardsTime": "Jump forwards time",
|
"LabelJumpForwardsTime": "Jump forwards time",
|
||||||
"LabelLanguage": "Språk",
|
"LabelLanguage": "Språk",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "Light",
|
"LabelLight": "Light",
|
||||||
"LabelLineSpacing": "Radavstånd",
|
"LabelLineSpacing": "Radavstånd",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "Listen Again",
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
"LabelNarrator": "Berättare",
|
"LabelNarrator": "Berättare",
|
||||||
"LabelNarrators": "Berättare",
|
"LabelNarrators": "Berättare",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Ej avslutad",
|
"LabelNotFinished": "Ej avslutad",
|
||||||
"LabelNotStarted": "Inte påbörjad",
|
"LabelNotStarted": "Inte påbörjad",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "Publiceringsdatum",
|
"LabelPubDate": "Publiceringsdatum",
|
||||||
"LabelPublishYear": "Publiceringsår",
|
"LabelPublishYear": "Publiceringsår",
|
||||||
"LabelRead": "Läst",
|
"LabelRead": "Läst",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
||||||
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
|
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
|
||||||
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
|
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
|
||||||
"LabelRSSFeedSlug": "RSS-flödesslag",
|
"LabelRSSFeedSlug": "RSS-flödesslag",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "Säsong",
|
"LabelSeason": "Säsong",
|
||||||
"LabelSelectADevice": "Select a device",
|
"LabelSelectADevice": "Select a device",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
|
|||||||
+17
-10
@@ -43,7 +43,7 @@
|
|||||||
"ButtonSendEbookToDevice": "将电子书发送到设备",
|
"ButtonSendEbookToDevice": "将电子书发送到设备",
|
||||||
"ButtonSeries": "系列",
|
"ButtonSeries": "系列",
|
||||||
"ButtonSetTimer": "设置定时器",
|
"ButtonSetTimer": "设置定时器",
|
||||||
"ButtonStream": "流",
|
"ButtonStream": "串流播放",
|
||||||
"ButtonSubmit": "提交",
|
"ButtonSubmit": "提交",
|
||||||
"ButtonSwitchServerUser": "切换服务器/用户",
|
"ButtonSwitchServerUser": "切换服务器/用户",
|
||||||
"ButtonUserStats": "用户统计信息",
|
"ButtonUserStats": "用户统计信息",
|
||||||
@@ -101,10 +101,11 @@
|
|||||||
"LabelClosePlayer": "关闭播放器",
|
"LabelClosePlayer": "关闭播放器",
|
||||||
"LabelCollapseSeries": "折叠系列",
|
"LabelCollapseSeries": "折叠系列",
|
||||||
"LabelComplete": "已完成",
|
"LabelComplete": "已完成",
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "继续看书",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "继续剧集",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "继续收听",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueReading": "继续阅读",
|
||||||
|
"LabelContinueSeries": "继续收听系列",
|
||||||
"LabelCustomTime": "自定义时间",
|
"LabelCustomTime": "自定义时间",
|
||||||
"LabelDescription": "描述",
|
"LabelDescription": "描述",
|
||||||
"LabelDisableAudioFadeOut": "禁用音频淡出",
|
"LabelDisableAudioFadeOut": "禁用音频淡出",
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
"LabelDisableShakeToResetHelp": "在计时器运行时摇晃设备,或者在计时器到期后2分钟内摇晃设备将重置睡眠计时器。启用此设置可禁用抖动以重置。",
|
"LabelDisableShakeToResetHelp": "在计时器运行时摇晃设备,或者在计时器到期后2分钟内摇晃设备将重置睡眠计时器。启用此设置可禁用抖动以重置。",
|
||||||
"LabelDisableVibrateOnReset": "重置时禁用振动",
|
"LabelDisableVibrateOnReset": "重置时禁用振动",
|
||||||
"LabelDisableVibrateOnResetHelp": "当睡眠计时器重置时,你的设备会振动。启用此设置以在睡眠计时器重置时不振动。",
|
"LabelDisableVibrateOnResetHelp": "当睡眠计时器重置时,你的设备会振动。启用此设置以在睡眠计时器重置时不振动。",
|
||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "发现",
|
||||||
"LabelDownload": "下载",
|
"LabelDownload": "下载",
|
||||||
"LabelDownloaded": "已下载",
|
"LabelDownloaded": "已下载",
|
||||||
"LabelDuration": "持续时间",
|
"LabelDuration": "持续时间",
|
||||||
@@ -149,9 +150,12 @@
|
|||||||
"LabelJumpBackwardsTime": "快退时间",
|
"LabelJumpBackwardsTime": "快退时间",
|
||||||
"LabelJumpForwardsTime": "快进时间",
|
"LabelJumpForwardsTime": "快进时间",
|
||||||
"LabelLanguage": "语言",
|
"LabelLanguage": "语言",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
|
"LabelLayoutAuto": "Auto",
|
||||||
|
"LabelLayoutSinglePage": "Single page",
|
||||||
"LabelLight": "轻",
|
"LabelLight": "轻",
|
||||||
"LabelLineSpacing": "行间距",
|
"LabelLineSpacing": "行间距",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "再次收听",
|
||||||
"LabelLocalBooks": "本地书籍",
|
"LabelLocalBooks": "本地书籍",
|
||||||
"LabelLocalPodcasts": "本地播客",
|
"LabelLocalPodcasts": "本地播客",
|
||||||
"LabelLockOrientation": "锁定方向",
|
"LabelLockOrientation": "锁定方向",
|
||||||
@@ -164,7 +168,8 @@
|
|||||||
"LabelName": "名称",
|
"LabelName": "名称",
|
||||||
"LabelNarrator": "演播者",
|
"LabelNarrator": "演播者",
|
||||||
"LabelNarrators": "演播者",
|
"LabelNarrators": "演播者",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "最新作者",
|
||||||
|
"LabelNewestEpisodes": "最新剧集",
|
||||||
"LabelNo": "取消",
|
"LabelNo": "取消",
|
||||||
"LabelNotFinished": "未听完",
|
"LabelNotFinished": "未听完",
|
||||||
"LabelNotStarted": "未开始",
|
"LabelNotStarted": "未开始",
|
||||||
@@ -182,12 +187,14 @@
|
|||||||
"LabelPubDate": "出版日期",
|
"LabelPubDate": "出版日期",
|
||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
"LabelRead": "阅读",
|
"LabelRead": "阅读",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelReadAgain": "再次阅读",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
|
"LabelRecentSeries": "最近添加系列",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
||||||
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
|
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
|
||||||
"LabelRSSFeedPreventIndexing": "防止索引",
|
"LabelRSSFeedPreventIndexing": "防止索引",
|
||||||
"LabelRSSFeedSlug": "RSS 源段",
|
"LabelRSSFeedSlug": "RSS 源段",
|
||||||
|
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||||
"LabelSeason": "季",
|
"LabelSeason": "季",
|
||||||
"LabelSelectADevice": "选择设备",
|
"LabelSelectADevice": "选择设备",
|
||||||
"LabelSeries": "系列",
|
"LabelSeries": "系列",
|
||||||
|
|||||||
+7
-11
@@ -1,14 +1,13 @@
|
|||||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
purge: {
|
content: [
|
||||||
safelist: [
|
'components/**/*.vue',
|
||||||
'bg-success',
|
'layouts/**/*.vue',
|
||||||
'bg-info',
|
'pages/**/*.vue',
|
||||||
'text-info'
|
'mixins/**/*.js',
|
||||||
]
|
'plugins/**/*.js'
|
||||||
},
|
],
|
||||||
darkMode: false,
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
screens: {
|
screens: {
|
||||||
@@ -65,8 +64,5 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
variants: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user