mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-07-28 23:48:45 +02:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f303abc19 | ||
|
|
85d6958025 | ||
|
|
669bd7827b | ||
|
|
2b48f0c6a9 | ||
|
|
2c44d38906 | ||
|
|
750726ff6a | ||
|
|
dafab492fe | ||
|
|
6419c8dc3a | ||
|
|
3bb5ce5924 | ||
|
|
882c2749ab | ||
|
|
26b0fae0fb | ||
|
|
fe921fd5b1 | ||
|
|
88e1877742 | ||
|
|
74758c7762 | ||
|
|
2000534e37 | ||
|
|
390388fe83 | ||
|
|
b9e3ccd0c1 |
@@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: 17
|
java-version: 21
|
||||||
|
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -36,8 +36,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 110
|
versionCode 111
|
||||||
versionName "0.9.79-beta"
|
versionName "0.9.80-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
manifestPlaceholders = [
|
manifestPlaceholders = [
|
||||||
"appAuthRedirectScheme": "com.audiobookshelf.app"
|
"appAuthRedirectScheme": "com.audiobookshelf.app"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
|||||||
import com.audiobookshelf.app.plugins.AbsDatabase
|
import com.audiobookshelf.app.plugins.AbsDatabase
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
import com.audiobookshelf.app.plugins.AbsFileSystem
|
import com.audiobookshelf.app.plugins.AbsFileSystem
|
||||||
|
import com.audiobookshelf.app.plugins.AbsLogger
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ class MainActivity : BridgeActivity() {
|
|||||||
registerPlugin(AbsDownloader::class.java)
|
registerPlugin(AbsDownloader::class.java)
|
||||||
registerPlugin(AbsFileSystem::class.java)
|
registerPlugin(AbsFileSystem::class.java)
|
||||||
registerPlugin(AbsDatabase::class.java)
|
registerPlugin(AbsDatabase::class.java)
|
||||||
|
registerPlugin(AbsLogger::class.java)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ object DeviceManager {
|
|||||||
var deviceData: DeviceData = dbManager.getDeviceData()
|
var deviceData: DeviceData = dbManager.getDeviceData()
|
||||||
var serverConnectionConfig: ServerConnectionConfig? = null
|
var serverConnectionConfig: ServerConnectionConfig? = null
|
||||||
|
|
||||||
val serverConnectionConfigId
|
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
|
||||||
get() = serverConnectionConfig?.id ?: ""
|
val serverConnectionConfigName get() = serverConnectionConfig?.name ?: ""
|
||||||
val serverAddress
|
val serverAddress
|
||||||
get() = serverConnectionConfig?.address ?: ""
|
get() = serverConnectionConfig?.address ?: ""
|
||||||
val serverUserId
|
val serverUserId
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.models.DownloadItem
|
import com.audiobookshelf.app.models.DownloadItem
|
||||||
|
import com.audiobookshelf.app.plugins.AbsLog
|
||||||
|
import com.audiobookshelf.app.plugins.AbsLogger
|
||||||
import io.paperdb.Paper
|
import io.paperdb.Paper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -287,4 +289,35 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
return sessions
|
return sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveLog(log:AbsLog) {
|
||||||
|
Paper.book("log").write(log.id, log)
|
||||||
|
}
|
||||||
|
fun getAllLogs() : List<AbsLog> {
|
||||||
|
val logs:MutableList<AbsLog> = mutableListOf()
|
||||||
|
Paper.book("log").allKeys.forEach { logId ->
|
||||||
|
Paper.book("log").read<AbsLog>(logId)?.let {
|
||||||
|
logs.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logs.sortedBy { it.timestamp }
|
||||||
|
}
|
||||||
|
fun removeAllLogs() {
|
||||||
|
Paper.book("log").destroy()
|
||||||
|
}
|
||||||
|
fun cleanLogs() {
|
||||||
|
val numberOfHoursToKeep = 48
|
||||||
|
val keepLogCutoff = System.currentTimeMillis() - (3600000 * numberOfHoursToKeep)
|
||||||
|
val allLogs = getAllLogs()
|
||||||
|
var logsRemoved = 0
|
||||||
|
allLogs.forEach {
|
||||||
|
if (it.timestamp < keepLogCutoff) {
|
||||||
|
Paper.book("log").delete(it.id)
|
||||||
|
logsRemoved++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (logsRemoved > 0) {
|
||||||
|
AbsLogger.info("DbManager", "cleanLogs: Removed $logsRemoved logs older than $numberOfHoursToKeep hours")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.audiobookshelf.app.data.MediaProgress
|
|||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
import com.audiobookshelf.app.plugins.AbsLogger
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
@@ -208,6 +209,7 @@ class MediaProgressSyncer(
|
|||||||
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
|
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently unused
|
||||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
it.updatedAt = mediaProgress.lastUpdate
|
it.updatedAt = mediaProgress.lastUpdate
|
||||||
@@ -260,44 +262,46 @@ class MediaProgressSyncer(
|
|||||||
tag,
|
tag,
|
||||||
"Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}"
|
"Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}"
|
||||||
)
|
)
|
||||||
|
AbsLogger.info("MediaProgressSyncer", "sync: Saved local progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.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
|
// Send sync to server also if connected to this server and local item belongs to this
|
||||||
// server
|
// server
|
||||||
|
val isConnectedToSameServer = it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId
|
||||||
if (hasNetworkConnection &&
|
if (hasNetworkConnection &&
|
||||||
shouldSyncServer &&
|
shouldSyncServer &&
|
||||||
!it.libraryItemId.isNullOrEmpty() &&
|
!it.libraryItemId.isNullOrEmpty() &&
|
||||||
it.serverConnectionConfigId != null &&
|
isConnectedToSameServer
|
||||||
DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId
|
|
||||||
) {
|
) {
|
||||||
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
|
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
|
||||||
|
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced local progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id})")
|
||||||
} else {
|
} else {
|
||||||
failedSyncs++
|
failedSyncs++
|
||||||
if (failedSyncs == 2) {
|
if (failedSyncs == 2) {
|
||||||
playerNotificationService.alertSyncFailing() // Show alert in client
|
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
}
|
}
|
||||||
Log.e(
|
AbsLogger.error("MediaProgressSyncer", "sync: Local progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (${DeviceManager.serverConnectionConfigName})")
|
||||||
tag,
|
|
||||||
"Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
AbsLogger.info("MediaProgressSyncer", "sync: Not sending local progress to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (hasNetworkConnection: $hasNetworkConnection) (isConnectedToSameServer: $isConnectedToSameServer)")
|
||||||
cb(SyncResult(false, null, null))
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (hasNetworkConnection && shouldSyncServer) {
|
} else if (hasNetworkConnection && shouldSyncServer) {
|
||||||
Log.d(tag, "sync: currentSessionId=$currentSessionId")
|
AbsLogger.info("MediaProgressSyncer", "sync: Sending progress sync to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
|
||||||
|
|
||||||
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
|
||||||
|
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
@@ -308,14 +312,12 @@ class MediaProgressSyncer(
|
|||||||
playerNotificationService.alertSyncFailing() // Show alert in client
|
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
}
|
}
|
||||||
Log.e(
|
AbsLogger.error("MediaProgressSyncer", "sync: Progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName})")
|
||||||
tag,
|
|
||||||
"Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
AbsLogger.info("MediaProgressSyncer", "sync: Not sending progress to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName}) (hasNetworkConnection: $hasNetworkConnection)")
|
||||||
cb(SyncResult(false, null, null))
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -36,6 +36,7 @@ import com.audiobookshelf.app.media.MediaManager
|
|||||||
import com.audiobookshelf.app.media.MediaProgressSyncer
|
import com.audiobookshelf.app.media.MediaProgressSyncer
|
||||||
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
|
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
|
||||||
import com.audiobookshelf.app.media.getUriToDrawable
|
import com.audiobookshelf.app.media.getUriToDrawable
|
||||||
|
import com.audiobookshelf.app.plugins.AbsLogger
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
@@ -452,7 +453,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
playbackSession
|
playbackSession
|
||||||
) // Save playback session to use when app is closed
|
) // Save playback session to use when app is closed
|
||||||
|
|
||||||
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
AbsLogger.info("PlayerNotificationService", "preparePlayer: Started playback session for item ${currentPlaybackSession?.mediaItemId}. MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
||||||
// Notify client
|
// Notify client
|
||||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
@@ -469,7 +470,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
val mediaSource: MediaSource
|
val mediaSource: MediaSource
|
||||||
|
|
||||||
if (playbackSession.isLocal) {
|
if (playbackSession.isLocal) {
|
||||||
Log.d(tag, "Playing Local Item")
|
AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing local item ${currentPlaybackSession?.mediaItemId}.")
|
||||||
val dataSourceFactory = DefaultDataSource.Factory(ctx)
|
val dataSourceFactory = DefaultDataSource.Factory(ctx)
|
||||||
|
|
||||||
val extractorsFactory = DefaultExtractorsFactory()
|
val extractorsFactory = DefaultExtractorsFactory()
|
||||||
@@ -483,7 +484,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
.createMediaSource(mediaItems[0])
|
.createMediaSource(mediaItems[0])
|
||||||
} else if (!playbackSession.isHLS) {
|
} else if (!playbackSession.isHLS) {
|
||||||
Log.d(tag, "Direct Playing Item")
|
AbsLogger.info("PlayerNotificationService", "preparePlayer: Direct playing item ${currentPlaybackSession?.mediaItemId}.")
|
||||||
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
|
|
||||||
val extractorsFactory = DefaultExtractorsFactory()
|
val extractorsFactory = DefaultExtractorsFactory()
|
||||||
@@ -498,7 +499,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
.createMediaSource(mediaItems[0])
|
.createMediaSource(mediaItems[0])
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Playing HLS Item")
|
AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing HLS stream of item ${currentPlaybackSession?.mediaItemId}.")
|
||||||
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
dataSourceFactory.setUserAgent(channelId)
|
dataSourceFactory.setUserAgent(channelId)
|
||||||
dataSourceFactory.setDefaultRequestProperties(
|
dataSourceFactory.setDefaultRequestProperties(
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
val playWhenReady = call.getBoolean("playWhenReady") == true
|
val playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
||||||
val startTimeOverride = call.getDouble("startTime")
|
val startTimeOverride = call.getDouble("startTime")
|
||||||
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
|
|
||||||
|
AbsLogger.info("AbsAudioPlayer", "prepareLibraryItem: lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
|
||||||
|
|
||||||
if (libraryItemId.isEmpty()) {
|
if (libraryItemId.isEmpty()) {
|
||||||
Log.e(tag, "Invalid call to play library item no library item id")
|
Log.e(tag, "Invalid call to play library item no library item id")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class AbsDatabase : Plugin() {
|
|||||||
|
|
||||||
DeviceManager.dbManager.cleanLocalMediaProgress()
|
DeviceManager.dbManager.cleanLocalMediaProgress()
|
||||||
DeviceManager.dbManager.cleanLocalLibraryItems()
|
DeviceManager.dbManager.cleanLocalLibraryItems()
|
||||||
|
DeviceManager.dbManager.cleanLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
@@ -220,12 +221,11 @@ class AbsDatabase : Plugin() {
|
|||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun syncLocalSessionsWithServer(call:PluginCall) {
|
fun syncLocalSessionsWithServer(call:PluginCall) {
|
||||||
if (DeviceManager.serverConnectionConfig == null) {
|
if (DeviceManager.serverConnectionConfig == null) {
|
||||||
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
|
AbsLogger.error("AbsDatabase", "syncLocalSessionsWithServer: not connected to server")
|
||||||
return call.resolve()
|
return call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
apiHandler.syncLocalMediaProgressForUser {
|
apiHandler.syncLocalMediaProgressForUser {
|
||||||
Log.d(tag, "Finished syncing local media progress for user")
|
|
||||||
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
||||||
|
|
||||||
if (savedSessions.isNotEmpty()) {
|
if (savedSessions.isNotEmpty()) {
|
||||||
@@ -233,6 +233,7 @@ class AbsDatabase : Plugin() {
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
|
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
|
||||||
} else {
|
} else {
|
||||||
|
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: Finished sending local playback sessions to server. Removing ${savedSessions.size} saved sessions.")
|
||||||
// Remove all local sessions
|
// Remove all local sessions
|
||||||
savedSessions.forEach {
|
savedSessions.forEach {
|
||||||
DeviceManager.dbManager.removePlaybackSession(it.id)
|
DeviceManager.dbManager.removePlaybackSession(it.id)
|
||||||
@@ -241,6 +242,7 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: No saved local playback sessions to send to server.")
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.getcapacitor.JSObject
|
||||||
|
import com.getcapacitor.Plugin
|
||||||
|
import com.getcapacitor.PluginCall
|
||||||
|
import com.getcapacitor.PluginMethod
|
||||||
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class AbsLog(
|
||||||
|
var id:String,
|
||||||
|
var tag:String,
|
||||||
|
var level:String,
|
||||||
|
var message:String,
|
||||||
|
var timestamp:Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AbsLogList(val value:List<AbsLog>)
|
||||||
|
|
||||||
|
@CapacitorPlugin(name = "AbsLogger")
|
||||||
|
class AbsLogger : Plugin() {
|
||||||
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
|
override fun load() {
|
||||||
|
onLogEmitter = { log:AbsLog ->
|
||||||
|
notifyListeners("onLog", JSObject(jacksonMapper.writeValueAsString(log)))
|
||||||
|
}
|
||||||
|
Log.i("AbsLogger", "Initialize AbsLogger plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var onLogEmitter:(log:AbsLog) -> Unit
|
||||||
|
|
||||||
|
fun log(level:String, tag:String, message:String) {
|
||||||
|
val absLog = AbsLog(id = UUID.randomUUID().toString(), tag, level, message, timestamp = System.currentTimeMillis())
|
||||||
|
DeviceManager.dbManager.saveLog(absLog)
|
||||||
|
onLogEmitter(absLog)
|
||||||
|
}
|
||||||
|
fun info(tag:String, message:String) {
|
||||||
|
Log.i("AbsLogger", message)
|
||||||
|
log("info", tag, message)
|
||||||
|
}
|
||||||
|
fun error(tag:String, message:String) {
|
||||||
|
Log.e("AbsLogger", message)
|
||||||
|
log("error", tag, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun info(call: PluginCall) {
|
||||||
|
val msg = call.getString("message") ?: return call.reject("No message")
|
||||||
|
val tag = call.getString("tag") ?: ""
|
||||||
|
info(tag, msg)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun error(call: PluginCall) {
|
||||||
|
val msg = call.getString("message") ?: return call.reject("No message")
|
||||||
|
val tag = call.getString("tag") ?: ""
|
||||||
|
error(tag, msg)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getAllLogs(call: PluginCall) {
|
||||||
|
val absLogs = DeviceManager.dbManager.getAllLogs()
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(AbsLogList(absLogs))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun clearLogs(call: PluginCall) {
|
||||||
|
DeviceManager.dbManager.removeAllLogs()
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import com.audiobookshelf.app.media.MediaProgressSyncData
|
|||||||
import com.audiobookshelf.app.media.SyncResult
|
import com.audiobookshelf.app.media.SyncResult
|
||||||
import com.audiobookshelf.app.models.User
|
import com.audiobookshelf.app.models.User
|
||||||
import com.audiobookshelf.app.BuildConfig
|
import com.audiobookshelf.app.BuildConfig
|
||||||
|
import com.audiobookshelf.app.plugins.AbsLogger
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
@@ -468,22 +469,27 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||||
|
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
|
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
|
||||||
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
|
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})")
|
||||||
|
|
||||||
postRequest("/api/session/local-all", payload, null) {
|
postRequest("/api/session/local-all", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
Log.e(tag, "Failed to sync local sessions")
|
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync local sessions. (${it.getString("error")})")
|
||||||
cb(false, it.getString("error"))
|
cb(false, it.getString("error"))
|
||||||
} else {
|
} else {
|
||||||
val response = jacksonMapper.readValue<LocalSessionsSyncResponsePayload>(it.toString())
|
val response = jacksonMapper.readValue<LocalSessionsSyncResponsePayload>(it.toString())
|
||||||
response.results.forEach { localSessionSyncResult ->
|
response.results.forEach { localSessionSyncResult ->
|
||||||
Log.d(tag, "Synced session result ${localSessionSyncResult.id}|${localSessionSyncResult.progressSynced}|${localSessionSyncResult.success}")
|
Log.d(tag, "Synced session result ${localSessionSyncResult.id}|${localSessionSyncResult.progressSynced}|${localSessionSyncResult.success}")
|
||||||
|
|
||||||
playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session ->
|
playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session ->
|
||||||
if (localSessionSyncResult.progressSynced == true) {
|
if (localSessionSyncResult.progressSynced == true) {
|
||||||
val syncResult = SyncResult(true, true, "Progress synced on server")
|
val syncResult = SyncResult(true, true, "Progress synced on server")
|
||||||
MediaEventManager.saveEvent(session, syncResult)
|
MediaEventManager.saveEvent(session, syncResult)
|
||||||
Log.i(tag, "Successfully synced session ${session.displayTitle} with server")
|
|
||||||
|
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Synced session \"${session.displayTitle}\" with server, server progress was updated for item ${session.mediaItemId}")
|
||||||
} else if (!localSessionSyncResult.success) {
|
} else if (!localSessionSyncResult.success) {
|
||||||
Log.e(tag, "Failed to sync session ${session.displayTitle} with server. Error: ${localSessionSyncResult.error}")
|
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync session \"${session.displayTitle}\" with server. Error: ${localSessionSyncResult.error}")
|
||||||
|
} else {
|
||||||
|
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Synced session \"${session.displayTitle}\" with server. Server progress was up-to-date for item ${session.mediaItemId}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,37 +499,72 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun syncLocalMediaProgressForUser(cb: () -> Unit) {
|
fun syncLocalMediaProgressForUser(cb: () -> Unit) {
|
||||||
|
AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: Server connection ${DeviceManager.serverConnectionConfigName}")
|
||||||
|
|
||||||
// Get all local media progress for this server
|
// Get all local media progress for this server
|
||||||
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
||||||
if (allLocalMediaProgress.isEmpty()) {
|
if (allLocalMediaProgress.isEmpty()) {
|
||||||
Log.d(tag, "No local media progress to sync")
|
AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: No local media progress to sync")
|
||||||
return cb()
|
return cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentUser { _user ->
|
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Found ${allLocalMediaProgress.size} local media progress")
|
||||||
_user?.let { user->
|
|
||||||
|
getCurrentUser { user ->
|
||||||
|
if (user == null) {
|
||||||
|
AbsLogger.error("ApiHandler", "syncLocalMediaProgressForUser: Failed to load user from server (${DeviceManager.serverConnectionConfigName})")
|
||||||
|
} else {
|
||||||
|
var numLocalMediaProgressUptToDate = 0
|
||||||
|
var numLocalMediaProgressUpdated = 0
|
||||||
|
|
||||||
// Compare server user progress with local progress
|
// Compare server user progress with local progress
|
||||||
user.mediaProgress.forEach { mediaProgress ->
|
user.mediaProgress.forEach { mediaProgress ->
|
||||||
// Get matching local media progress
|
// Get matching local media progress
|
||||||
allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress ->
|
allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress ->
|
||||||
if (mediaProgress.lastUpdate > localMediaProgress.lastUpdate) {
|
if (mediaProgress.lastUpdate > localMediaProgress.lastUpdate) {
|
||||||
Log.d(tag, "Server progress for media item id=\"${mediaProgress.mediaItemId}\" is more recent then local. Updating local current time ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
|
val updateLogs = mutableListOf<String>()
|
||||||
|
if (mediaProgress.progress != localMediaProgress.progress) {
|
||||||
|
updateLogs.add("Updated progress from ${localMediaProgress.progress} to ${mediaProgress.progress}")
|
||||||
|
}
|
||||||
|
if (mediaProgress.currentTime != localMediaProgress.currentTime) {
|
||||||
|
updateLogs.add("Updated currentTime from ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
|
||||||
|
}
|
||||||
|
if (mediaProgress.isFinished != localMediaProgress.isFinished) {
|
||||||
|
updateLogs.add("Updated isFinished from ${localMediaProgress.isFinished} to ${mediaProgress.isFinished}")
|
||||||
|
}
|
||||||
|
if (mediaProgress.ebookProgress != localMediaProgress.ebookProgress) {
|
||||||
|
updateLogs.add("Updated ebookProgress from ${localMediaProgress.isFinished} to ${mediaProgress.isFinished}")
|
||||||
|
}
|
||||||
|
if (updateLogs.isNotEmpty()) {
|
||||||
|
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Server progress for item \"${mediaProgress.mediaItemId}\" is more recent than local (server lastUpdate=${mediaProgress.lastUpdate}, local lastUpdate=${localMediaProgress.lastUpdate}). ${updateLogs.joinToString()}")
|
||||||
|
}
|
||||||
|
|
||||||
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||||
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
|
|
||||||
|
// Only report sync if progress changed
|
||||||
|
if (updateLogs.isNotEmpty()) {
|
||||||
|
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
|
||||||
|
}
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
numLocalMediaProgressUpdated++
|
||||||
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
|
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
|
||||||
// Patch ebook progress to server
|
// Patch ebook progress to server
|
||||||
|
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Local progress for ebook item \"${mediaProgress.mediaItemId}\" is more recent than server progress. Local progress last updated ${localMediaProgress.lastUpdate}, server progress last updated ${mediaProgress.lastUpdate}. Sending server request to update ebook progress from ${mediaProgress.ebookProgress} to ${localMediaProgress.ebookProgress}")
|
||||||
val endpoint = "/api/me/progress/${localMediaProgress.libraryItemId}"
|
val endpoint = "/api/me/progress/${localMediaProgress.libraryItemId}"
|
||||||
val updatePayload = JSObject()
|
val updatePayload = JSObject()
|
||||||
updatePayload.put("ebookLocation", localMediaProgress.ebookLocation)
|
updatePayload.put("ebookLocation", localMediaProgress.ebookLocation)
|
||||||
updatePayload.put("ebookProgress", localMediaProgress.ebookProgress)
|
updatePayload.put("ebookProgress", localMediaProgress.ebookProgress)
|
||||||
updatePayload.put("lastUpdate", localMediaProgress.lastUpdate)
|
updatePayload.put("lastUpdate", localMediaProgress.lastUpdate)
|
||||||
patchRequest(endpoint,updatePayload) {
|
patchRequest(endpoint,updatePayload) {
|
||||||
Log.d(tag, "syncLocalMediaProgressForUser patched ebook progress")
|
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Successfully updated server ebook progress for item item \"${mediaProgress.mediaItemId}\"")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
numLocalMediaProgressUptToDate++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Finishing syncing local media progress with server. $numLocalMediaProgressUptToDate up-to-date, $numLocalMediaProgressUpdated updated")
|
||||||
}
|
}
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
import { AbsAudioPlayer, AbsLogger } from '@/plugins/capacitor'
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
|
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
|
||||||
|
|
||||||
@@ -190,6 +190,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
|
await AbsLogger.info({ tag: 'AudioPlayerContainer', message: `playLibraryItem: Received play request for library item ${payload.libraryItemId} ${payload.episodeId ? `episode ${payload.episodeId}` : ''}` })
|
||||||
const libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
const episodeId = payload.episodeId
|
const episodeId = payload.episodeId
|
||||||
const startTime = payload.startTime
|
const startTime = payload.startTime
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ export default {
|
|||||||
to: '/settings'
|
to: '/settings'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.$platform !== 'ios') {
|
||||||
|
items.push({
|
||||||
|
icon: 'bug_report',
|
||||||
|
iconOutlined: true,
|
||||||
|
text: this.$strings.ButtonLogs,
|
||||||
|
to: '/logs'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.serverConnectionConfig) {
|
if (this.serverConnectionConfig) {
|
||||||
items.push({
|
items.push({
|
||||||
icon: 'language',
|
icon: 'language',
|
||||||
|
|||||||
@@ -15,20 +15,17 @@
|
|||||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; };
|
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; };
|
||||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.swift */; };
|
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.swift */; };
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */; };
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */; };
|
||||||
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE628043E72006DB301 /* AbsDatabase.m */; };
|
|
||||||
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */; };
|
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */; };
|
||||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEA280443DD006DB301 /* Database.swift */; };
|
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEA280443DD006DB301 /* Database.swift */; };
|
||||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEC28044E6C006DB301 /* Store.swift */; };
|
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEC28044E6C006DB301 /* Store.swift */; };
|
||||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970B2806E2590096F747 /* ApiClient.swift */; };
|
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970B2806E2590096F747 /* ApiClient.swift */; };
|
||||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
|
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
||||||
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B951282EE822008272D4 /* AbsDownloader.m */; };
|
|
||||||
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; };
|
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; };
|
||||||
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; };
|
|
||||||
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
|
||||||
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
|
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
|
||||||
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
|
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
|
||||||
|
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */; };
|
||||||
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
|
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
|
||||||
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
|
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
@@ -90,21 +87,18 @@
|
|||||||
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSession.swift; sourceTree = "<group>"; };
|
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSession.swift; sourceTree = "<group>"; };
|
||||||
3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = "<group>"; };
|
3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = "<group>"; };
|
||||||
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDatabase.swift; sourceTree = "<group>"; };
|
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDatabase.swift; sourceTree = "<group>"; };
|
||||||
3AD4FCE628043E72006DB301 /* AbsDatabase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDatabase.m; sourceTree = "<group>"; };
|
|
||||||
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionConfig.swift; sourceTree = "<group>"; };
|
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionConfig.swift; sourceTree = "<group>"; };
|
||||||
3AD4FCEA280443DD006DB301 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
3AD4FCEA280443DD006DB301 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||||
3AD4FCEC28044E6C006DB301 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
3AD4FCEC28044E6C006DB301 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
||||||
3AF1970B2806E2590096F747 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = "<group>"; };
|
3AF1970B2806E2590096F747 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = "<group>"; };
|
||||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
||||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
|
|
||||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
||||||
4D66B951282EE822008272D4 /* AbsDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDownloader.m; sourceTree = "<group>"; };
|
|
||||||
4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = "<group>"; };
|
4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = "<group>"; };
|
||||||
4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = "<group>"; };
|
|
||||||
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = "<group>"; };
|
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = "<group>"; };
|
||||||
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
|
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
|
||||||
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
||||||
|
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsLogger.swift; sourceTree = "<group>"; };
|
||||||
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
|
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
|
||||||
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
|
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
@@ -217,13 +211,10 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */,
|
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */,
|
||||||
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
|
|
||||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
||||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
|
|
||||||
4D66B951282EE822008272D4 /* AbsDownloader.m */,
|
|
||||||
4D66B953282EE87C008272D4 /* AbsDownloader.swift */,
|
4D66B953282EE87C008272D4 /* AbsDownloader.swift */,
|
||||||
4D66B955282EE951008272D4 /* AbsFileSystem.m */,
|
|
||||||
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */,
|
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */,
|
||||||
|
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */,
|
||||||
);
|
);
|
||||||
path = plugins;
|
path = plugins;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -531,7 +522,6 @@
|
|||||||
files = (
|
files = (
|
||||||
E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */,
|
E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */,
|
||||||
E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */,
|
E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */,
|
||||||
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */,
|
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||||
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */,
|
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */,
|
||||||
E9FA07E328C82848005520B0 /* Logger.swift in Sources */,
|
E9FA07E328C82848005520B0 /* Logger.swift in Sources */,
|
||||||
@@ -545,6 +535,7 @@
|
|||||||
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
||||||
E9D5504C28AC1AE000C746DD /* PodcastEpisode.swift in Sources */,
|
E9D5504C28AC1AE000C746DD /* PodcastEpisode.swift in Sources */,
|
||||||
E9D5506A28AC1DF100C746DD /* LocalFile.swift in Sources */,
|
E9D5506A28AC1DF100C746DD /* LocalFile.swift in Sources */,
|
||||||
|
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */,
|
||||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
||||||
E9D5506F28AC1E8E00C746DD /* DownloadItem.swift in Sources */,
|
E9D5506F28AC1E8E00C746DD /* DownloadItem.swift in Sources */,
|
||||||
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
||||||
@@ -553,7 +544,6 @@
|
|||||||
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
||||||
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */,
|
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */,
|
||||||
E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */,
|
E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */,
|
||||||
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
|
|
||||||
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */,
|
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */,
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
||||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
||||||
@@ -561,7 +551,6 @@
|
|||||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||||
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
|
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
|
||||||
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
|
|
||||||
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
|
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
|
||||||
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
|
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
|
||||||
E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */,
|
E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */,
|
||||||
@@ -574,7 +563,6 @@
|
|||||||
E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */,
|
E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */,
|
||||||
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */,
|
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */,
|
||||||
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */,
|
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */,
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
|
||||||
E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */,
|
E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */,
|
||||||
E9D5505028AC1B3E00C746DD /* Author.swift in Sources */,
|
E9D5505028AC1B3E00C746DD /* Author.swift in Sources */,
|
||||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
||||||
@@ -744,12 +732,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 = 36;
|
CURRENT_PROJECT_VERSION = 37;
|
||||||
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.79;
|
MARKETING_VERSION = 0.9.80;
|
||||||
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)";
|
||||||
@@ -768,12 +756,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 = 36;
|
CURRENT_PROJECT_VERSION = 37;
|
||||||
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.79;
|
MARKETING_VERSION = 0.9.80;
|
||||||
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 = "";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class MyViewController: CAPBridgeViewController {
|
|||||||
bridge?.registerPluginInstance(AbsAudioPlayer())
|
bridge?.registerPluginInstance(AbsAudioPlayer())
|
||||||
bridge?.registerPluginInstance(AbsDownloader())
|
bridge?.registerPluginInstance(AbsDownloader())
|
||||||
bridge?.registerPluginInstance(AbsFileSystem())
|
bridge?.registerPluginInstance(AbsFileSystem())
|
||||||
|
bridge?.registerPluginInstance(AbsLogger())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
//
|
|
||||||
// AbsAudioPlayer.m
|
|
||||||
// App
|
|
||||||
//
|
|
||||||
// Created by Rasmus Krämer on 13.04.22.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <Capacitor/Capacitor.h>
|
|
||||||
|
|
||||||
CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
|
|
||||||
CAP_PLUGIN_METHOD(onReady, CAPPluginReturnNone);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(prepareLibraryItem, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(setChapterTrack, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(cancelSleepTimer, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(decreaseSleepTime, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(increaseSleepTime, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(getSleepTimerTime, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(setSleepTimer, CAPPluginReturnPromise);
|
|
||||||
)
|
|
||||||
@@ -11,7 +11,29 @@ import RealmSwift
|
|||||||
import Network
|
import Network
|
||||||
|
|
||||||
@objc(AbsAudioPlayer)
|
@objc(AbsAudioPlayer)
|
||||||
public class AbsAudioPlayer: CAPPlugin {
|
public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
|
||||||
|
public var identifier = "AbsAudioPlayerPlugin"
|
||||||
|
public var jsName = "AbsAudioPlayer"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "onReady", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "prepareLibraryItem", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "closePlayback", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setPlaybackSpeed", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setChapterTrack", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "playPlayer", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "pausePlayer", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "playPause", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "seek", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "seekForward", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "seekBackward", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getCurrentTime", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "cancelSleepTimer", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "decreaseSleepTime", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "increaseSleepTime", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getSleepTimerTime", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise)
|
||||||
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsAudioPlayer")
|
private let logger = AppLogger(category: "AbsAudioPlayer")
|
||||||
|
|
||||||
private var initialPlayWhenReady = false
|
private var initialPlayWhenReady = false
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// AbsDatabase.m
|
|
||||||
// App
|
|
||||||
//
|
|
||||||
// Created by Rasmus Krämer on 11.04.22.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <Capacitor/Capacitor.h>
|
|
||||||
|
|
||||||
CAP_PLUGIN(AbsDatabase, "AbsDatabase",
|
|
||||||
CAP_PLUGIN_METHOD(setCurrentServerConnectionConfig, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(removeServerConnectionConfig, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(logout, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(getDeviceData, CAPPluginReturnPromise);
|
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(removeLocalMediaProgress, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(syncServerMediaProgressWithLocalMediaProgress, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(syncLocalSessionsWithServer, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(updateLocalMediaProgressFinished, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(updateLocalEbookProgress, CAPPluginReturnPromise);
|
|
||||||
)
|
|
||||||
@@ -27,7 +27,27 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc(AbsDatabase)
|
@objc(AbsDatabase)
|
||||||
public class AbsDatabase: CAPPlugin {
|
public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
||||||
|
public var identifier = "AbsDatabasePlugin"
|
||||||
|
public var jsName = "AbsDatabase"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "setCurrentServerConnectionConfig", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "removeServerConnectionConfig", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getDeviceData", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getLocalLibraryItems", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getLocalLibraryItem", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getLocalLibraryItemByLId", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getLocalLibraryItemsInFolder", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getAllLocalMediaProgress", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "removeLocalMediaProgress", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "syncServerMediaProgressWithLocalMediaProgress", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "syncLocalSessionsWithServer", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "updateLocalMediaProgressFinished", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "updateDeviceSettings", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise)
|
||||||
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsDatabase")
|
private let logger = AppLogger(category: "AbsDatabase")
|
||||||
|
|
||||||
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
|
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// AbsDownloader.m
|
|
||||||
// App
|
|
||||||
//
|
|
||||||
// Created by advplyr on 5/13/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <Capacitor/Capacitor.h>
|
|
||||||
|
|
||||||
CAP_PLUGIN(AbsDownloader, "AbsDownloader",
|
|
||||||
CAP_PLUGIN_METHOD(downloadLibraryItem, CAPPluginReturnPromise);
|
|
||||||
)
|
|
||||||
@@ -10,7 +10,12 @@ import Capacitor
|
|||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
|
||||||
@objc(AbsDownloader)
|
@objc(AbsDownloader)
|
||||||
public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDelegate {
|
||||||
|
public var identifier = "AbsDownloaderPlugin"
|
||||||
|
public var jsName = "AbsDownloader"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "downloadLibraryItem", returnType: CAPPluginReturnPromise)
|
||||||
|
]
|
||||||
|
|
||||||
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
//
|
|
||||||
// AbsFileSystem.m
|
|
||||||
// App
|
|
||||||
//
|
|
||||||
// Created by advplyr on 5/13/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <Capacitor/Capacitor.h>
|
|
||||||
|
|
||||||
CAP_PLUGIN(AbsFileSystem, "AbsFileSystem",
|
|
||||||
CAP_PLUGIN_METHOD(selectFolder, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(checkFolderPermission, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(scanFolder, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(removeFolder, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(removeLocalLibraryItem, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(scanLocalLibraryItem, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(deleteItem, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(deleteTrackFromItem, CAPPluginReturnPromise);
|
|
||||||
)
|
|
||||||
@@ -9,7 +9,20 @@ import Foundation
|
|||||||
import Capacitor
|
import Capacitor
|
||||||
|
|
||||||
@objc(AbsFileSystem)
|
@objc(AbsFileSystem)
|
||||||
public class AbsFileSystem: CAPPlugin {
|
public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
|
||||||
|
public var identifier = "AbsFileSystemPlugin"
|
||||||
|
public var jsName = "AbsFileSystem"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "selectFolder", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "checkFolderPermission", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "scanFolder", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "removeFolder", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "removeLocalLibraryItem", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "scanLocalLibraryItem", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "deleteItem", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise)
|
||||||
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsFileSystem")
|
private let logger = AppLogger(category: "AbsFileSystem")
|
||||||
|
|
||||||
@objc func selectFolder(_ call: CAPPluginCall) {
|
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// AbsLogger.swift
|
||||||
|
// Audiobookshelf
|
||||||
|
//
|
||||||
|
// Created by advplyr on 4/20/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@objc(AbsLogger)
|
||||||
|
public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
|
||||||
|
public var identifier = "AbsLoggerPlugin"
|
||||||
|
public var jsName = "AbsLogger"
|
||||||
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
|
CAPPluginMethod(name: "info", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "error", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getAllLogs", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "clearLogs", returnType: CAPPluginReturnPromise)
|
||||||
|
]
|
||||||
|
|
||||||
|
private let logger = AppLogger(category: "AbsLogger")
|
||||||
|
|
||||||
|
@objc func info(_ call: CAPPluginCall) {
|
||||||
|
let message = call.getString("message") ?? ""
|
||||||
|
let tag = call.getString("tag") ?? ""
|
||||||
|
|
||||||
|
logger.log("[\(tag)] \(message)")
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func error(_ call: CAPPluginCall) {
|
||||||
|
let message = call.getString("message") ?? ""
|
||||||
|
let tag = call.getString("tag") ?? ""
|
||||||
|
|
||||||
|
logger.error("[\(tag)] \(message)")
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getAllLogs(_ call: CAPPluginCall) {
|
||||||
|
call.unimplemented("Not implemented on iOS")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func clearLogs(_ call: CAPPluginCall) {
|
||||||
|
call.unimplemented("Not implemented on iOS")
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-12
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CapacitorHttp } from '@capacitor/core'
|
import { CapacitorHttp } from '@capacitor/core'
|
||||||
|
import { AbsLogger } from '@/plugins/capacitor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -72,6 +73,9 @@ export default {
|
|||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
currentLibraryName() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
|
},
|
||||||
attemptingConnection: {
|
attemptingConnection: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.state.attemptingConnection
|
return this.$store.state.attemptingConnection
|
||||||
@@ -114,6 +118,7 @@ export default {
|
|||||||
console.warn('[default] attemptConnection')
|
console.warn('[default] attemptConnection')
|
||||||
if (!this.networkConnected) {
|
if (!this.networkConnected) {
|
||||||
console.warn('[default] No network connection')
|
console.warn('[default] No network connection')
|
||||||
|
AbsLogger.info({ tag: 'default', message: 'attemptConnection: No network connection' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.attemptingConnection) {
|
if (this.attemptingConnection) {
|
||||||
@@ -134,13 +139,14 @@ export default {
|
|||||||
if (!serverConfig) {
|
if (!serverConfig) {
|
||||||
// No last server config set
|
// No last server config set
|
||||||
this.attemptingConnection = false
|
this.attemptingConnection = false
|
||||||
|
AbsLogger.info({ tag: 'default', message: 'attemptConnection: No last server config set' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
|
AbsLogger.info({ tag: 'default', message: `attemptConnection: Got server config, attempt authorize (${serverConfig.name})` })
|
||||||
|
|
||||||
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 6000).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)
|
AbsLogger.error({ tag: 'default', message: `attemptConnection: Server auth failed (${serverConfig.name})` })
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!authRes) {
|
if (!authRes) {
|
||||||
@@ -166,7 +172,7 @@ export default {
|
|||||||
|
|
||||||
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
|
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
|
||||||
|
|
||||||
console.log('[default] Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
|
AbsLogger.info({ tag: 'default', message: `attemptConnection: Successful connection to last saved server config (${serverConnectionConfig.name})` })
|
||||||
await this.initLibraries()
|
await this.initLibraries()
|
||||||
this.attemptingConnection = false
|
this.attemptingConnection = false
|
||||||
},
|
},
|
||||||
@@ -186,7 +192,8 @@ export default {
|
|||||||
}
|
}
|
||||||
this.inittingLibraries = true
|
this.inittingLibraries = true
|
||||||
await this.$store.dispatch('libraries/load')
|
await this.$store.dispatch('libraries/load')
|
||||||
console.log(`[default] initLibraries loaded ${this.currentLibraryId}`)
|
|
||||||
|
AbsLogger.info({ tag: 'default', message: `initLibraries loading library ${this.currentLibraryName}` })
|
||||||
await this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
await this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
this.$eventBus.$emit('library-changed')
|
this.$eventBus.$emit('library-changed')
|
||||||
this.inittingLibraries = false
|
this.inittingLibraries = false
|
||||||
@@ -197,7 +204,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[default] Calling syncLocalSessions')
|
AbsLogger.info({ tag: 'default', message: 'Calling syncLocalSessions' })
|
||||||
const response = await this.$db.syncLocalSessionsWithServer(isFirstSync)
|
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)
|
||||||
@@ -214,12 +221,12 @@ export default {
|
|||||||
},
|
},
|
||||||
async userMediaProgressUpdated(payload) {
|
async userMediaProgressUpdated(payload) {
|
||||||
const prog = payload.data // MediaProgress
|
const prog = payload.data // MediaProgress
|
||||||
console.log(`[default] userMediaProgressUpdate checking for local media progress ${payload.id}`)
|
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Received updated media progress for current user from socket event. Media item id ${payload.id}` })
|
||||||
|
|
||||||
// Check if this media item is currently open in the player, paused, and this progress update is coming from a different session
|
// Check if this media item is currently open in the player, paused, and this progress update is coming from a different session
|
||||||
const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId)
|
const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId)
|
||||||
if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) {
|
if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) {
|
||||||
console.log('[default] userMediaProgressUpdated for current open media item', payload.data.currentTime)
|
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Item is currently open in player, paused and this progress update is coming from a different session. Updating playback time to ${payload.data.currentTime}` })
|
||||||
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
|
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,12 +237,12 @@ export default {
|
|||||||
// Progress update is more recent then local progress
|
// Progress update is more recent then local progress
|
||||||
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
|
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
|
||||||
if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) {
|
if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) {
|
||||||
console.log('[default] syncing progress server lastUpdate > local lastUpdate but currentTime and isFinished is equal')
|
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: server lastUpdate is more recent but progress is up-to-date (libraryItemId: ${prog.libraryItemId}${prog.episodeId ? ` episodeId: ${prog.episodeId}` : ''})` })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server progress is more up-to-date
|
// Server progress is more up-to-date
|
||||||
console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}`)
|
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}` })
|
||||||
const payload = {
|
const payload = {
|
||||||
localMediaProgressId: localProg.id,
|
localMediaProgressId: localProg.id,
|
||||||
mediaProgress: prog
|
mediaProgress: prog
|
||||||
@@ -273,7 +280,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newLocalMediaProgress?.id) {
|
if (newLocalMediaProgress?.id) {
|
||||||
console.log(`[default] local media progress updated for ${newLocalMediaProgress.id}`)
|
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: local media progress updated for ${newLocalMediaProgress.id}` })
|
||||||
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
|
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -310,6 +317,7 @@ export default {
|
|||||||
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
|
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||||
|
|
||||||
if (this.$store.state.isFirstLoad) {
|
if (this.$store.state.isFirstLoad) {
|
||||||
|
AbsLogger.info({ tag: 'default', message: `mounted: initializing first load (${this.$platform} v${this.$config.version})` })
|
||||||
this.$store.commit('setIsFirstLoad', false)
|
this.$store.commit('setIsFirstLoad', false)
|
||||||
|
|
||||||
this.loadSavedSettings()
|
this.loadSavedSettings()
|
||||||
@@ -322,17 +330,18 @@ export default {
|
|||||||
await this.$store.dispatch('setupNetworkListener')
|
await this.$store.dispatch('setupNetworkListener')
|
||||||
|
|
||||||
if (this.$store.state.user.serverConnectionConfig) {
|
if (this.$store.state.user.serverConnectionConfig) {
|
||||||
|
AbsLogger.info({ tag: 'default', message: `mounted: Server connected, init libraries (${this.$store.getters['user/getServerConfigName']})` })
|
||||||
await this.initLibraries()
|
await this.initLibraries()
|
||||||
} else {
|
} else {
|
||||||
|
AbsLogger.info({ tag: 'default', message: `mounted: Server not connected, attempt connection` })
|
||||||
await this.attemptConnection()
|
await this.attemptConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
|
||||||
await this.syncLocalSessions(true)
|
await this.syncLocalSessions(true)
|
||||||
|
|
||||||
this.hasMounted = true
|
this.hasMounted = true
|
||||||
|
|
||||||
console.log('[default] fully initialized')
|
AbsLogger.info({ tag: 'default', message: 'mounted: fully initialized' })
|
||||||
this.$eventBus.$emit('abs-ui-ready')
|
this.$eventBus.$emit('abs-ui-ready')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.79-beta",
|
"version": "0.9.80-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.79-beta",
|
"version": "0.9.80-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/keep-awake": "^7.0.0",
|
"@capacitor-community/keep-awake": "^7.0.0",
|
||||||
"@capacitor-community/volume-buttons": "^7.0.0",
|
"@capacitor-community/volume-buttons": "^7.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.79-beta",
|
"version": "0.9.80-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",
|
||||||
|
|||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full py-4">
|
||||||
|
<div class="flex items-center mb-2 space-x-2 px-4">
|
||||||
|
<p class="text-lg font-bold">{{ $strings.ButtonLogs }}</p>
|
||||||
|
<ui-icon-btn outlined borderless :icon="isCopied ? 'check' : 'content_copy'" @click="copyToClipboard" />
|
||||||
|
<ui-icon-btn outlined borderless icon="share" @click="shareLogs" />
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<ui-icon-btn outlined borderless icon="more_vert" @click="showDialog = true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full h-[calc(100%-40px)] overflow-y-auto relative" ref="logContainer">
|
||||||
|
<div v-if="!logs.length && !isLoading" class="flex items-center justify-center h-32 p-4">
|
||||||
|
<p class="text-gray-400">{{ $strings.MessageNoLogs }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasScrolled" class="sticky top-0 left-0 w-full h-10 bg-gradient-to-t from-transparent to-bg z-10 pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div v-for="(log, index) in logs" :key="log.id" class="py-2 px-4" :class="{ 'bg-white/5': index % 2 === 0 }">
|
||||||
|
<div class="flex items-center space-x-4 mb-1">
|
||||||
|
<div class="text-xs uppercase font-bold" :class="{ 'text-error': log.level === 'error', 'text-blue-500': log.level === 'info' }">{{ log.level }}</div>
|
||||||
|
<div class="text-xs text-gray-400">{{ formatEpochToDatetimeString(log.timestamp) }}</div>
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<div class="text-xs text-gray-400">{{ log.tag }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">{{ maskServerAddress ? log.maskedMessage : log.message }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<modals-dialog v-model="showDialog" :items="dialogItems" @action="dialogAction" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { AbsLogger } from '@/plugins/capacitor'
|
||||||
|
import { FileSharer } from '@webnativellc/capacitor-filesharer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logs: [],
|
||||||
|
isLoading: true,
|
||||||
|
isCopied: false,
|
||||||
|
hasScrolled: false,
|
||||||
|
maskServerAddress: true,
|
||||||
|
showDialog: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dialogItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.maskServerAddress ? this.$strings.ButtonUnmaskServerAddress : this.$strings.ButtonMaskServerAddress,
|
||||||
|
value: 'toggle-mask-server-address',
|
||||||
|
icon: this.maskServerAddress ? 'remove_moderator' : 'shield'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonClearLogs,
|
||||||
|
value: 'clear-logs',
|
||||||
|
icon: 'delete'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async dialogAction(action) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
|
|
||||||
|
if (action === 'clear-logs') {
|
||||||
|
await AbsLogger.clearLogs()
|
||||||
|
this.logs = []
|
||||||
|
} else if (action === 'toggle-mask-server-address') {
|
||||||
|
this.maskServerAddress = !this.maskServerAddress
|
||||||
|
}
|
||||||
|
this.showDialog = false
|
||||||
|
},
|
||||||
|
toggleMaskServerAddress() {
|
||||||
|
this.maskServerAddress = !this.maskServerAddress
|
||||||
|
},
|
||||||
|
async copyToClipboard() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
|
this.$copyToClipboard(this.getLogsString()).then(() => {
|
||||||
|
this.isCopied = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isCopied = false
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Formats an epoch timestamp to YYYY-MM-DD HH:mm:ss.SSS
|
||||||
|
* Use 24 hour time format
|
||||||
|
* @param {number} epoch
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatEpochToDatetimeString(epoch) {
|
||||||
|
return new Date(epoch)
|
||||||
|
.toLocaleString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
fractionalSecondDigits: 3,
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
.replace(',', '')
|
||||||
|
},
|
||||||
|
getLogsString() {
|
||||||
|
return this.logs
|
||||||
|
.map((log) => {
|
||||||
|
const logMessage = this.maskServerAddress ? log.maskedMessage : log.message
|
||||||
|
return `${this.formatEpochToDatetimeString(log.timestamp)} [${log.level.toUpperCase()}] ${logMessage}`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
},
|
||||||
|
async shareLogs() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
|
// Share .txt file with logs
|
||||||
|
const base64Data = Buffer.from(this.getLogsString()).toString('base64')
|
||||||
|
|
||||||
|
FileSharer.share({
|
||||||
|
filename: `abs_logs_${this.$platform}_${this.$config.version}.txt`,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
base64Data
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message !== 'USER_CANCELLED') {
|
||||||
|
console.error('Failed to share', error.message)
|
||||||
|
this.$toast.error('Failed to share: ' + error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scrollToBottom() {
|
||||||
|
this.$refs.logContainer.scrollTop = this.$refs.logContainer.scrollHeight
|
||||||
|
this.hasScrolled = this.$refs.logContainer.scrollTop > 0
|
||||||
|
},
|
||||||
|
maskLogMessage(message) {
|
||||||
|
return message.replace(/(https?:\/\/)\S+/g, '$1[SERVER_ADDRESS]')
|
||||||
|
},
|
||||||
|
loadLogs() {
|
||||||
|
this.isLoading = true
|
||||||
|
AbsLogger.getAllLogs()
|
||||||
|
.then((logData) => {
|
||||||
|
const logs = logData.value || []
|
||||||
|
this.logs = logs.map((log) => {
|
||||||
|
log.maskedMessage = this.maskLogMessage(log.message)
|
||||||
|
return log
|
||||||
|
})
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.isLoading = false
|
||||||
|
console.error('Failed to load logs', error)
|
||||||
|
this.$toast.error('Failed to load logs: ' + error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
AbsLogger.addListener('onLog', (log) => {
|
||||||
|
log.maskedMessage = this.maskLogMessage(log.message)
|
||||||
|
this.logs.push(log)
|
||||||
|
this.logs.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.loadLogs()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
AbsLogger.removeAllListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { registerPlugin, WebPlugin } from '@capacitor/core'
|
import { registerPlugin, WebPlugin } from '@capacitor/core'
|
||||||
|
import { AbsLogger } from '@/plugins/capacitor'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
const { PlayerState } = require('../constants')
|
const { PlayerState } = require('../constants')
|
||||||
|
|
||||||
@@ -88,6 +89,9 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For testing onLog events in web while on the logs page
|
||||||
|
AbsLogger.info({ tag: 'AbsAudioPlayer', message: 'playPause' })
|
||||||
|
|
||||||
if (this.player.paused) this.player.play()
|
if (this.player.paused) this.player.play()
|
||||||
else this.player.pause()
|
else this.player.pause()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { registerPlugin, WebPlugin } from '@capacitor/core'
|
||||||
|
|
||||||
|
class AbsLoggerWeb extends WebPlugin {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.logs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLog(level, tag, message) {
|
||||||
|
const log = {
|
||||||
|
id: Math.random().toString(36).substring(2, 15),
|
||||||
|
tag: tag,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
level: level,
|
||||||
|
message: message
|
||||||
|
}
|
||||||
|
this.logs.push(log)
|
||||||
|
this.notifyListeners('onLog', log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMethod
|
||||||
|
async info(data) {
|
||||||
|
if (data?.message) {
|
||||||
|
this.saveLog('info', data.tag || '', data.message)
|
||||||
|
console.log('AbsLogger: info', `[${data.tag || ''}]:`, data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMethod
|
||||||
|
async error(data) {
|
||||||
|
if (data?.message) {
|
||||||
|
this.saveLog('error', data.tag || '', data.message)
|
||||||
|
console.error('AbsLogger: error', `[${data.tag || ''}]:`, data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMethod
|
||||||
|
async getAllLogs() {
|
||||||
|
return {
|
||||||
|
value: this.logs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginMethod
|
||||||
|
async clearLogs() {
|
||||||
|
this.logs = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AbsLogger = registerPlugin('AbsLogger', {
|
||||||
|
web: () => new AbsLoggerWeb()
|
||||||
|
})
|
||||||
|
|
||||||
|
export { AbsLogger }
|
||||||
@@ -2,12 +2,9 @@ import Vue from 'vue'
|
|||||||
import { AbsAudioPlayer } from './AbsAudioPlayer'
|
import { AbsAudioPlayer } from './AbsAudioPlayer'
|
||||||
import { AbsDownloader } from './AbsDownloader'
|
import { AbsDownloader } from './AbsDownloader'
|
||||||
import { AbsFileSystem } from './AbsFileSystem'
|
import { AbsFileSystem } from './AbsFileSystem'
|
||||||
|
import { AbsLogger } from './AbsLogger'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
|
||||||
Vue.prototype.$platform = Capacitor.getPlatform()
|
Vue.prototype.$platform = Capacitor.getPlatform()
|
||||||
|
|
||||||
export {
|
export { AbsAudioPlayer, AbsDownloader, AbsFileSystem, AbsLogger }
|
||||||
AbsAudioPlayer,
|
|
||||||
AbsDownloader,
|
|
||||||
AbsFileSystem
|
|
||||||
}
|
|
||||||
|
|||||||
+126
-10
@@ -20,24 +20,26 @@
|
|||||||
"ButtonDisableAutoTimer": "تعطيل المؤقت التلقائي",
|
"ButtonDisableAutoTimer": "تعطيل المؤقت التلقائي",
|
||||||
"ButtonDisconnect": "فصل",
|
"ButtonDisconnect": "فصل",
|
||||||
"ButtonGoToWebClient": "اذهب إلى عميل الويب",
|
"ButtonGoToWebClient": "اذهب إلى عميل الويب",
|
||||||
"ButtonHistory": "تاريخ",
|
"ButtonHistory": "السجل التاريخي",
|
||||||
"ButtonHome": "الرئيسية",
|
"ButtonHome": "الرئيسية",
|
||||||
"ButtonIssues": "مشاكل",
|
"ButtonIssues": "مشاكل",
|
||||||
"ButtonLatest": "أحدث",
|
"ButtonLatest": "الأحدث",
|
||||||
"ButtonLibrary": "المكتبة",
|
"ButtonLibrary": "المكتبة",
|
||||||
"ButtonLocalMedia": "ملفات الوسائط المحلية",
|
"ButtonLocalMedia": "الوسائط المحلية",
|
||||||
|
"ButtonLogs": "سجلات",
|
||||||
"ButtonManageLocalFiles": "إدارة الملفات المحلية",
|
"ButtonManageLocalFiles": "إدارة الملفات المحلية",
|
||||||
"ButtonNewFolder": "مجلد جديد",
|
"ButtonNewFolder": "مجلد جديد",
|
||||||
"ButtonNextEpisode": "الحلقة التالية",
|
"ButtonNextEpisode": "الحلقة التالية",
|
||||||
|
"ButtonOk": "موافق",
|
||||||
"ButtonOpenFeed": "فتح التغذية",
|
"ButtonOpenFeed": "فتح التغذية",
|
||||||
"ButtonOverride": "تجاوز",
|
"ButtonOverride": "تجاوز",
|
||||||
"ButtonPause": "تَوَقَّف",
|
"ButtonPause": "إيقاف مؤقت",
|
||||||
"ButtonPlay": "تشغيل",
|
"ButtonPlay": "تشغيل",
|
||||||
"ButtonPlayEpisode": "شغل الحلقة",
|
"ButtonPlayEpisode": "تشغيل الحلقة",
|
||||||
"ButtonPlaylists": "قوائم التشغيل",
|
"ButtonPlaylists": "قوائم التشغيل",
|
||||||
"ButtonRead": "اقرأ",
|
"ButtonRead": "اقرأ",
|
||||||
"ButtonReadLess": "قلص",
|
"ButtonReadLess": "اقرأ أقل",
|
||||||
"ButtonReadMore": "المزيد",
|
"ButtonReadMore": "اقرأ أكثر",
|
||||||
"ButtonRemove": "إزالة",
|
"ButtonRemove": "إزالة",
|
||||||
"ButtonRemoveFromServer": "إزالة من الخادم",
|
"ButtonRemoveFromServer": "إزالة من الخادم",
|
||||||
"ButtonSave": "حفظ",
|
"ButtonSave": "حفظ",
|
||||||
@@ -47,21 +49,135 @@
|
|||||||
"ButtonSeries": "سلسلة",
|
"ButtonSeries": "سلسلة",
|
||||||
"ButtonSetTimer": "اضبط مؤقت",
|
"ButtonSetTimer": "اضبط مؤقت",
|
||||||
"ButtonStream": "بث",
|
"ButtonStream": "بث",
|
||||||
"ButtonSubmit": "تقديم",
|
"ButtonSubmit": "إرسال",
|
||||||
"ButtonSwitchServerUser": "تبديل الخادم/المستخدم",
|
"ButtonSwitchServerUser": "تبديل الخادم/المستخدم",
|
||||||
"ButtonUserStats": "إحصائيات المستخدم",
|
"ButtonUserStats": "إحصائيات المستخدم",
|
||||||
"ButtonYes": "نعم",
|
"ButtonYes": "نعم",
|
||||||
"HeaderAccount": "الحساب",
|
"HeaderAccount": "الحساب",
|
||||||
"HeaderAdvanced": "متقدم",
|
"HeaderAdvanced": "متقدم",
|
||||||
"HeaderAudioTracks": "المسارات الصوتية",
|
"HeaderAndroidAutoSettings": "إعدادات أندرويد للسيارة",
|
||||||
|
"HeaderAudioTracks": "المقاطع الصوتية",
|
||||||
"HeaderChapters": "الفصول",
|
"HeaderChapters": "الفصول",
|
||||||
"HeaderCollection": "مجموعة",
|
"HeaderCollection": "مجموعة",
|
||||||
"HeaderCollectionItems": "عناصر المجموعة",
|
"HeaderCollectionItems": "عناصر المجموعة",
|
||||||
|
"HeaderConnectionStatus": "حالة الاتصال",
|
||||||
"HeaderDataSettings": "إعدادات البيانات",
|
"HeaderDataSettings": "إعدادات البيانات",
|
||||||
"HeaderDetails": "التفاصيل",
|
"HeaderDetails": "التفاصيل",
|
||||||
|
"HeaderDownloads": "التنزيلات",
|
||||||
"HeaderEbookFiles": "ملفات الكتب الإلكترونية",
|
"HeaderEbookFiles": "ملفات الكتب الإلكترونية",
|
||||||
"HeaderEpisodes": "الحلقات",
|
"HeaderEpisodes": "الحلقات",
|
||||||
"HeaderEreaderSettings": "إعدادات القارئ الإلكتروني",
|
"HeaderEreaderSettings": "إعدادات القارئ الإلكتروني",
|
||||||
"HeaderLatestEpisodes": "أحدث الحلقات",
|
"HeaderLatestEpisodes": "أحدث الحلقات",
|
||||||
"HeaderLibraries": "المكتبات"
|
"HeaderLibraries": "المكتبات",
|
||||||
|
"HeaderLocalFolders": "المجلدات المحلية",
|
||||||
|
"HeaderLocalLibraryItems": "عناصر المكتبة المحلية",
|
||||||
|
"HeaderNewPlaylist": "قائمة تشغيل جديدة",
|
||||||
|
"HeaderOpenRSSFeed": "فتح تغذية RSS",
|
||||||
|
"HeaderPlaybackSettings": "إعدادات التشغيل",
|
||||||
|
"HeaderPlaylist": "قائمة تشغيل",
|
||||||
|
"HeaderPlaylistItems": "عناصر قائمة التشغيل",
|
||||||
|
"HeaderProgressSyncFailed": "فشلت المزامنة",
|
||||||
|
"HeaderRSSFeed": "تغذية RSS",
|
||||||
|
"HeaderRSSFeedGeneral": "تفاصيل RSS",
|
||||||
|
"HeaderRSSFeedIsOpen": "مغذي RSS مفتوح",
|
||||||
|
"HeaderSelectDownloadLocation": "حدد موقع التنزيل",
|
||||||
|
"HeaderSettings": "إعدادات",
|
||||||
|
"HeaderSleepTimer": "مؤقت النوم",
|
||||||
|
"HeaderSleepTimerSettings": "إعدادات مؤقت النوم",
|
||||||
|
"HeaderStatsMinutesListeningChart": "الدقائق المسموعة (آخر 7 أيام)",
|
||||||
|
"HeaderStatsRecentSessions": "الجلسات الأخيرة",
|
||||||
|
"HeaderTableOfContents": "جدول المحتويات",
|
||||||
|
"HeaderUserInterfaceSettings": "إعدادات واجهة المستخدم",
|
||||||
|
"HeaderYourStats": "إحصائياتك",
|
||||||
|
"LabelAddToPlaylist": "أضف إلى قائمة التشغيل",
|
||||||
|
"LabelAddedAt": "أضيفت على",
|
||||||
|
"LabelAddedDate": "تمت الإضافة",
|
||||||
|
"LabelAll": "الكل",
|
||||||
|
"LabelAllowSeekingOnMediaControls": "السماح بالبحث عن الموضع في التحكم بإشعارات الوسائط",
|
||||||
|
"LabelAlways": "دائماً",
|
||||||
|
"LabelAndroidAutoBrowseLimitForGrouping": "حد السحب الأبجدي",
|
||||||
|
"LabelAndroidAutoBrowseLimitForGroupingHelp": "لا تستخدم السحب الأبجدي عندما يكون عدد العناصر المراد عرضها أقل من هذا العدد",
|
||||||
|
"LabelAndroidAutoBrowseSeriesSequenceOrder": "ترتيب كتب السلسلة",
|
||||||
|
"LabelAskConfirmation": "اطلب التأكيد",
|
||||||
|
"LabelAuthor": "المؤلف",
|
||||||
|
"LabelAuthorFirstLast": "المؤلف (الاسم الأول الأخير)",
|
||||||
|
"LabelAuthorLastFirst": "المؤلف (الاسم الأخير، الأول)",
|
||||||
|
"LabelAuthors": "المؤلفون",
|
||||||
|
"LabelAutoDownloadEpisodes": "تنزيل الحلقات تلقائيًا",
|
||||||
|
"LabelAutoRewindTime": "إعادة الوقت تلقائياَ",
|
||||||
|
"LabelAutoSleepTimer": "مؤقت النوم التلقائي",
|
||||||
|
"LabelAutoSleepTimerAutoRewind": "مؤقتا النوم والإرجاع التلقائيين",
|
||||||
|
"LabelAutoSleepTimerAutoRewindHelp": "عندما ينتهي مؤقت النوم التلقائي، فإن تشغيل العنصر مرة أخرى سيؤدي إلى الإرجاع التلقائي.",
|
||||||
|
"LabelAutoSleepTimerHelp": "عند تشغيل الوسائط بين أوقات البداية والنهاية المحددة، سيبدأ مؤقت النوم تلقائيًا.",
|
||||||
|
"LabelBooks": "الكتب",
|
||||||
|
"LabelChapterTrack": "مسار الفصل",
|
||||||
|
"LabelChapters": "الفصول",
|
||||||
|
"LabelClosePlayer": "إغلاق المشغل",
|
||||||
|
"LabelCollapseSeries": "إخفاء المسلسلات",
|
||||||
|
"LabelComplete": "مكتمل",
|
||||||
|
"LabelContinueBooks": "استمرار الكتب",
|
||||||
|
"LabelContinueEpisodes": "استمرار الحلقات",
|
||||||
|
"LabelContinueListening": "استمرار الاستماع",
|
||||||
|
"LabelContinueReading": "استمرار القراءة",
|
||||||
|
"LabelContinueSeries": "استمرار المسلسلات",
|
||||||
|
"LabelCustomTime": "الوقت المخصص",
|
||||||
|
"LabelDescription": "الوصف",
|
||||||
|
"LabelDisableAudioFadeOut": "تعطيل التلاشي الصوتي",
|
||||||
|
"LabelDisableAudioFadeOutHelp": "سيبدأ مستوى الصوت بالانخفاض عندما يتبقى أقل من دقيقة واحدة على مؤقت النوم. فعّل هذا الإعداد لعدم التلاشي.",
|
||||||
|
"LabelDisableAutoRewind": "تعطيل الإعادة التلقائية",
|
||||||
|
"LabelDisableShakeToReset": "تعطيل الرج لإعادة الضبط",
|
||||||
|
"LabelDisableShakeToResetHelp": "سيؤدي هزّ جهازك أثناء تشغيل المؤقت أو خلال دقيقتين من انتهاءه إلى إعادة ضبط مؤقت النوم. فعّل هذا الإعداد لتعطيل الرج لإعادة الضبط.",
|
||||||
|
"LabelDisableVibrateOnReset": "تعطيل الاهتزاز عند إعادة الضبط",
|
||||||
|
"LabelDisableVibrateOnResetHelp": "عند إعادة ضبط مؤقت النوم، سيهتز جهازك. فعّل هذا الإعداد لعدم الاهتزاز عند إعادة ضبط مؤقت النوم.",
|
||||||
|
"LabelDiscover": "استكشف",
|
||||||
|
"LabelDownload": "تنزيل",
|
||||||
|
"LabelDownloadUsingCellular": "التنزيل عبر بيانات الهاتف",
|
||||||
|
"LabelDownloaded": "تم تنزيلها",
|
||||||
|
"LabelDuration": "المدة",
|
||||||
|
"LabelEbook": "الكتاب الإلكتروني",
|
||||||
|
"LabelEbooks": "الكتب الإلكترونية",
|
||||||
|
"LabelEnable": "تمكين",
|
||||||
|
"LabelEnableMp3IndexSeeking": "تمكين البحث عن فهرس mp3",
|
||||||
|
"LabelEnableMp3IndexSeekingHelp": "يجب تفعيل هذا الإعداد فقط إذا كانت ملفات MP3 لديك لا تعمل بشكل صحيح. غالبًا ما يكون البحث غير الدقيق ناتجًا عن ملفات MP3 ذات معدل البت المتغير (VBR). سيفرض هذا الإعداد البحث عن الفهرس، حيث يتم إنشاء تعيين زمني للبايت أثناء قراءة الملف. في بعض الحالات، مع ملفات MP3 كبيرة الحجم، قد يكون هناك تأخير عند البحث قرب نهاية الملف.",
|
||||||
|
"LabelEnd": "انهاء",
|
||||||
|
"LabelEndOfChapter": "نهاية الفصل",
|
||||||
|
"LabelEndTime": "وقت النهاية",
|
||||||
|
"LabelEpisode": "الحلقة",
|
||||||
|
"LabelFeedURL": "عنوان التغذية",
|
||||||
|
"LabelFile": "الملف",
|
||||||
|
"LabelFileBirthtime": "وقت انشاء الملف",
|
||||||
|
"LabelFileModified": "تم تعديل الملف",
|
||||||
|
"LabelFilename": "اسم الملف",
|
||||||
|
"LabelFinished": "المنجزة",
|
||||||
|
"LabelFolder": "المجلد",
|
||||||
|
"LabelFontBoldness": "تعريض الخط",
|
||||||
|
"LabelFontScale": "نطاق الخط",
|
||||||
|
"LabelGenre": "التصنيف",
|
||||||
|
"LabelGenres": "التصانيف",
|
||||||
|
"LabelHapticFeedback": "ردود الفعل اللمسية",
|
||||||
|
"LabelHasEbook": "يحتوي كتاب إلكتروني",
|
||||||
|
"LabelHasSupplementaryEbook": "يحتوي كتاب إلكتروني تكميلي",
|
||||||
|
"LabelHeavy": "ثقيل",
|
||||||
|
"LabelHigh": "مرتفع",
|
||||||
|
"LabelHost": "المضيف",
|
||||||
|
"LabelInProgress": "تحت التنفيذ",
|
||||||
|
"LabelIncomplete": "غير مكتمل",
|
||||||
|
"LabelInternalAppStorage": "وحدة تخزين التطبيق الداخلي",
|
||||||
|
"LabelJumpBackwardsTime": "وقت القفز للخلف",
|
||||||
|
"LabelJumpForwardsTime": "وقت القفز للأمام",
|
||||||
|
"LabelKeepScreenAwake": "إبقاء الشاشة يقظة",
|
||||||
|
"LabelLanguage": "اللغة",
|
||||||
|
"LabelLayout": "التنسيق",
|
||||||
|
"LabelLayoutAuto": "تلقائي",
|
||||||
|
"LabelLayoutSinglePage": "صفحة واحدة",
|
||||||
|
"LabelLight": "خفيف/فاتح",
|
||||||
|
"LabelLineSpacing": "تباعد الأسطر",
|
||||||
|
"LabelListenAgain": "الاستماع مجدداً",
|
||||||
|
"LabelLocalBooks": "الكتب المحلية",
|
||||||
|
"LabelLocalPodcasts": "المدونات الصوتية المحلية",
|
||||||
|
"LabelLockOrientation": "قفل الاتجاه",
|
||||||
|
"LabelLockPlayer": "قفل المشغل",
|
||||||
|
"LabelLow": "منخفض",
|
||||||
|
"LabelMediaType": "نوع الوسائط",
|
||||||
|
"LabelMedium": "متوسط"
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -26,6 +26,7 @@
|
|||||||
"ButtonLatest": "Nejnovější",
|
"ButtonLatest": "Nejnovější",
|
||||||
"ButtonLibrary": "Knihovna",
|
"ButtonLibrary": "Knihovna",
|
||||||
"ButtonLocalMedia": "Místní média",
|
"ButtonLocalMedia": "Místní média",
|
||||||
|
"ButtonLogs": "Záznamy",
|
||||||
"ButtonManageLocalFiles": "Spravovat místní soubory",
|
"ButtonManageLocalFiles": "Spravovat místní soubory",
|
||||||
"ButtonNewFolder": "Nová složka",
|
"ButtonNewFolder": "Nová složka",
|
||||||
"ButtonNextEpisode": "Další epizoda",
|
"ButtonNextEpisode": "Další epizoda",
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
"HeaderEbookFiles": "Soubory e-knih",
|
"HeaderEbookFiles": "Soubory e-knih",
|
||||||
"HeaderEpisodes": "Epizody",
|
"HeaderEpisodes": "Epizody",
|
||||||
"HeaderEreaderSettings": "Nastavení čtečky e-knih",
|
"HeaderEreaderSettings": "Nastavení čtečky e-knih",
|
||||||
"HeaderLatestEpisodes": "Nejnovější epizody",
|
"HeaderLatestEpisodes": "Nové epizody",
|
||||||
"HeaderLibraries": "Knihovny",
|
"HeaderLibraries": "Knihovny",
|
||||||
"HeaderLocalFolders": "Místní složky",
|
"HeaderLocalFolders": "Místní složky",
|
||||||
"HeaderLocalLibraryItems": "Místní položky knihovny",
|
"HeaderLocalLibraryItems": "Místní položky knihovny",
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
"HeaderPlaybackSettings": "Nastavení přehrávání",
|
"HeaderPlaybackSettings": "Nastavení přehrávání",
|
||||||
"HeaderPlaylist": "Seznam skladeb",
|
"HeaderPlaylist": "Seznam skladeb",
|
||||||
"HeaderPlaylistItems": "Položky seznamu přehrávání",
|
"HeaderPlaylistItems": "Položky seznamu přehrávání",
|
||||||
"HeaderProgressSyncFailed": "Chyba při synchronizaci",
|
"HeaderProgressSyncFailed": "Chyba při synchronizaci pokroku",
|
||||||
"HeaderRSSFeed": "RSS kanál",
|
"HeaderRSSFeed": "RSS kanál",
|
||||||
"HeaderRSSFeedGeneral": "Detaily RSS",
|
"HeaderRSSFeedGeneral": "Detaily RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Kanál RSS je otevřen",
|
"HeaderRSSFeedIsOpen": "Kanál RSS je otevřen",
|
||||||
@@ -94,6 +95,9 @@
|
|||||||
"LabelAll": "Vše",
|
"LabelAll": "Vše",
|
||||||
"LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií",
|
"LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií",
|
||||||
"LabelAlways": "Vždy",
|
"LabelAlways": "Vždy",
|
||||||
|
"LabelAndroidAutoBrowseLimitForGrouping": "Omezení abecedního rozbalovacího seznamu",
|
||||||
|
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nepoužívejte abecední řazení, pokud je k zobrazení méně než tento počet položek",
|
||||||
|
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Řazení sérií knih",
|
||||||
"LabelAskConfirmation": "Požádat o potvrzení",
|
"LabelAskConfirmation": "Požádat o potvrzení",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
|
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
|
||||||
@@ -134,7 +138,7 @@
|
|||||||
"LabelEbooks": "E-knihy",
|
"LabelEbooks": "E-knihy",
|
||||||
"LabelEnable": "Povolit",
|
"LabelEnable": "Povolit",
|
||||||
"LabelEnableMp3IndexSeeking": "Povolit indexové vyhledávání mp3",
|
"LabelEnableMp3IndexSeeking": "Povolit indexové vyhledávání mp3",
|
||||||
"LabelEnableMp3IndexSeekingHelp": "Toto nastavení by mělo být povoleno pouze v případě, že soubory MP3 nejsou správně vyhledávány. Nepřesné vyhledávání je s největší pravděpodobností způsobeno soubory MP3 s proměnlivým datovým tokem (VBR). Toto nastavení vynutí indexové vyhledávání, při kterém se při čtení souboru vytváří mapování času na bajty. V některých případech u velkých souborů MP3 dochází ke zpoždění při vyhledávání ke konci souboru.",
|
"LabelEnableMp3IndexSeekingHelp": "Toto nastavení by mělo být povoleno pouze v případě, že v souborech MP3 nelze přeskakovat. Nepřesné přeskakování je s největší pravděpodobností způsobeno soubory MP3 s proměnlivým datovým tokem (VBR). Toto nastavení vynutí indexové vyhledávání, při kterém se při čtení souboru vytváří mapování času na bajty. V některých případech u velkých souborů MP3 dochází ke zpoždění při vyhledávání ke konci souboru.",
|
||||||
"LabelEnd": "Konec",
|
"LabelEnd": "Konec",
|
||||||
"LabelEndOfChapter": "Konec kapitoly",
|
"LabelEndOfChapter": "Konec kapitoly",
|
||||||
"LabelEndTime": "Do",
|
"LabelEndTime": "Do",
|
||||||
@@ -161,6 +165,7 @@
|
|||||||
"LabelInternalAppStorage": "Interní úložiště aplikace",
|
"LabelInternalAppStorage": "Interní úložiště aplikace",
|
||||||
"LabelJumpBackwardsTime": "Délka skoku zpět v čase",
|
"LabelJumpBackwardsTime": "Délka skoku zpět v čase",
|
||||||
"LabelJumpForwardsTime": "Délka skoku vpřed v čase",
|
"LabelJumpForwardsTime": "Délka skoku vpřed v čase",
|
||||||
|
"LabelKeepScreenAwake": "Nezhasínat obrazovku",
|
||||||
"LabelLanguage": "Jazyk",
|
"LabelLanguage": "Jazyk",
|
||||||
"LabelLayout": "Rozvržení",
|
"LabelLayout": "Rozvržení",
|
||||||
"LabelLayoutAuto": "Automatické",
|
"LabelLayoutAuto": "Automatické",
|
||||||
@@ -175,6 +180,7 @@
|
|||||||
"LabelLow": "Nízké",
|
"LabelLow": "Nízké",
|
||||||
"LabelMediaType": "Typ média",
|
"LabelMediaType": "Typ média",
|
||||||
"LabelMedium": "Střední",
|
"LabelMedium": "Střední",
|
||||||
|
"LabelMissing": "Chybějící",
|
||||||
"LabelMore": "Více",
|
"LabelMore": "Více",
|
||||||
"LabelMoreInfo": "Více informací",
|
"LabelMoreInfo": "Více informací",
|
||||||
"LabelName": "Jméno",
|
"LabelName": "Jméno",
|
||||||
@@ -192,6 +198,8 @@
|
|||||||
"LabelNotFinished": "Nedokončeno",
|
"LabelNotFinished": "Nedokončeno",
|
||||||
"LabelNotStarted": "Nezahájeno",
|
"LabelNotStarted": "Nezahájeno",
|
||||||
"LabelNumEpisodes": "{0} epizod",
|
"LabelNumEpisodes": "{0} epizod",
|
||||||
|
"LabelNumEpisodesIncomplete": "{0} epizod, {1} nekompletní",
|
||||||
|
"LabelNumberOfEpisodes": "Počet epizod",
|
||||||
"LabelOff": "Vypnout",
|
"LabelOff": "Vypnout",
|
||||||
"LabelOn": "Zapnuto",
|
"LabelOn": "Zapnuto",
|
||||||
"LabelPassword": "Heslo",
|
"LabelPassword": "Heslo",
|
||||||
@@ -220,6 +228,8 @@
|
|||||||
"LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti",
|
"LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti",
|
||||||
"LabelSeason": "Sezóna",
|
"LabelSeason": "Sezóna",
|
||||||
"LabelSelectADevice": "Vyberte zařízení",
|
"LabelSelectADevice": "Vyberte zařízení",
|
||||||
|
"LabelSequenceAscending": "Řadit vzestupně",
|
||||||
|
"LabelSequenceDescending": "Řadit sestupně",
|
||||||
"LabelSeries": "Série",
|
"LabelSeries": "Série",
|
||||||
"LabelServerAddress": "Adresa serveru",
|
"LabelServerAddress": "Adresa serveru",
|
||||||
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
||||||
@@ -243,6 +253,7 @@
|
|||||||
"LabelTag": "Štítek",
|
"LabelTag": "Štítek",
|
||||||
"LabelTags": "Štítky",
|
"LabelTags": "Štítky",
|
||||||
"LabelTheme": "Téma",
|
"LabelTheme": "Téma",
|
||||||
|
"LabelThemeBlack": "Černé",
|
||||||
"LabelThemeDark": "Tmavé",
|
"LabelThemeDark": "Tmavé",
|
||||||
"LabelThemeLight": "Světlé",
|
"LabelThemeLight": "Světlé",
|
||||||
"LabelTimeRemaining": "{0} zbývá",
|
"LabelTimeRemaining": "{0} zbývá",
|
||||||
@@ -269,6 +280,7 @@
|
|||||||
"MessageBookshelfEmpty": "Knihovna je prázdná",
|
"MessageBookshelfEmpty": "Knihovna je prázdná",
|
||||||
"MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.",
|
"MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.",
|
||||||
"MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.",
|
"MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.",
|
||||||
|
"MessageConfirmDisableAutoTimer": "Určitě chcete vypnout automatický časovač pro zbytek dnešního dne? Časovač bude opět aktivován po uběhnutí doby automatického spánku nebo po restartování aplikace.",
|
||||||
"MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?",
|
"MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?",
|
||||||
"MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
|
"MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
|
||||||
"MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?",
|
"MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?",
|
||||||
@@ -283,8 +295,10 @@
|
|||||||
"MessageFetching": "Načítání...",
|
"MessageFetching": "Načítání...",
|
||||||
"MessageFollowTheProjectOnGithub": "Sledujte projekt na GitHubu",
|
"MessageFollowTheProjectOnGithub": "Sledujte projekt na GitHubu",
|
||||||
"MessageItemDownloadCompleteFailedToCreate": "Stahování položky bylo dokončeno, ale nepodařilo se vytvořit položku v knihovně",
|
"MessageItemDownloadCompleteFailedToCreate": "Stahování položky bylo dokončeno, ale nepodařilo se vytvořit položku v knihovně",
|
||||||
|
"MessageItemMissing": "Položka chybí a musí být opravena na serveru. Typicky je položka označena jako chybějící, protože cesty k souborům nejsou přístupné.",
|
||||||
"MessageLoading": "Načítá se...",
|
"MessageLoading": "Načítá se...",
|
||||||
"MessageLoadingServerData": "Načítání dat ze serveru...",
|
"MessageLoadingServerData": "Načítání dat ze serveru...",
|
||||||
|
"MessageLocalFolderDescription": "„Interní úložiště aplikace“ je přístupné pouze této aplikaci. Tato aplikace podporuje pouze média stažená přímo prostřednictvím aplikace. Složky sdíleného úložiště lze použít k tomu, aby ostatní aplikace měly přístup k médiím staženým touto aplikací.",
|
||||||
"MessageMarkAsFinished": "Označit jako dokončené",
|
"MessageMarkAsFinished": "Označit jako dokončené",
|
||||||
"MessageMediaLinkedToADifferentServer": "Média jsou propojena se serverem Audiobookshelf na jiné adrese ({0}). Pokrok bude synchronizován, když budete připojeni k této adrese serveru.",
|
"MessageMediaLinkedToADifferentServer": "Média jsou propojena se serverem Audiobookshelf na jiné adrese ({0}). Pokrok bude synchronizován, když budete připojeni k této adrese serveru.",
|
||||||
"MessageMediaLinkedToADifferentUser": "Média jsou propojena se serverem, ale byla stažena jiným uživatelem. Pokrok bude synchronizován pouze s uživatelem, který je stáhl.",
|
"MessageMediaLinkedToADifferentUser": "Média jsou propojena se serverem, ale byla stažena jiným uživatelem. Pokrok bude synchronizován pouze s uživatelem, který je stáhl.",
|
||||||
@@ -303,7 +317,10 @@
|
|||||||
"MessageNoSeries": "Žádné série",
|
"MessageNoSeries": "Žádné série",
|
||||||
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
|
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
|
||||||
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
|
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
|
||||||
|
"MessageOldServerConnectionWarning": "Konfigurace připojení k serveru používá staré ID uživatele. Odstraňte a znovu přidejte toto připojení k serveru.",
|
||||||
|
"MessageOldServerConnectionWarningHelp": "Připojení k tomuto serveru jste původně nastavili před migrací databáze ve verzi 2.3.0 vydané v červnu 2023. Budoucí aktualizace serveru odstraní možnost přihlášení pomocí tohoto starého připojení. Odstraňte prosím stávající připojení k serveru a připojte se znovu (pomocí stejné adresy serveru a přihlašovacích údajů). Pokud máte v tomto zařízení stažená média, bude nutné je pro synchronizaci se serverem stáhnout znovu.",
|
||||||
"MessagePodcastSearchField": "Zadejte hledaný pojem pro RSS feed URL",
|
"MessagePodcastSearchField": "Zadejte hledaný pojem pro RSS feed URL",
|
||||||
|
"MessageProgressSyncFailed": "Poslední pokus o nahlášení průběhu poslechu na server se nezdařil. Během přehrávání médií se budou požadavky na synchronizaci pokroku nadále pokoušet každých 15 až 60 sekund.",
|
||||||
"MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
|
"MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
|
||||||
"MessageSeriesAlreadyDownloaded": "Všechny knihy v této sérii jste již stáhli.",
|
"MessageSeriesAlreadyDownloaded": "Všechny knihy v této sérii jste již stáhli.",
|
||||||
"MessageSeriesDownloadConfirm": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2} do složky {3}?",
|
"MessageSeriesDownloadConfirm": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2} do složky {3}?",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"ButtonLatest": "Neueste",
|
"ButtonLatest": "Neueste",
|
||||||
"ButtonLibrary": "Bibliothek",
|
"ButtonLibrary": "Bibliothek",
|
||||||
"ButtonLocalMedia": "Lokale Medien",
|
"ButtonLocalMedia": "Lokale Medien",
|
||||||
|
"ButtonLogs": "Protokolle",
|
||||||
"ButtonManageLocalFiles": "Verwalte lokale Dateien",
|
"ButtonManageLocalFiles": "Verwalte lokale Dateien",
|
||||||
"ButtonNewFolder": "Neuer Ordner",
|
"ButtonNewFolder": "Neuer Ordner",
|
||||||
"ButtonNextEpisode": "Nächste Episode",
|
"ButtonNextEpisode": "Nächste Episode",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"ButtonCancel": "Cancel",
|
"ButtonCancel": "Cancel",
|
||||||
"ButtonCancelTimer": "Cancel Timer",
|
"ButtonCancelTimer": "Cancel Timer",
|
||||||
"ButtonClearFilter": "Clear Filter",
|
"ButtonClearFilter": "Clear Filter",
|
||||||
|
"ButtonClearLogs": "Clear Logs",
|
||||||
"ButtonCloseFeed": "Close Feed",
|
"ButtonCloseFeed": "Close Feed",
|
||||||
"ButtonCollections": "Collections",
|
"ButtonCollections": "Collections",
|
||||||
"ButtonConnect": "Connect",
|
"ButtonConnect": "Connect",
|
||||||
@@ -26,7 +27,9 @@
|
|||||||
"ButtonLatest": "Latest",
|
"ButtonLatest": "Latest",
|
||||||
"ButtonLibrary": "Library",
|
"ButtonLibrary": "Library",
|
||||||
"ButtonLocalMedia": "Local Media",
|
"ButtonLocalMedia": "Local Media",
|
||||||
|
"ButtonLogs": "Logs",
|
||||||
"ButtonManageLocalFiles": "Manage Local Files",
|
"ButtonManageLocalFiles": "Manage Local Files",
|
||||||
|
"ButtonMaskServerAddress": "Mask server address",
|
||||||
"ButtonNewFolder": "New Folder",
|
"ButtonNewFolder": "New Folder",
|
||||||
"ButtonNextEpisode": "Next Episode",
|
"ButtonNextEpisode": "Next Episode",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
"ButtonStream": "Stream",
|
"ButtonStream": "Stream",
|
||||||
"ButtonSubmit": "Submit",
|
"ButtonSubmit": "Submit",
|
||||||
"ButtonSwitchServerUser": "Switch Server/User",
|
"ButtonSwitchServerUser": "Switch Server/User",
|
||||||
|
"ButtonUnmaskServerAddress": "Unmask server address",
|
||||||
"ButtonUserStats": "User Stats",
|
"ButtonUserStats": "User Stats",
|
||||||
"ButtonYes": "Yes",
|
"ButtonYes": "Yes",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -310,6 +314,7 @@
|
|||||||
"MessageNoItems": "No Items",
|
"MessageNoItems": "No Items",
|
||||||
"MessageNoItemsFound": "No items found",
|
"MessageNoItemsFound": "No items found",
|
||||||
"MessageNoListeningSessions": "No Listening Sessions",
|
"MessageNoListeningSessions": "No Listening Sessions",
|
||||||
|
"MessageNoLogs": "No logs",
|
||||||
"MessageNoMediaFolders": "No Media Folders",
|
"MessageNoMediaFolders": "No Media Folders",
|
||||||
"MessageNoNetworkConnection": "No network connection",
|
"MessageNoNetworkConnection": "No network connection",
|
||||||
"MessageNoPodcastsFound": "No podcasts found",
|
"MessageNoPodcastsFound": "No podcasts found",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"ButtonLatest": "Останні",
|
"ButtonLatest": "Останні",
|
||||||
"ButtonLibrary": "Бібліотека",
|
"ButtonLibrary": "Бібліотека",
|
||||||
"ButtonLocalMedia": "Локальні файли",
|
"ButtonLocalMedia": "Локальні файли",
|
||||||
|
"ButtonLogs": "Журнали",
|
||||||
"ButtonManageLocalFiles": "Керування локальними файлами",
|
"ButtonManageLocalFiles": "Керування локальними файлами",
|
||||||
"ButtonNewFolder": "Нова тека",
|
"ButtonNewFolder": "Нова тека",
|
||||||
"ButtonNextEpisode": "Наступний епізод",
|
"ButtonNextEpisode": "Наступний епізод",
|
||||||
|
|||||||
Reference in New Issue
Block a user