diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6c3a0b9c..b2373ca4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,6 +12,16 @@ body: - type: markdown attributes: value: 'Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug.' + - type: dropdown + id: confirm-check + attributes: + label: I have verified that the [bug is not already awaiting release](https://github.com/advplyr/audiobookshelf-app/issues?q=is%3Aissue%20label%3A%22awaiting%20release%22) + multiple: false + options: + - 'Yes' + - 'No' + validations: + required: true - type: textarea id: what-happened attributes: @@ -44,7 +54,7 @@ body: attributes: label: Phone Model description: What kind of phone are you using? - placeholder: e.g. Pixel 6, iPhone 14, Samusung Galaxy s23, etc + placeholder: e.g. Pixel 6, iPhone 14, Samsung Galaxy s23, etc validations: required: true - type: input @@ -62,10 +72,10 @@ body: description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* multiple: true options: - - Android App - 0.9.79 - - iOS App - 0.9.79 - - Android App - 0.9.78 - - iOS App - 0.9.78 + - 'Android App - 0.9.79' + - 'iOS App - 0.9.79' + - 'Android App - 0.9.78' + - 'iOS App - 0.9.78' validations: required: true - type: dropdown @@ -74,10 +84,10 @@ body: label: Installation Source multiple: true options: - - Google Play Store - - Testflight - - SideStore - - Other (List in "Additional Notes") + - 'Google Play Store' + - 'Testflight' + - 'SideStore' + - 'Other (List in "Additional Notes")' validations: required: true - type: textarea diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index db7b50e2..a718c407 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: 17 + java-version: 21 - name: install dependencies run: npm ci diff --git a/.github/workflows/deploy-apk.yml b/.github/workflows/deploy-apk.yml index 861c8e40..9d51af81 100644 --- a/.github/workflows/deploy-apk.yml +++ b/.github/workflows/deploy-apk.yml @@ -24,7 +24,7 @@ jobs: uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: 17 + java-version: 21 - name: install dependencies run: npm ci diff --git a/android/app/build.gradle b/android/app/build.gradle index 265e71ce..d8e19d60 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -36,8 +36,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 110 - versionName "0.9.79-beta" + versionCode 111 + versionName "0.9.80-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders = [ "appAuthRedirectScheme": "com.audiobookshelf.app" @@ -86,7 +86,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation project(':capacitor-android') - implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation project(':capacitor-cordova-android-plugins') diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 5b1074a7..47375cfd 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,7 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { - implementation project(':byteowls-capacitor-filesharer') + implementation project(':webnativellc-capacitor-filesharer') implementation project(':capacitor-community-keep-awake') implementation project(':capacitor-community-volume-buttons') implementation project(':capacitor-app') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3af65bd7..1d497a60 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -51,7 +51,7 @@ { + val logs:MutableList = mutableListOf() + Paper.book("log").allKeys.forEach { logId -> + Paper.book("log").read(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") + } + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt index c7b7aaa4..eb2e51ac 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt @@ -1,15 +1,20 @@ package com.audiobookshelf.app.managers import android.content.Context +import android.media.MediaPlayer import android.os.* import android.util.Log +import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.SLEEP_TIMER_WAKE_UP_EXPIRATION +import com.audiobookshelf.app.plugins.AbsLogger import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt +const val SLEEP_TIMER_CHIME_SOUND_VOLUME = 0.7f + class SleepTimerManager constructor(private val playerNotificationService: PlayerNotificationService) { private val tag = "SleepTimerManager" @@ -156,6 +161,10 @@ constructor(private val playerNotificationService: PlayerNotificationService) { ) } + if (sleepTimeSecondsRemaining == 30 && sleepTimerElapsed > 1 && DeviceManager.deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime == true) { + playChimeSound() + } + if (sleepTimeSecondsRemaining <= 0) { Log.d(tag, "Sleep Timer Pausing Player on Chapter") pause() @@ -263,6 +272,18 @@ constructor(private val playerNotificationService: PlayerNotificationService) { } } + /** Plays chime sound */ + private fun playChimeSound() { + AbsLogger.info(tag, "playChimeSound: Playing sleep timer chime sound") + val ctx = playerNotificationService.getContext() + val mediaPlayer = MediaPlayer.create(ctx, R.raw.bell) + mediaPlayer.setVolume(SLEEP_TIMER_CHIME_SOUND_VOLUME, SLEEP_TIMER_CHIME_SOUND_VOLUME) + mediaPlayer.start() + mediaPlayer.setOnCompletionListener { + mediaPlayer.release() + } + } + /** * Gets the chapter end time for use in End of Chapter timers. If less than 10 seconds remain in * the chapter, then use the next chapter. diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index 59f22840..39b3c25f 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -136,7 +136,6 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) { val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig() if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { - Log.d(tag, "Reset caches") podcastEpisodeLibraryItemMap = mutableMapOf() serverLibraries = listOf() serverLibraryItems = mutableListOf() diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt index ae110498..1aeed0a0 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt @@ -8,6 +8,7 @@ import com.audiobookshelf.app.data.MediaProgress import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.player.PlayerNotificationService +import com.audiobookshelf.app.plugins.AbsLogger import com.audiobookshelf.app.server.ApiHandler import java.util.* import kotlin.concurrent.schedule @@ -208,6 +209,7 @@ class MediaProgressSyncer( MediaEventManager.seekEvent(currentPlaybackSession!!, null) } + // Currently unused fun syncFromServerProgress(mediaProgress: MediaProgress) { currentPlaybackSession?.let { it.updatedAt = mediaProgress.lastUpdate @@ -260,44 +262,46 @@ class MediaProgressSyncer( tag, "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 // Send sync to server also if connected to this server and local item belongs to this // server + val isConnectedToSameServer = it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && - it.serverConnectionConfigId != null && - DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId + isConnectedToSameServer ) { apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg -> if (syncSuccess) { failedSyncs = 0 playerNotificationService.alertSyncSuccess() 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 { failedSyncs++ if (failedSyncs == 2) { playerNotificationService.alertSyncFailing() // Show alert in client failedSyncs = 0 } - Log.e( - tag, - "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}" - ) + AbsLogger.error("MediaProgressSyncer", "sync: Local progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (${DeviceManager.serverConnectionConfigName})") } cb(SyncResult(true, syncSuccess, errorMsg)) } } 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)) } } } 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 -> 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 playerNotificationService.alertSyncSuccess() lastSyncTime = System.currentTimeMillis() @@ -308,14 +312,12 @@ class MediaProgressSyncer( playerNotificationService.alertSyncFailing() // Show alert in client failedSyncs = 0 } - Log.e( - tag, - "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}" - ) + AbsLogger.error("MediaProgressSyncer", "sync: Progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName})") } cb(SyncResult(true, syncSuccess, errorMsg)) } } 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)) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt b/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt index 35a14cf2..eaf580bd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/AbMediaDescriptionAdapter.kt @@ -7,7 +7,6 @@ import android.net.Uri import android.os.Build import android.provider.MediaStore import android.support.v4.media.session.MediaControllerCompat -import android.util.Log import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.R import com.bumptech.glide.Glide @@ -41,7 +40,6 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl // Cache the bitmap for the current audiobook so that successive calls to // `getCurrentLargeIcon` don't cause the bitmap to be recreated. currentIconUri = albumArtUri - Log.d(tag, "ART $currentIconUri") if (currentIconUri.toString().startsWith("content://")) { currentBitmap = if (Build.VERSION.SDK_INT < 28) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index fb83402c..a7aa7fa8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -8,7 +8,6 @@ import android.util.Log import android.view.KeyEvent import com.audiobookshelf.app.data.LibraryItemWrapper import com.audiobookshelf.app.data.PodcastEpisode -import com.audiobookshelf.app.device.DeviceManager import java.util.* import kotlin.concurrent.schedule diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index e9e792b7..cf61c145 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -4,14 +4,11 @@ import android.annotation.SuppressLint import android.app.* import android.content.Context import android.content.Intent -import android.graphics.Bitmap import android.graphics.Color -import android.graphics.ImageDecoder import android.hardware.Sensor import android.hardware.SensorManager import android.net.* import android.os.* -import android.provider.MediaStore import android.provider.Settings import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat @@ -36,6 +33,7 @@ import com.audiobookshelf.app.media.MediaManager import com.audiobookshelf.app.media.MediaProgressSyncer import com.audiobookshelf.app.media.getUriToAbsIconDrawable import com.audiobookshelf.app.media.getUriToDrawable +import com.audiobookshelf.app.plugins.AbsLogger import com.audiobookshelf.app.server.ApiHandler import com.google.android.exoplayer2.* import com.google.android.exoplayer2.audio.AudioAttributes @@ -310,19 +308,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val coverUri = currentPlaybackSession!!.getCoverUri(ctx) - var bitmap: Bitmap? = null - // Local covers get bitmap - if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) { - bitmap = - if (Build.VERSION.SDK_INT < 28) { - MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) - } else { - val source: ImageDecoder.Source = - ImageDecoder.createSource(ctx.contentResolver, coverUri) - ImageDecoder.decodeBitmap(source) - } - } - // Fix for local images crashing on Android 11 for specific devices // https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958 try { @@ -346,8 +331,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { .setExtras(extra) .setTitle(currentPlaybackSession!!.displayTitle) - bitmap?.let { mediaDescriptionBuilder.setIconBitmap(it) } - ?: mediaDescriptionBuilder.setIconUri(coverUri) + mediaDescriptionBuilder.setIconUri(coverUri) return mediaDescriptionBuilder.build() } @@ -452,7 +436,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { playbackSession ) // 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 clientEventEmitter?.onPlaybackSession(playbackSession) @@ -469,7 +453,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val mediaSource: MediaSource if (playbackSession.isLocal) { - Log.d(tag, "Playing Local Item") + AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing local item ${currentPlaybackSession?.mediaItemId}.") val dataSourceFactory = DefaultDataSource.Factory(ctx) val extractorsFactory = DefaultExtractorsFactory() @@ -483,7 +467,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) .createMediaSource(mediaItems[0]) } else if (!playbackSession.isHLS) { - Log.d(tag, "Direct Playing Item") + AbsLogger.info("PlayerNotificationService", "preparePlayer: Direct playing item ${currentPlaybackSession?.mediaItemId}.") val dataSourceFactory = DefaultHttpDataSource.Factory() val extractorsFactory = DefaultExtractorsFactory() @@ -498,7 +482,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) .createMediaSource(mediaItems[0]) } else { - Log.d(tag, "Playing HLS Item") + AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing HLS stream of item ${currentPlaybackSession?.mediaItemId}.") val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setUserAgent(channelId) dataSourceFactory.setDefaultRequestProperties( @@ -1105,11 +1089,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // No further calls will be made to other media browsing methods. null } else { - Log.d(tag, "Android Auto starting $clientPackageName $clientUid") + AbsLogger.info(tag, "onGetRoot: clientPackageName: $clientPackageName, clientUid: $clientUid") isStarted = true // Reset cache if no longer connected to server or server changed if (mediaManager.checkResetServerItems()) { + AbsLogger.info(tag, "onGetRoot: Reset Android Auto server items cache (${DeviceManager.serverConnectionConfigString})") forceReloadingAndroidAuto = true } @@ -1134,7 +1119,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { parentMediaId: String, result: Result> ) { - Log.d(tag, "ON LOAD CHILDREN $parentMediaId") + AbsLogger.info(tag, "onLoadChildren: parentMediaId: $parentMediaId (${DeviceManager.serverConnectionConfigString})") result.detach() @@ -1145,7 +1130,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads - val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") val localBrowseItems: MutableList = mutableListOf() @@ -1242,8 +1226,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.d(tag, "Trying to initialize browseTree.") if (!this::browseTree.isInitialized || forceReloadingAndroidAuto) { forceReloadingAndroidAuto = false + AbsLogger.info(tag, "onLoadChildren: Loading Android Auto items") mediaManager.loadAndroidAutoItems { - Log.d(tag, "android auto loaded. Starting browseTree initialize") + AbsLogger.info(tag, "onLoadChildren: Loaded Android Auto data, initializing browseTree") + browseTree = BrowseTree( this, @@ -1259,15 +1245,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { MediaBrowserCompat.MediaItem.FLAG_BROWSABLE ) } - Log.d(tag, "browseTree initialize and android auto loaded") + result.sendResult(children as MutableList?) firstLoadDone = true if (mediaManager.serverLibraries.isNotEmpty()) { - Log.d(tag, "Starting personalization fetch") - mediaManager.populatePersonalizedDataForAllLibraries { notifyChildrenChanged("/") } + AbsLogger.info(tag, "onLoadChildren: Android Auto fetching personalized data for all libraries") + mediaManager.populatePersonalizedDataForAllLibraries { + AbsLogger.info(tag, "onLoadChildren: Android Auto loaded personalized data for all libraries") + notifyChildrenChanged("/") + } - Log.d(tag, "Initialize inprogress items") - mediaManager.initializeInProgressItems { notifyChildrenChanged("/") } + AbsLogger.info(tag, "onLoadChildren: Android Auto fetching in progress items") + mediaManager.initializeInProgressItems { + AbsLogger.info(tag, "onLoadChildren: Android Auto loaded in progress items") + notifyChildrenChanged("/") + } } } } else { @@ -1287,7 +1279,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { MediaBrowserCompat.MediaItem.FLAG_BROWSABLE ) } - Log.d(tag, "browseTree initialize and android auto loaded") + + AbsLogger.info(tag, "onLoadChildren: Android auto data loaded") result.sendResult(children as MutableList?) } } else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/ShakeDetector.kt b/android/app/src/main/java/com/audiobookshelf/app/player/ShakeDetector.kt index 331543db..d25206d1 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/ShakeDetector.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/ShakeDetector.kt @@ -5,6 +5,7 @@ import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.plugins.AbsLogger import kotlin.math.sqrt class ShakeDetector : SensorEventListener { @@ -46,6 +47,7 @@ class ShakeDetector : SensorEventListener { if (mShakeTimestamp + SHAKE_COUNT_RESET_TIME_MS < now) { mShakeCount = 0 } + AbsLogger.info("ShakeDetector", "Device shake above threshold ($gForce > $shakeThreshold)") mShakeTimestamp = now mShakeCount++ mListener!!.onShake(mShakeCount) diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index e51e2ee6..79a460d1 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -180,7 +180,8 @@ class AbsAudioPlayer : Plugin() { val playWhenReady = call.getBoolean("playWhenReady") == true val playbackRate = call.getFloat("playbackRate",1f) ?: 1f 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()) { Log.e(tag, "Invalid call to play library item no library item id") diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 304cbdd6..bcf0e34d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -36,6 +36,7 @@ class AbsDatabase : Plugin() { DeviceManager.dbManager.cleanLocalMediaProgress() DeviceManager.dbManager.cleanLocalLibraryItems() + DeviceManager.dbManager.cleanLogs() } @PluginMethod @@ -220,12 +221,11 @@ class AbsDatabase : Plugin() { @PluginMethod fun syncLocalSessionsWithServer(call:PluginCall) { if (DeviceManager.serverConnectionConfig == null) { - Log.e(tag, "syncLocalSessionsWithServer not connected to server") + AbsLogger.error("AbsDatabase", "syncLocalSessionsWithServer: not connected to server") return call.resolve() } apiHandler.syncLocalMediaProgressForUser { - Log.d(tag, "Finished syncing local media progress for user") val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId } if (savedSessions.isNotEmpty()) { @@ -233,6 +233,7 @@ class AbsDatabase : Plugin() { if (!success) { call.resolve(JSObject("{\"error\":\"$errorMsg\"}")) } else { + AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: Finished sending local playback sessions to server. Removing ${savedSessions.size} saved sessions.") // Remove all local sessions savedSessions.forEach { DeviceManager.dbManager.removePlaybackSession(it.id) @@ -241,6 +242,7 @@ class AbsDatabase : Plugin() { } } } else { + AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: No saved local playback sessions to send to server.") call.resolve() } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsLogger.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsLogger.kt new file mode 100644 index 00000000..ababfb59 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsLogger.kt @@ -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) + +@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))) + } + info("AbsLogger", "load: AbsLogger plugin initialized") + } + + companion object { + var onLogEmitter:((log:AbsLog) -> Unit)? = null + + 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?.let { it(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() + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index dfd97064..bf807f73 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -13,6 +13,7 @@ import com.audiobookshelf.app.media.MediaProgressSyncData import com.audiobookshelf.app.media.SyncResult import com.audiobookshelf.app.models.User import com.audiobookshelf.app.BuildConfig +import com.audiobookshelf.app.plugins.AbsLogger import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.core.json.JsonReadFeature 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 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) { 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")) } else { val response = jacksonMapper.readValue(it.toString()) response.results.forEach { localSessionSyncResult -> Log.d(tag, "Synced session result ${localSessionSyncResult.id}|${localSessionSyncResult.progressSynced}|${localSessionSyncResult.success}") + playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session -> if (localSessionSyncResult.progressSynced == true) { val syncResult = SyncResult(true, true, "Progress synced on server") 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) { - 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) { + AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: Server connection ${DeviceManager.serverConnectionConfigName}") + // Get all local media progress for this server val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId } 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() } - getCurrentUser { _user -> - _user?.let { user-> + AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Found ${allLocalMediaProgress.size} local media progress") + + 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 user.mediaProgress.forEach { mediaProgress -> // Get matching local media progress allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress -> 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() + 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) - 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) + numLocalMediaProgressUpdated++ } else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) { // 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 updatePayload = JSObject() updatePayload.put("ebookLocation", localMediaProgress.ebookLocation) updatePayload.put("ebookProgress", localMediaProgress.ebookProgress) updatePayload.put("lastUpdate", localMediaProgress.lastUpdate) 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() } diff --git a/android/app/src/main/res/raw/bell.mp3 b/android/app/src/main/res/raw/bell.mp3 new file mode 100644 index 00000000..c14bd84f Binary files /dev/null and b/android/app/src/main/res/raw/bell.mp3 differ diff --git a/android/build.gradle b/android/build.gradle index ee66229c..b315e439 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { classpath 'com.google.gms:google-services:4.4.2' - classpath 'com.android.tools.build:gradle:8.8.0' + classpath 'com.android.tools.build:gradle:8.8.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index ce3d71fa..2c9a795a 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,8 +2,8 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') -include ':byteowls-capacitor-filesharer' -project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android') +include ':webnativellc-capacitor-filesharer' +project(':webnativellc-capacitor-filesharer').projectDir = new File('../node_modules/@webnativellc/capacitor-filesharer/android') include ':capacitor-community-keep-awake' project(':capacitor-community-keep-awake').projectDir = new File('../node_modules/@capacitor-community/keep-awake/android') diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 36074adc..bc60d1fc 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/variables.gradle b/android/variables.gradle index d459d20b..0c396aa6 100644 --- a/android/variables.gradle +++ b/android/variables.gradle @@ -1,24 +1,24 @@ ext { minSdkVersion = 24 compileSdkVersion = 35 - targetSdkVersion = 34 - androidxActivityVersion = '1.8.0' + targetSdkVersion = 35 + androidxActivityVersion = '1.9.2' androidxAppCompatVersion = '1.7.0' - androidxCoordinatorLayoutVersion = '1.2.0' - androidxCoreVersion = '1.12.0' - androidxFragmentVersion = '1.6.2' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.15.0' + androidxFragmentVersion = '1.8.4' junitVersion = '4.13.2' - androidxJunitVersion = '1.1.5' - androidxEspressoCoreVersion = '3.5.1' + androidxJunitVersion = '1.2.1' + androidxEspressoCoreVersion = '3.6.1' cordovaAndroidVersion = '10.1.1' - androidx_core_ktx_version = '1.15.0' + androidx_core_ktx_version = '1.16.0' androidx_media_version = '1.7.0' exoplayer_version = '2.18.7' - glide_version = '4.11.0' + glide_version = '4.16.0' junit_version = '4.13.2' kotlin_version = '2.1.0' kotlin_coroutines_version = '1.10.1' test_runner_version = '1.1.0' coreSplashScreenVersion = '1.0.1' - androidxWebkitVersion = '1.9.0' + androidxWebkitVersion = '1.12.1' } diff --git a/assets/fonts.css b/assets/fonts.css index 1c2cf48d..1d434b77 100644 --- a/assets/fonts.css +++ b/assets/fonts.css @@ -1,19 +1,12 @@ @font-face { - font-family: 'Material Icons'; + font-family: 'Material Symbols Rounded'; font-style: normal; font-weight: 400; - src: url(/fonts/MaterialIcons-Regular.ttf) format('truetype'); + src: url(/fonts/MaterialSymbolsRounded.woff2) format('woff2'); } -@font-face { - font-family: 'Material Icons Outlined'; - font-style: normal; - font-weight: 400; - src: url(/fonts/MaterialIconsOutlined-Regular.otf) format('opentype'); -} - -.material-icons { - font-family: 'Material Icons'; +.material-symbols { + font-family: 'Material Symbols Rounded'; font-weight: normal; font-style: normal; line-height: 1; @@ -24,31 +17,14 @@ word-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; + vertical-align: top; } -.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) { - font-size: 1.5rem; +.material-symbols.fill { + font-variation-settings: + 'FILL' 1 } -.material-icons-outlined { - font-family: 'Material Icons Outlined'; - font-weight: normal; - font-style: normal; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-smoothing: antialiased; -} - -.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) { - font-size: 1.5rem; -} - - /* cyrillic-ext */ @font-face { font-family: 'Source Sans Pro'; @@ -317,4 +293,4 @@ font-display: swap; src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} \ No newline at end of file +} diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index e3424b01..4f8bfb97 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -5,9 +5,9 @@ - arrow_back + arrow_back -
+

{{ currentLibraryName }}

@@ -21,16 +21,18 @@ -
- {{ isCasting ? 'cast_connected' : 'cast' }} +
+ + {{ isCasting ? 'cast_connected' : 'cast' }} +
- - search + + search
- menu + menu
@@ -54,9 +56,6 @@ export default { this.$store.commit('setCastAvailable', val) } }, - socketConnected() { - return this.$store.state.socketConnected - }, currentLibrary() { return this.$store.getters['libraries/getCurrentLibrary'] }, diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index d24bdddb..b7747f2c 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -4,13 +4,13 @@
- expand_more + keyboard_arrow_down
- {{ isCasting ? 'cast_connected' : 'cast' }} + {{ isCasting ? 'cast_connected' : 'cast' }}
- more_vert + more_vert

{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}

@@ -36,7 +36,7 @@
- error + error
@@ -50,9 +50,9 @@
- {{ bookmarks.length ? 'bookmark' : 'bookmark_border' }} + bookmark - bookmark + bookmark {{ currentPlaybackRate }}x @@ -62,23 +62,23 @@

{{ sleepTimeRemainingPretty }}

- format_list_bulleted + format_list_bulleted
- first_page - {{ jumpBackwardsIcon }} + first_page + {{ jumpBackwardsIcon }}
- {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }} + {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}
- {{ jumpForwardIcon }} - last_page + {{ jumpForwardIcon }} + last_page
@@ -1106,7 +1106,7 @@ export default { min-height: 40px; margin: 0px 7px; } -#playerControls .play-btn .material-icons { +#playerControls .play-btn .material-symbols { transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition-property: font-size; @@ -1142,7 +1142,7 @@ export default { min-width: 65px; min-height: 65px; } -.fullscreen #playerControls .play-btn .material-icons { +.fullscreen #playerControls .play-btn .material-symbols { font-size: 2.1rem; } diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index fe3f569d..958fc968 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -9,7 +9,7 @@ \ No newline at end of file + diff --git a/components/home/BookshelfNavBar.vue b/components/home/BookshelfNavBar.vue index 0d04dfe6..1bfb4b8d 100644 --- a/components/home/BookshelfNavBar.vue +++ b/components/home/BookshelfNavBar.vue @@ -62,7 +62,7 @@ export default { items.push({ to: '/bookshelf/add-podcast', routeName: 'bookshelf-add-podcast', - iconPack: 'material-icons', + iconPack: 'material-symbols', icon: 'podcasts', iconClass: 'text-xl', text: this.$strings.ButtonAdd @@ -97,7 +97,7 @@ export default { { to: '/bookshelf/collections', routeName: 'bookshelf-collections', - iconPack: 'material-icons-outlined', + iconPack: 'material-symbols', icon: 'collections_bookmark', iconClass: 'text-xl', text: this.$strings.ButtonCollections @@ -117,8 +117,9 @@ export default { items.push({ to: '/bookshelf/playlists', routeName: 'bookshelf-playlists', - iconPack: 'material-icons', + iconPack: 'material-symbols', icon: 'queue_music', + iconClass: 'text-2xl', text: this.$strings.ButtonPlaylists }) } @@ -149,4 +150,4 @@ export default { #bookshelf-navbar a { font-size: 0.9rem; } - \ No newline at end of file + diff --git a/components/home/BookshelfToolbar.vue b/components/home/BookshelfToolbar.vue index 98cbb86d..5e48b3f4 100644 --- a/components/home/BookshelfToolbar.vue +++ b/components/home/BookshelfToolbar.vue @@ -5,16 +5,16 @@

{{ $formatNumber(totalEntities) }} {{ entityTitle }}

{{ selectedSeriesName }} ({{ $formatNumber(totalEntities) }})

- {{ !bookshelfListView ? 'view_list' : 'grid_view' }} + {{ !bookshelfListView ? 'view_list' : 'grid_view' }} - download - more_vert + download + more_vert
diff --git a/components/modals/AutoSleepTimerRewindLengthModal.vue b/components/modals/AutoSleepTimerRewindLengthModal.vue index 423827cb..b32ecb43 100644 --- a/components/modals/AutoSleepTimerRewindLengthModal.vue +++ b/components/modals/AutoSleepTimerRewindLengthModal.vue @@ -6,22 +6,25 @@
-
+ " + >
- arrow_back + arrow_back
- remove + remove

{{ manualTimeoutMin }} min

- add + add
{{ $strings.ButtonSetTimer }} diff --git a/components/modals/BookmarksModal.vue b/components/modals/BookmarksModal.vue index ed92341d..5d91bcf8 100644 --- a/components/modals/BookmarksModal.vue +++ b/components/modals/BookmarksModal.vue @@ -10,7 +10,7 @@
- arrow_back + arrow_back

{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}

@@ -31,7 +31,7 @@
- add + add

{{ $strings.ButtonCreateBookmark }}

{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}

diff --git a/components/modals/CustomHeadersModal.vue b/components/modals/CustomHeadersModal.vue index b2df36ef..913eabc8 100644 --- a/components/modals/CustomHeadersModal.vue +++ b/components/modals/CustomHeadersModal.vue @@ -25,7 +25,7 @@

{{ value }}

- +
diff --git a/components/modals/Dialog.vue b/components/modals/Dialog.vue index 814f43ea..6543f0ec 100644 --- a/components/modals/Dialog.vue +++ b/components/modals/Dialog.vue @@ -13,7 +13,7 @@
  • - {{ item.icon }} + {{ item.icon }}

    {{ item.text }}

  • diff --git a/components/modals/FilterModal.vue b/components/modals/FilterModal.vue index d6403879..7664e768 100644 --- a/components/modals/FilterModal.vue +++ b/components/modals/FilterModal.vue @@ -14,7 +14,7 @@ {{ item.text }}
    - arrow_right + arrow_right
    @@ -22,7 +22,7 @@
    • - arrow_left + arrow_left
      {{ $strings.ButtonBack }} diff --git a/components/modals/Modal.vue b/components/modals/Modal.vue index 44639f26..bb2a56da 100644 --- a/components/modals/Modal.vue +++ b/components/modals/Modal.vue @@ -3,7 +3,7 @@
      - close + close
      @@ -110,4 +110,4 @@ export default { this.$eventBus.$off('close-modal', this.closeModalEvt) } } - \ No newline at end of file + diff --git a/components/modals/OrderModal.vue b/components/modals/OrderModal.vue index b01b5dab..6bd303d4 100644 --- a/components/modals/OrderModal.vue +++ b/components/modals/OrderModal.vue @@ -8,7 +8,7 @@ {{ item.text }}
      - {{ descending ? 'south' : 'north' }} + {{ descending ? 'south' : 'north' }}
    • @@ -119,6 +119,10 @@ export default { { text: this.$strings.LabelEpisode, value: 'episode' + }, + { + text: this.$strings.LabelFilename, + value: 'audioFile.metadata.filename' } ] } diff --git a/components/modals/PlaybackSpeedModal.vue b/components/modals/PlaybackSpeedModal.vue index 109527bb..f61903ec 100644 --- a/components/modals/PlaybackSpeedModal.vue +++ b/components/modals/PlaybackSpeedModal.vue @@ -19,13 +19,13 @@

    {{ playbackRate }}

    @@ -118,4 +118,4 @@ button.icon-num-btn:disabled::before { button.icon-num-btn:disabled span { color: #777; } - \ No newline at end of file + diff --git a/components/modals/PodcastEpisodesFeedModal.vue b/components/modals/PodcastEpisodesFeedModal.vue index da34649b..cca9a083 100644 --- a/components/modals/PodcastEpisodesFeedModal.vue +++ b/components/modals/PodcastEpisodesFeedModal.vue @@ -10,7 +10,7 @@
    -
    +
      diff --git a/components/modals/SleepTimerLengthModal.vue b/components/modals/SleepTimerLengthModal.vue index bad7252f..1e399eac 100644 --- a/components/modals/SleepTimerLengthModal.vue +++ b/components/modals/SleepTimerLengthModal.vue @@ -16,12 +16,12 @@
      - arrow_back + arrow_back
      - remove + remove

      {{ manualTimeoutMin }} min

      - add + add
      {{ $strings.ButtonSetTimer }}
      diff --git a/components/modals/SleepTimerModal.vue b/components/modals/SleepTimerModal.vue index dbf2c43d..9e74b11c 100644 --- a/components/modals/SleepTimerModal.vue +++ b/components/modals/SleepTimerModal.vue @@ -10,12 +10,12 @@
      - arrow_back + arrow_back
      - remove + remove

      {{ manualTimeoutMin }} min

      - add + add
      {{ $strings.ButtonSetTimer }}
      @@ -40,9 +40,9 @@
    - remove + remove

    {{ timeRemainingPretty }}

    - add + add
    {{ isAuto ? $strings.ButtonDisableAutoTimer : $strings.ButtonCancelTimer }} diff --git a/components/modals/bookmarks/BookmarkItem.vue b/components/modals/bookmarks/BookmarkItem.vue index 65234c7b..19421a34 100644 --- a/components/modals/bookmarks/BookmarkItem.vue +++ b/components/modals/bookmarks/BookmarkItem.vue @@ -2,16 +2,16 @@
    - {{ highlight ? 'bookmark' : 'bookmark_border' }} + bookmark

    {{ bookmark.title }}

    -

    schedule{{ $secondsToTimestamp(bookmark.time / playbackRate) }}

    +

    schedule{{ $secondsToTimestamp(bookmark.time / playbackRate) }}

    - edit - delete + edit + delete
    @@ -42,4 +42,4 @@ export default { } } } - \ No newline at end of file + diff --git a/components/modals/downloads/DownloadItem.vue b/components/modals/downloads/DownloadItem.vue index 679de08f..1596653f 100644 --- a/components/modals/downloads/DownloadItem.vue +++ b/components/modals/downloads/DownloadItem.vue @@ -9,13 +9,13 @@
    - error_outline + error_outline
    - delete + delete
    @@ -49,4 +49,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/components/modals/playlists/AddCreateModal.vue b/components/modals/playlists/AddCreateModal.vue index 80718950..82d8f63a 100644 --- a/components/modals/playlists/AddCreateModal.vue +++ b/components/modals/playlists/AddCreateModal.vue @@ -3,7 +3,7 @@

    {{ $strings.LabelAddToPlaylist }}

    @@ -12,7 +12,7 @@
    - arrow_back + arrow_back

    {{ $strings.HeaderNewPlaylist }}

    diff --git a/components/modals/rssfeeds/RssFeedModal.vue b/components/modals/rssfeeds/RssFeedModal.vue index e7e23576..97b11a0f 100644 --- a/components/modals/rssfeeds/RssFeedModal.vue +++ b/components/modals/rssfeeds/RssFeedModal.vue @@ -3,7 +3,7 @@
    @@ -14,7 +14,7 @@ - {{ linkCopied ? 'done' : 'content_copy' }} + {{ linkCopied ? 'check' : 'content_copy' }}
    diff --git a/components/readers/ComicReader.vue b/components/readers/ComicReader.vue index eb237eef..e18885e3 100644 --- a/components/readers/ComicReader.vue +++ b/components/readers/ComicReader.vue @@ -23,7 +23,7 @@
    -
    +

    {{ page }} / {{ numPages }}

    @@ -49,7 +49,8 @@ export default { default: () => {} }, isLocal: Boolean, - keepProgress: Boolean + keepProgress: Boolean, + showingToolbar: Boolean }, data() { return { @@ -372,14 +373,12 @@ export default { \ No newline at end of file + diff --git a/components/readers/EpubReader.vue b/components/readers/EpubReader.vue index 813b64e7..f40cf07c 100644 --- a/components/readers/EpubReader.vue +++ b/components/readers/EpubReader.vue @@ -2,7 +2,7 @@
    -
    +

    Location {{ currentLocationNum }} of {{ totalLocations }}

    {{ progress }}%

    @@ -105,17 +105,21 @@ export default { isLightTheme() { return this.ereaderSettings.theme === 'light' }, + isDarkTheme() { + return this.ereaderSettings.theme === 'dark' + }, themeRules() { const isDark = this.ereaderSettings.theme === 'dark' - const fontColor = isDark ? '#fff' : '#000' - const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)' + const isBlack = this.ereaderSettings.theme === 'black' + const fontColor = isDark ? '#fff' : isBlack ? '#fff' : '#000' + const backgroundColor = isDark ? 'rgb(35 35 35)' : isBlack ? 'rgb(0 0 0)' : 'rgb(255, 255, 255)' return { '*': { color: `${fontColor}!important`, 'background-color': `${backgroundColor}!important`, 'line-height': this.ereaderSettings.lineSpacing + '%!important', - '-webkit-text-stroke': this.ereaderSettings.textStroke/100 + 'px ' + fontColor + '!important' + '-webkit-text-stroke': this.ereaderSettings.textStroke / 100 + 'px ' + fontColor + '!important' }, a: { color: `${fontColor}!important` @@ -427,7 +431,7 @@ export default { document.removeEventListener('orientationchange', this.screenOrientationChange) } window.removeEventListener('resize', this.screenOrientationChange) - }, + } } diff --git a/components/readers/PdfReader.vue b/components/readers/PdfReader.vue index 88544c7c..8a163719 100644 --- a/components/readers/PdfReader.vue +++ b/components/readers/PdfReader.vue @@ -2,12 +2,12 @@
    - arrow_back_ios + arrow_back_ios
    - arrow_forward_ios + arrow_forward_ios
    diff --git a/components/readers/Reader.vue b/components/readers/Reader.vue index 516d8902..137457a4 100644 --- a/components/readers/Reader.vue +++ b/components/readers/Reader.vue @@ -1,20 +1,20 @@ \ No newline at end of file + diff --git a/components/stats/YearInReviewServer.vue b/components/stats/YearInReviewServer.vue index e6d443d0..d655efb3 100644 --- a/components/stats/YearInReviewServer.vue +++ b/components/stats/YearInReviewServer.vue @@ -8,7 +8,7 @@ \ No newline at end of file + diff --git a/components/tables/ebook/EbookFilesTableRow.vue b/components/tables/ebook/EbookFilesTableRow.vue index 0c81a90c..263dcafe 100644 --- a/components/tables/ebook/EbookFilesTableRow.vue +++ b/components/tables/ebook/EbookFilesTableRow.vue @@ -1,6 +1,6 @@ diff --git a/components/ui/Menu.vue b/components/ui/Menu.vue index 26f5a708..9d9bd6af 100644 --- a/components/ui/Menu.vue +++ b/components/ui/Menu.vue @@ -5,7 +5,7 @@ {{ label }} - person + person @@ -58,4 +58,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/components/ui/MultiSelect.vue b/components/ui/MultiSelect.vue index 432d082a..03222f68 100644 --- a/components/ui/MultiSelect.vue +++ b/components/ui/MultiSelect.vue @@ -6,8 +6,8 @@
    - edit - close + edit + close
    {{ item }}
    @@ -22,7 +22,7 @@ {{ item }}
    - checkmark + checkmark @@ -258,4 +258,4 @@ input:read-only { color: #aaa; background-color: #444; } - \ No newline at end of file + diff --git a/components/ui/TextInput.vue b/components/ui/TextInput.vue index 0e193ba7..942a97e5 100644 --- a/components/ui/TextInput.vue +++ b/components/ui/TextInput.vue @@ -2,13 +2,13 @@
    - {{ prependIcon }} + {{ prependIcon }}
    - close + close
    - {{ appendIcon }} + {{ appendIcon }}
    @@ -91,4 +91,4 @@ input[type='time']::-webkit-calendar-picker-indicator { html[data-theme='light'] input[type='time']::-webkit-calendar-picker-indicator { filter: unset; } - \ No newline at end of file + diff --git a/components/widgets/ConnectionIndicator.vue b/components/widgets/ConnectionIndicator.vue index cefe3379..24911433 100644 --- a/components/widgets/ConnectionIndicator.vue +++ b/components/widgets/ConnectionIndicator.vue @@ -1,6 +1,6 @@ @@ -79,4 +79,4 @@ export default { mounted() {}, beforeDestroy() {} } - \ No newline at end of file + diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 9f65ad17..167c0ff0 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -15,20 +15,17 @@ 3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; }; 3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.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 */; }; 3AD4FCEB280443DD006DB301 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEA280443DD006DB301 /* Database.swift */; }; 3AD4FCED28044E6C006DB301 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEC28044E6C006DB301 /* Store.swift */; }; 3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970B2806E2590096F747 /* ApiClient.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 */; }; - 4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B951282EE822008272D4 /* AbsDownloader.m */; }; 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 */; }; 4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.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 */; }; 4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; }; 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 = ""; }; 3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = ""; }; 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDatabase.swift; sourceTree = ""; }; - 3AD4FCE628043E72006DB301 /* AbsDatabase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDatabase.m; sourceTree = ""; }; 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionConfig.swift; sourceTree = ""; }; 3AD4FCEA280443DD006DB301 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; 3AD4FCEC28044E6C006DB301 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 3AF1970B2806E2590096F747 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = ""; }; 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = ""; }; - 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = ""; }; 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = ""; }; - 4D66B951282EE822008272D4 /* AbsDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDownloader.m; sourceTree = ""; }; 4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = ""; }; - 4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = ""; }; 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = ""; }; 4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = ""; }; 4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = ""; }; 4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsLogger.swift; sourceTree = ""; }; 4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = ""; }; 4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; @@ -217,13 +211,10 @@ isa = PBXGroup; children = ( 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */, - 3AD4FCE628043E72006DB301 /* AbsDatabase.m */, 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */, - 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */, - 4D66B951282EE822008272D4 /* AbsDownloader.m */, 4D66B953282EE87C008272D4 /* AbsDownloader.swift */, - 4D66B955282EE951008272D4 /* AbsFileSystem.m */, 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */, + 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */, ); path = plugins; sourceTree = ""; @@ -531,7 +522,6 @@ files = ( E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */, E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */, - 3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */, E9FA07E328C82848005520B0 /* Logger.swift in Sources */, @@ -545,6 +535,7 @@ 4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */, E9D5504C28AC1AE000C746DD /* PodcastEpisode.swift in Sources */, E9D5506A28AC1DF100C746DD /* LocalFile.swift in Sources */, + 4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */, 3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */, E9D5506F28AC1E8E00C746DD /* DownloadItem.swift in Sources */, 3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */, @@ -553,7 +544,6 @@ 3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */, 4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */, E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */, - 4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */, EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */, 3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */, 3AB34053280829BF0039308B /* Extensions.swift in Sources */, @@ -561,7 +551,6 @@ 3AD4FCEB280443DD006DB301 /* Database.swift in Sources */, 3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */, 4DABC04F2B0139CA000F6264 /* User.swift in Sources */, - 4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */, E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */, EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */, E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */, @@ -574,7 +563,6 @@ E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */, 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */, E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */, - 3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */, E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */, E9D5505028AC1B3E00C746DD /* Author.swift in Sources */, 3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */, @@ -744,12 +732,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Icons; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 36; + CURRENT_PROJECT_VERSION = 37; DEVELOPMENT_TEAM = 7UFJ7D8V6A; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.79; + MARKETING_VERSION = 0.9.80; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -768,12 +756,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Icons; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 36; + CURRENT_PROJECT_VERSION = 37; DEVELOPMENT_TEAM = 7UFJ7D8V6A; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.9.79; + MARKETING_VERSION = 0.9.80; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 0a912b89..bd270f50 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. let configuration = Realm.Configuration( - schemaVersion: 18, + schemaVersion: 19, migrationBlock: { [weak self] migration, oldSchemaVersion in if (oldSchemaVersion < 1) { self?.logger.log("Realm schema version was \(oldSchemaVersion)") @@ -61,6 +61,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { newObject?["streamingUsingCellular"] = "ALWAYS" } } + if (oldSchemaVersion < 18) { + self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings") + migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in + newObject?["disableSleepTimerFadeOut"] = false + } + } } ) diff --git a/ios/App/App/MyViewController.swift b/ios/App/App/MyViewController.swift index e606a93d..486792e1 100644 --- a/ios/App/App/MyViewController.swift +++ b/ios/App/App/MyViewController.swift @@ -21,6 +21,7 @@ class MyViewController: CAPBridgeViewController { bridge?.registerPluginInstance(AbsAudioPlayer()) bridge?.registerPluginInstance(AbsDownloader()) bridge?.registerPluginInstance(AbsFileSystem()) + bridge?.registerPluginInstance(AbsLogger()) } diff --git a/ios/App/App/plugins/AbsAudioPlayer.m b/ios/App/App/plugins/AbsAudioPlayer.m deleted file mode 100644 index 8170d059..00000000 --- a/ios/App/App/plugins/AbsAudioPlayer.m +++ /dev/null @@ -1,35 +0,0 @@ -// -// AbsAudioPlayer.m -// App -// -// Created by Rasmus Krämer on 13.04.22. -// - -#import -#import - -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); - ) diff --git a/ios/App/App/plugins/AbsAudioPlayer.swift b/ios/App/App/plugins/AbsAudioPlayer.swift index c35a6ce0..1db47887 100644 --- a/ios/App/App/plugins/AbsAudioPlayer.swift +++ b/ios/App/App/plugins/AbsAudioPlayer.swift @@ -11,7 +11,29 @@ import RealmSwift import Network @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 var initialPlayWhenReady = false diff --git a/ios/App/App/plugins/AbsDatabase.m b/ios/App/App/plugins/AbsDatabase.m deleted file mode 100644 index 5fb59835..00000000 --- a/ios/App/App/plugins/AbsDatabase.m +++ /dev/null @@ -1,29 +0,0 @@ -// -// AbsDatabase.m -// App -// -// Created by Rasmus Krämer on 11.04.22. -// - -#import -#import - -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); - ) diff --git a/ios/App/App/plugins/AbsDatabase.swift b/ios/App/App/plugins/AbsDatabase.swift index 2a15197c..8208be78 100644 --- a/ios/App/App/plugins/AbsDatabase.swift +++ b/ios/App/App/plugins/AbsDatabase.swift @@ -27,7 +27,27 @@ extension String { } @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") @objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) { @@ -246,6 +266,7 @@ public class AbsDatabase: CAPPlugin { let languageCode = call.getString("languageCode") ?? "en-us" let downloadUsingCellular = call.getString("downloadUsingCellular") ?? "ALWAYS" let streamingUsingCellular = call.getString("streamingUsingCellular") ?? "ALWAYS" + let disableSleepTimerFadeOut = call.getBool("disableSleepTimerFadeOut") ?? false let settings = DeviceSettings() settings.disableAutoRewind = disableAutoRewind settings.enableAltView = enableAltView @@ -257,6 +278,7 @@ public class AbsDatabase: CAPPlugin { settings.languageCode = languageCode settings.downloadUsingCellular = downloadUsingCellular settings.streamingUsingCellular = streamingUsingCellular + settings.disableSleepTimerFadeOut = disableSleepTimerFadeOut Database.shared.setDeviceSettings(deviceSettings: settings) diff --git a/ios/App/App/plugins/AbsDownloader.m b/ios/App/App/plugins/AbsDownloader.m deleted file mode 100644 index fa1a55ee..00000000 --- a/ios/App/App/plugins/AbsDownloader.m +++ /dev/null @@ -1,13 +0,0 @@ -// -// AbsDownloader.m -// App -// -// Created by advplyr on 5/13/22. -// - -#import -#import - -CAP_PLUGIN(AbsDownloader, "AbsDownloader", - CAP_PLUGIN_METHOD(downloadLibraryItem, CAPPluginReturnPromise); - ) diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index 62d21eb5..b14aa09b 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -10,7 +10,12 @@ import Capacitor import RealmSwift @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] diff --git a/ios/App/App/plugins/AbsFileSystem.m b/ios/App/App/plugins/AbsFileSystem.m deleted file mode 100644 index b1c01f18..00000000 --- a/ios/App/App/plugins/AbsFileSystem.m +++ /dev/null @@ -1,20 +0,0 @@ -// -// AbsFileSystem.m -// App -// -// Created by advplyr on 5/13/22. -// - -#import -#import - -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); - ) diff --git a/ios/App/App/plugins/AbsFileSystem.swift b/ios/App/App/plugins/AbsFileSystem.swift index 5859f7e0..974b900a 100644 --- a/ios/App/App/plugins/AbsFileSystem.swift +++ b/ios/App/App/plugins/AbsFileSystem.swift @@ -9,7 +9,20 @@ import Foundation import Capacitor @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") @objc func selectFolder(_ call: CAPPluginCall) { diff --git a/ios/App/App/plugins/AbsLogger.swift b/ios/App/App/plugins/AbsLogger.swift new file mode 100644 index 00000000..0d7fa254 --- /dev/null +++ b/ios/App/App/plugins/AbsLogger.swift @@ -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") + } +} diff --git a/ios/App/AudiobookshelfUnitTests/Shared/player/util/PlayerTimeUtilsTests.swift b/ios/App/AudiobookshelfUnitTests/Shared/player/util/PlayerTimeUtilsTests.swift index 91bc8403..64c045c4 100644 --- a/ios/App/AudiobookshelfUnitTests/Shared/player/util/PlayerTimeUtilsTests.swift +++ b/ios/App/AudiobookshelfUnitTests/Shared/player/util/PlayerTimeUtilsTests.swift @@ -12,9 +12,36 @@ final class PlayerTimeUtilsTests: XCTestCase { func testCalcSeekBackTime() { let currentTime: Double = 1000 - let threeSecondsAgo = Date(timeIntervalSinceNow: -3) - let lastPlayedMs = threeSecondsAgo.timeIntervalSince1970 * 1000 - XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: lastPlayedMs), 998) + + // 1. Nil lastPlayedMs → should seek back 5s + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: nil), 1000) + + // 2. Played ~2s ago (<6s) → should seek back 2s + let played2sAgo = Date(timeIntervalSinceNow: -2).timeIntervalSince1970 * 1000 + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: played2sAgo), 1000) + + // 3. Played ~12s ago (6-12s range) → should seek back 10s + let played12sAgo = Date(timeIntervalSinceNow: -12).timeIntervalSince1970 * 1000 + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: played12sAgo), 997) + + // 4. Played ~62s ago (12-30s range) → should seek back 15s + let played62sAgo = Date(timeIntervalSinceNow: -62).timeIntervalSince1970 * 1000 + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: played62sAgo), 990) + + // 5. Played ~302s ago (30-180s range) → should seek back 20s + let played302sAgo = Date(timeIntervalSinceNow: -302).timeIntervalSince1970 * 1000 + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: played302sAgo), 980) + + // 6. Played ~1802s ago (180-3600s range) → should seek back 25s + let played1802sAgo = Date(timeIntervalSinceNow: -1802).timeIntervalSince1970 * 1000 + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: played1802sAgo), 970) + + // 8. Edge case where currentTime is small and would go negative + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: 1, lastPlayedMs: played12sAgo), 0) + + // 9. Edge case: negative lastPlayedMs (should be treated as an old timestamp) + XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: -5000), 970) + } func testCalcSeekBackTimeWithZeroCurrentTime() { @@ -27,18 +54,18 @@ final class PlayerTimeUtilsTests: XCTestCase { func testTimeSinceLastPlayed() throws { let fiveSecondsAgo = Date(timeIntervalSinceNow: -5) let lastPlayedMs = fiveSecondsAgo.timeIntervalSince1970 * 1000 - XCTAssertEqual(PlayerTimeUtils.timeSinceLastPlayed(lastPlayedMs)!, -5, accuracy: 1.0) + XCTAssertEqual(PlayerTimeUtils.timeSinceLastPlayed(lastPlayedMs)!, 5, accuracy: 1.0) XCTAssertNil(PlayerTimeUtils.timeSinceLastPlayed(nil)) } func testTimeToSeekBackForSinceLastPlayed() throws { - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(nil), 5, "Seeks back 5 seconds for nil") - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(5), 2, "Seeks back 2 seconds for less than 6 seconds") - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(11), 10, "Seeks back 10 seconds for less than 12 seconds") - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(29), 15, "Seeks back 15 seconds for less than 30 seconds") - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(179), 20, "Seeks back 20 seconds for less than 2 minutes") - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(3599), 25, "Seeks back 25 seconds for less than 59 minutes") - XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(60000), 29, "Seeks back 29 seconds for anything over 59 minuts") + XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(nil), 0, "Seeks back 0 seconds for nil") + XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(5), 0, "Seeks back 0 seconds for less than 10 seconds") + XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(11), 3, "Seeks back 3 seconds for less than 1 minute") + XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(298), 10, "Seeks back 10 seconds for less than 5 minutes") + XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(1798), 20, "Seeks back 20 seconds for less than 30 minutes") + XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(3599), 30, "Seeks back 30 seconds for greater than 30 minutes") + } } diff --git a/ios/App/Podfile b/ios/App/Podfile index fd0ec2f0..b7fd47d3 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -1,6 +1,6 @@ require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' -platform :ios, '13.0' +platform :ios, '14.0' use_frameworks! # workaround to avoid Xcode caching of Pods that requires @@ -11,7 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' - pod 'ByteowlsCapacitorFilesharer', :path => '../../node_modules/@byteowls/capacitor-filesharer' + pod 'WebnativellcCapacitorFilesharer', :path => '../../node_modules/@webnativellc/capacitor-filesharer' pod 'CapacitorCommunityKeepAwake', :path => '../../node_modules/@capacitor-community/keep-awake' pod 'CapacitorCommunityVolumeButtons', :path => '../../node_modules/@capacitor-community/volume-buttons' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index ae9fc907..8cf19b7b 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -1,41 +1,40 @@ PODS: - Alamofire (5.8.1) - - ByteowlsCapacitorFilesharer (6.0.0): - - Capacitor - - Capacitor (6.2.0): + - Capacitor (7.2.0): - CapacitorCordova - - CapacitorApp (6.0.2): + - CapacitorApp (7.0.1): - Capacitor - - CapacitorBrowser (6.0.4): + - CapacitorBrowser (7.0.1): - Capacitor - - CapacitorClipboard (6.0.2): + - CapacitorClipboard (7.0.1): - Capacitor - - CapacitorCommunityKeepAwake (5.0.1): + - CapacitorCommunityKeepAwake (7.0.0): - Capacitor - - CapacitorCommunityVolumeButtons (6.0.1): + - CapacitorCommunityVolumeButtons (7.0.0): - Capacitor - - CapacitorCordova (6.2.0) - - CapacitorDialog (6.0.2): + - CapacitorCordova (7.2.0) + - CapacitorDialog (7.0.1): - Capacitor - - CapacitorHaptics (6.0.2): + - CapacitorHaptics (7.0.1): - Capacitor - - CapacitorNetwork (6.0.3): + - CapacitorNetwork (7.0.1): - Capacitor - - CapacitorPreferences (6.0.3): + - CapacitorPreferences (7.0.1): - Capacitor - - CapacitorStatusBar (6.0.2): + - CapacitorStatusBar (7.0.1): - Capacitor - - CordovaPlugins (6.2.0): + - CordovaPlugins (6.2.1): - CapacitorCordova - Realm (10.47.0): - Realm/Headers (= 10.47.0) - Realm/Headers (10.47.0) - RealmSwift (10.47.0): - Realm (= 10.47.0) + - WebnativellcCapacitorFilesharer (7.0.4): + - Capacitor DEPENDENCIES: - Alamofire (~> 5.5) - - "ByteowlsCapacitorFilesharer (from `../../node_modules/@byteowls/capacitor-filesharer`)" - "Capacitor (from `../../node_modules/@capacitor/ios`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)" @@ -50,6 +49,7 @@ DEPENDENCIES: - "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)" - CordovaPlugins (from `../capacitor-cordova-ios-plugins`) - RealmSwift (~> 10) + - "WebnativellcCapacitorFilesharer (from `../../node_modules/@webnativellc/capacitor-filesharer`)" SPEC REPOS: trunk: @@ -58,8 +58,6 @@ SPEC REPOS: - RealmSwift EXTERNAL SOURCES: - ByteowlsCapacitorFilesharer: - :path: "../../node_modules/@byteowls/capacitor-filesharer" Capacitor: :path: "../../node_modules/@capacitor/ios" CapacitorApp: @@ -86,26 +84,28 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/status-bar" CordovaPlugins: :path: "../capacitor-cordova-ios-plugins" + WebnativellcCapacitorFilesharer: + :path: "../../node_modules/@webnativellc/capacitor-filesharer" SPEC CHECKSUMS: Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 - ByteowlsCapacitorFilesharer: ea14537059851aed44df8c52f0984f61cd34f53b - Capacitor: 1f3c7b9802d958cd8c4eb63895fff85dff2e1eea - CapacitorApp: 2a8c3a0b0814322e5e6e15fe595f02c3808f0f8b - CapacitorBrowser: ef0529d16cd8839281050c350e7bbee4f5c6d65f - CapacitorClipboard: 55e0a514f1e97b1409d533266c119dcbff3e78c3 - CapacitorCommunityKeepAwake: e2ddd50812e3407f8dc3a2c28e97d66e9b59b2f5 - CapacitorCommunityVolumeButtons: a7612c5996f1c66320ef7a567522346d84a57e22 - CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9 - CapacitorDialog: e966e2261e1c74a8a502e610dd66b4f97ec6fcca - CapacitorHaptics: b53409aaca1203f79c6d0eb3ed5de40556339518 - CapacitorNetwork: da96b5fff8d05b67f4658503aabb22f65bda2c0f - CapacitorPreferences: 9f9935bce493977183511362131a93f7b260191a - CapacitorStatusBar: 3b9ac7d0684770522c532d1158a1434512ab1477 - CordovaPlugins: 08731213c63ccf137c576da2926bf4b3a8c40f17 + Capacitor: 106e7a4205f4618d582b886a975657c61179138d + CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39 + CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06 + CapacitorClipboard: b98aead5dc7ec595547fc2c5d75bacd2ae3338bc + CapacitorCommunityKeepAwake: 00dfd8fa3cca0df003c9a3e2cd7bee678aeec68b + CapacitorCommunityVolumeButtons: 8a0443a202ed659688d85f4d44d66f42f62f2b56 + CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f + CapacitorDialog: 9b934329026b2b0ffa56939bb06df3c67541a2ab + CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3 + CapacitorNetwork: 07ec4c69c1bb696f41c23e00d31bda1bbb221bba + CapacitorPreferences: cbf154e5e5519b7f5ab33817a334dda1e98387f9 + CapacitorStatusBar: 275cbf2f4dfc00388f519ef80c7ec22edda342c9 + CordovaPlugins: 5a72a85b45469e68556bb172409f1b6d57b27236 Realm: e43fb540ae947497e3ea8a662443256920602060 RealmSwift: 8b06ed06b5d16749ae0c4d91c0cba414a9e28189 + WebnativellcCapacitorFilesharer: 10b111373d4dc49608935600dcbcc14605258c73 -PODFILE CHECKSUM: 96d7bd74a37a613766883c65fc082490b5dbd7e9 +PODFILE CHECKSUM: 498821c0cfa2508609567fa95d7244c01cbef538 COCOAPODS: 1.12.1 diff --git a/ios/App/Shared/models/DeviceSettings.swift b/ios/App/Shared/models/DeviceSettings.swift index 4cd32ecc..1ad8546d 100644 --- a/ios/App/Shared/models/DeviceSettings.swift +++ b/ios/App/Shared/models/DeviceSettings.swift @@ -19,6 +19,7 @@ class DeviceSettings: Object { @Persisted var languageCode: String = "en-us" @Persisted var downloadUsingCellular: String = "ALWAYS" @Persisted var streamingUsingCellular: String = "ALWAYS" + @Persisted var disableSleepTimerFadeOut: Bool = false } func getDefaultDeviceSettings() -> DeviceSettings { @@ -36,6 +37,7 @@ func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary { "hapticFeedback": settings.hapticFeedback, "languageCode": settings.languageCode, "downloadUsingCellular": settings.downloadUsingCellular, - "streamingUsingCellular": settings.streamingUsingCellular + "streamingUsingCellular": settings.streamingUsingCellular, + "disableSleepTimerFadeOut": settings.disableSleepTimerFadeOut ] } diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 0ad0e9e6..e46b0da7 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -45,6 +45,8 @@ class AudioPlayer: NSObject { private var queueObserver:NSKeyValueObservation? private var queueItemStatusObserver:NSKeyValueObservation? + private var isRebuildingQueue = false + // Sleep timer values internal var sleepTimeChapterStopAt: Double? internal var sleepTimeChapterToken: Any? @@ -365,7 +367,57 @@ class AudioPlayer: NSObject { self.status = .paused updateNowPlaying() } - + + public func startFadeOut() { + guard self.isInitialized() else { return } + guard let currentTime = self.getCurrentTime() else { return } + logger.log("fadeOut: Fading out playback") + + // Define fade parameters. + let fadeDuration: Float = 60.0 // total fade duration in seconds + let interval: Float = 1.0 // timer interval in seconds + + // Get the current volume. + let initialVolume = self.audioPlayer.volume + let targetVolume: Float = 0.0 + + // If the current volume is already at or below zero, just pause. + if initialVolume <= targetVolume { + self.pause() + return + } + + // Calculate the volume change per timer tick. + // (targetVolume - initialVolume) is negative since target < initial. + let step = (targetVolume - initialVolume) * interval / fadeDuration + + // Schedule a timer on the main queue to adjust the volume. + DispatchQueue.runOnMainQueue { [weak self] in + var timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(interval), repeats: true) { t in + guard let self = self else { + t.invalidate() + return + } + + // Calculate the new volume. + let newVolume = self.audioPlayer.volume + step + + // Check if the next step would go below zero. + if newVolume > targetVolume { + self.audioPlayer.volume = newVolume + } else { + // Ensure volume is exactly zero and end fade. + self.audioPlayer.volume = targetVolume + t.invalidate() + self.logger.log("Fadeout: Fade complete, pausing playback") + self.pause() + self.audioPlayer.volume = initialVolume + self.seek(currentTime, from: "fadeOut") + } + } + } + } + public func seek(_ to: Double, from: String) { logger.log("SEEK: Seek to \(to) from \(from)") @@ -374,6 +426,31 @@ class AudioPlayer: NSObject { let indexOfSeek = getItemIndexForTime(time: to) logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") + if self.audioPlayer.currentItem == nil { + self.currentTrackIndex = indexOfSeek + + try? playbackSession.update { + playbackSession.currentTime = to + } + + let playerItems = self.allPlayerItems[indexOfSeek.. Double? { guard let playbackSession = self.getPlaybackSession() else { return nil } - let currentTrackTime = self.audioPlayer.currentTime().seconds let audioTrack = playbackSession.audioTracks[currentTrackIndex] let startOffset = audioTrack.startOffset ?? 0.0 + + // if the currentTrackTime isNan, then fall back on session. + let currentTrackTime = self.audioPlayer.currentTime().seconds + if currentTrackTime.isNaN { + return playbackSession.currentTime + } return startOffset + currentTrackTime } @@ -718,6 +804,15 @@ class AudioPlayer: NSObject { if keyPath == #keyPath(AVPlayer.currentItem) { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) logger.log("WARNING: Item ended") + + if audioPlayer.currentItem == nil { + // if the queue is rebuilding, we expect the current item may be nil + if self.isRebuildingQueue { + return + } + logger.log("Player ended or next item is nil, marking ended") + self.markAudioSessionAs(active: false) + } } } else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 28cfdf06..2200112f 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -125,7 +125,11 @@ extension AudioPlayer { if var sleepTimeRemaining = self.sleepTimeRemaining { sleepTimeRemaining -= 1 self.sleepTimeRemaining = sleepTimeRemaining - + + if sleepTimeRemaining == 60 && self.isSleepTimerFadeOutEnabled() { + self.startFadeOut() + } + // Handle the sleep if the timer has expired if sleepTimeRemaining <= 0 { self.handleSleepEnd() @@ -154,5 +158,9 @@ extension AudioPlayer { private func isChapterSleepTimerSet() -> Bool { return self.sleepTimeChapterStopAt != nil } - + + private func isSleepTimerFadeOutEnabled() -> Bool { + let deviceSettings = Database.shared.getDeviceSettings() + return !deviceSettings.disableSleepTimerFadeOut + } } diff --git a/ios/App/Shared/player/util/PlayerTimeUtils.swift b/ios/App/Shared/player/util/PlayerTimeUtils.swift index 24fc00a1..27206008 100644 --- a/ios/App/Shared/player/util/PlayerTimeUtils.swift +++ b/ios/App/Shared/player/util/PlayerTimeUtils.swift @@ -21,27 +21,27 @@ class PlayerTimeUtils { static internal func timeSinceLastPlayed(_ lastPlayedMs: Double?) -> TimeInterval? { guard let lastPlayedMs = lastPlayedMs else { return nil } let lastPlayed = Date(timeIntervalSince1970: lastPlayedMs / 1000) - return lastPlayed.timeIntervalSinceNow + return Date().timeIntervalSince(lastPlayed) } static internal func timeToSeekBackForSinceLastPlayed(_ sinceLastPlayed: TimeInterval?) -> TimeInterval { + if isAutoRewindDisabled(){ + return 0 + } if let sinceLastPlayed = sinceLastPlayed { - if sinceLastPlayed < 6 { - return 2 - } else if sinceLastPlayed < 12 { - return 10 - } else if sinceLastPlayed < 30 { - return 15 - } else if sinceLastPlayed < 180 { - return 20 - } else if sinceLastPlayed < 3600 { - return 25 - } else { - return 29 - } + if sinceLastPlayed < 10 { return 0 } // 10s or less = no seekback + else if sinceLastPlayed < 60 { return 3 } // 10s to 1m = jump back 3s + else if sinceLastPlayed < 300 { return 10 } // 1m to 5m = jump back 10s + else if sinceLastPlayed < 1800 { return 20 } // 5m to 30m = jump back 20s + else { return 30 } // 30m and up = jump back 30s } else { - return 5 + return 0 } } + static internal func isAutoRewindDisabled() -> Bool { + let deviceSettings = Database.shared.getDeviceSettings() + return deviceSettings.disableAutoRewind + } + } diff --git a/layouts/default.vue b/layouts/default.vue index 135df4ce..09a9355b 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -16,6 +16,7 @@ \ No newline at end of file + diff --git a/pages/collection/_id.vue b/pages/collection/_id.vue index 964faa19..9f371c66 100644 --- a/pages/collection/_id.vue +++ b/pages/collection/_id.vue @@ -13,7 +13,7 @@
    - {{ playerIsPlaying ? 'pause' : 'play_arrow' }} + {{ playerIsPlaying ? 'pause' : 'play_arrow' }} {{ playerIsPlaying ? $strings.ButtonPause : $strings.ButtonPlay }}
    @@ -139,4 +139,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/pages/connect.vue b/pages/connect.vue index ec1b2426..b20c5f69 100644 --- a/pages/connect.vue +++ b/pages/connect.vue @@ -2,7 +2,7 @@
    - arrow_back + arrow_back
    diff --git a/pages/downloading.vue b/pages/downloading.vue index c793533b..248499e8 100644 --- a/pages/downloading.vue +++ b/pages/downloading.vue @@ -7,7 +7,7 @@
    - check_circle_outline + check_circle {{ Math.round(itemPart.progress) }}%
    diff --git a/pages/downloads.vue b/pages/downloads.vue index 42bd1f9d..be7b4eef 100644 --- a/pages/downloads.vue +++ b/pages/downloads.vue @@ -16,16 +16,14 @@

    {{ $bytesPretty(mediaItem.size) }}

    - chevron_right + chevron_right
    -
    - {{ $strings.LabelTotalSize }}: {{ $bytesPretty(localLibraryItems.reduce((acc, item) => acc + item.size, 0)) }} -
    +
    {{ $strings.LabelTotalSize }}: {{ $bytesPretty(localLibraryItems.reduce((acc, item) => acc + item.size, 0)) }}
    diff --git a/pages/item/_id/_episode/index.vue b/pages/item/_id/_episode/index.vue index be3ec3c6..e560e45f 100644 --- a/pages/item/_id/_episode/index.vue +++ b/pages/item/_id/_episode/index.vue @@ -30,14 +30,14 @@
    - {{ playerIsPlaying ? 'pause' : 'play_arrow' }} + {{ playerIsPlaying ? 'pause' : 'play_arrow' }} {{ playerIsPlaying ? $strings.ButtonPause : localEpisodeId ? $strings.ButtonPlay : $strings.ButtonStream }} - {{ (downloadItem || startingDownload) ? 'downloading' : 'download' }} + {{ downloadItem || startingDownload ? 'downloading' : 'download' }} - more_vert + more_vert
    @@ -324,14 +324,16 @@ export default { return timeParts.reduce((acc, part, index) => acc * 60 + part, 0) } - return description.replace(timeMarkerLinkRegex, (match, href, displayTime) => { - const time = displayTime.match(timeMarkerRegex)[0] - const seekTimeInSeconds = convertToSeconds(time) - return `${displayTime}` - }).replace(timeMarkerRegex, (match) => { - const seekTimeInSeconds = convertToSeconds(match) - return `${match}` - }) + return description + .replace(timeMarkerLinkRegex, (match, href, displayTime) => { + const time = displayTime.match(timeMarkerRegex)[0] + const seekTimeInSeconds = convertToSeconds(time) + return `${displayTime}` + }) + .replace(timeMarkerRegex, (match) => { + const seekTimeInSeconds = convertToSeconds(match) + return `${match}` + }) }, async deleteLocalEpisode() { await this.$hapticsImpact() @@ -612,7 +614,7 @@ export default { }, beforeDestroy() { this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem) - document.querySelectorAll('.time-marker').forEach(marker => { + document.querySelectorAll('.time-marker').forEach((marker) => { marker.removeEventListener('click', this.clickPlaybackTime) }) } diff --git a/pages/item/_id/index.vue b/pages/item/_id/index.vue index b6d76e10..92895276 100644 --- a/pages/item/_id/index.vue +++ b/pages/item/_id/index.vue @@ -51,22 +51,22 @@
    - {{ playerIsPlaying ? 'pause' : 'play_arrow' }} + {{ playerIsPlaying ? 'pause' : 'play_arrow' }} {{ playerIsPlaying ? $strings.ButtonPause : isPodcast ? $strings.ButtonNextEpisode : hasLocal ? $strings.ButtonPlay : $strings.ButtonStream }} - auto_stories + auto_stories {{ $strings.ButtonRead }} {{ ebookFormat }} - {{ downloadItem || startingDownload ? 'downloading' : 'download' }} + {{ downloadItem || startingDownload ? 'downloading' : 'download' }} - more_vert + more_vert
    - error + error {{ $strings.LabelMissing }} @@ -140,7 +140,7 @@
    {{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} - {{ showFullDescription ? 'expand_less' : 'expand_more' }} + {{ showFullDescription ? 'arrow_drop_up' : 'arrow_drop_down' }}
    diff --git a/pages/localMedia/folders/_id.vue b/pages/localMedia/folders/_id.vue index 3c722b6c..9fb189dd 100644 --- a/pages/localMedia/folders/_id.vue +++ b/pages/localMedia/folders/_id.vue @@ -4,7 +4,7 @@

    {{ $strings.LabelFolder }}: {{ folderName }}

    - more_vert + more_vert

    {{ $strings.LabelMediaType }}: {{ mediaType }}

    @@ -22,7 +22,7 @@

    {{ getLocalLibraryItemSubText(localLibraryItem) }}

    - arrow_right + arrow_right
    @@ -159,4 +159,4 @@ export default { height: calc(100vh - 310px); max-height: calc(100vh - 310px); } - \ No newline at end of file + diff --git a/pages/localMedia/folders/index.vue b/pages/localMedia/folders/index.vue index b63f25f1..301c84c3 100644 --- a/pages/localMedia/folders/index.vue +++ b/pages/localMedia/folders/index.vue @@ -4,17 +4,17 @@

    {{ $strings.HeaderLocalFolders }}

    - +
    diff --git a/pages/localMedia/item/_id.vue b/pages/localMedia/item/_id.vue index 454904d7..1704cfdb 100644 --- a/pages/localMedia/item/_id.vue +++ b/pages/localMedia/item/_id.vue @@ -6,9 +6,9 @@
    - more_vert + more_vert

    {{ $strings.LabelFolder }}: {{ folderName }}

    @@ -27,7 +27,7 @@ @@ -66,7 +66,7 @@

    {{ $elapsedPretty(episode.audioTrack.duration) }}

    - more_vert + more_vert
    @@ -98,7 +98,7 @@
    - music_note + music_note

    {{ file.filename }}

    @@ -111,9 +111,7 @@
    -
    - {{ $strings.LabelTotalSize }}: {{ $bytesPretty(totalLibraryItemSize) }} -
    +
    {{ $strings.LabelTotalSize }}: {{ $bytesPretty(totalLibraryItemSize) }}
    @@ -263,7 +261,7 @@ export default { return this.$store.state.playerIsStartingPlayback }, totalAudioSize() { - return this.audioTracks.reduce((acc, item) => item.metadata ? acc + item.metadata.size : acc, 0) + return this.audioTracks.reduce((acc, item) => (item.metadata ? acc + item.metadata.size : acc), 0) }, totalEpisodesSize() { return this.episodes.reduce((acc, item) => acc + item.size, 0) @@ -353,7 +351,7 @@ export default { } else if (action == 'play-episode') { this.playEpisode() } - this.showDialog = false; + this.showDialog = false }, getLocalFileForTrack(localFileId) { return this.localFiles.find((lf) => lf.id == localFileId) diff --git a/pages/logs.vue b/pages/logs.vue new file mode 100644 index 00000000..2933393d --- /dev/null +++ b/pages/logs.vue @@ -0,0 +1,175 @@ + + + diff --git a/pages/media/_id/history.vue b/pages/media/_id/history.vue index fb38c67a..d63b7b66 100644 --- a/pages/media/_id/history.vue +++ b/pages/media/_id/history.vue @@ -10,11 +10,11 @@

    {{ name }}

    {{ $formatDate(evt.timestamp, 'HH:mm') }}

    - {{ getEventIcon(evt.name) }} + {{ getEventIcon(evt.name) }}

    {{ evt.name }}

    - cloud_done - error_outline + cloud_done + error_outline

    +{{ evt.num }}

    @@ -150,9 +150,9 @@ export default { getEventIcon(name) { switch (name) { case 'Play': - return 'play_circle_filled' + return 'play_circle' case 'Pause': - return 'pause_circle_filled' + return 'pause_circle' case 'Stop': return 'stop_circle' case 'Save': diff --git a/pages/playlist/_id.vue b/pages/playlist/_id.vue index 25096407..6f84cc8b 100644 --- a/pages/playlist/_id.vue +++ b/pages/playlist/_id.vue @@ -11,7 +11,7 @@
    - {{ playerIsPlaying ? 'pause' : 'play_arrow' }} + {{ playerIsPlaying ? 'pause' : 'play_arrow' }} {{ playerIsPlaying ? $strings.ButtonPause : $strings.ButtonPlay }}
    @@ -173,4 +173,4 @@ export default { this.$socket.$off('playlist_removed', this.playlistRemoved) } } - \ No newline at end of file + diff --git a/pages/settings.vue b/pages/settings.vue index 258d7375..660d6a6a 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -36,7 +36,7 @@

    {{ $strings.HeaderPlaybackSettings }}

    -
    +
    @@ -44,13 +44,13 @@
    - {{ currentJumpBackwardsTimeIcon }} + {{ currentJumpBackwardsTimeIcon }}

    {{ $strings.LabelJumpBackwardsTime }}

    - {{ currentJumpForwardTimeIcon }} + {{ currentJumpForwardTimeIcon }}

    {{ $strings.LabelJumpForwardsTime }}

    @@ -59,7 +59,7 @@

    {{ $strings.LabelEnableMp3IndexSeeking }}

    - info + info
    @@ -76,7 +76,7 @@

    {{ $strings.LabelDisableShakeToReset }}

    - info + info

    {{ $strings.LabelShakeSensitivity }}

    @@ -84,26 +84,35 @@
    -
    -
    - -
    -

    {{ $strings.LabelDisableAudioFadeOut }}

    - info + +
    +
    +
    +

    {{ $strings.LabelDisableAudioFadeOut }}

    + info +
    + @@ -126,7 +135,7 @@

    {{ $strings.LabelAutoSleepTimerAutoRewind }}

    - info + info

    {{ $strings.LabelAutoRewindTime }}

    @@ -156,7 +165,7 @@

    {{ $strings.LabelAndroidAutoBrowseLimitForGrouping }}

    - info + info

    {{ $strings.LabelAndroidAutoBrowseSeriesSequenceOrder }}

    @@ -205,6 +214,7 @@ export default { sleepTimerLength: 900000, // 15 minutes disableSleepTimerFadeOut: false, disableSleepTimerResetFeedback: false, + enableSleepTimerAlmostDoneChime: false, autoSleepTimerAutoRewind: false, autoSleepTimerAutoRewindTime: 300000, // 5 minutes languageCode: 'en-us', @@ -232,6 +242,10 @@ export default { name: this.$strings.LabelDisableVibrateOnReset, message: this.$strings.LabelDisableVibrateOnResetHelp }, + enableSleepTimerAlmostDoneChime: { + name: this.$strings.LabelSleepTimerAlmostDoneChime, + message: this.$strings.LabelSleepTimerAlmostDoneChimeHelp + }, autoSleepTimerAutoRewind: { name: this.$strings.LabelAutoSleepTimerAutoRewind, message: this.$strings.LabelAutoSleepTimerAutoRewindHelp @@ -547,6 +561,10 @@ export default { this.settings.disableSleepTimerResetFeedback = !this.settings.disableSleepTimerResetFeedback this.saveSettings() }, + toggleSleepTimerAlmostDoneChime() { + this.settings.enableSleepTimerAlmostDoneChime = !this.settings.enableSleepTimerAlmostDoneChime + this.saveSettings() + }, toggleDisableAutoRewind() { this.settings.disableAutoRewind = !this.settings.disableAutoRewind this.saveSettings() @@ -618,6 +636,7 @@ export default { this.settings.sleepTimerLength = !isNaN(deviceSettings.sleepTimerLength) ? deviceSettings.sleepTimerLength : 900000 // 15 minutes this.settings.disableSleepTimerFadeOut = !!deviceSettings.disableSleepTimerFadeOut this.settings.disableSleepTimerResetFeedback = !!deviceSettings.disableSleepTimerResetFeedback + this.settings.enableSleepTimerAlmostDoneChime = !!deviceSettings.enableSleepTimerAlmostDoneChime this.settings.autoSleepTimerAutoRewind = !!deviceSettings.autoSleepTimerAutoRewind this.settings.autoSleepTimerAutoRewindTime = !isNaN(deviceSettings.autoSleepTimerAutoRewindTime) ? deviceSettings.autoSleepTimerAutoRewindTime : 300000 // 5 minutes diff --git a/plugins/capacitor/AbsAudioPlayer.js b/plugins/capacitor/AbsAudioPlayer.js index cec8f6c7..43326165 100644 --- a/plugins/capacitor/AbsAudioPlayer.js +++ b/plugins/capacitor/AbsAudioPlayer.js @@ -1,4 +1,5 @@ import { registerPlugin, WebPlugin } from '@capacitor/core' +import { AbsLogger } from '@/plugins/capacitor' import { nanoid } from 'nanoid' const { PlayerState } = require('../constants') @@ -88,6 +89,9 @@ class AbsAudioPlayerWeb extends WebPlugin { 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() else this.player.pause() return { diff --git a/plugins/capacitor/AbsLogger.js b/plugins/capacitor/AbsLogger.js new file mode 100644 index 00000000..be65787a --- /dev/null +++ b/plugins/capacitor/AbsLogger.js @@ -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 } diff --git a/plugins/capacitor/index.js b/plugins/capacitor/index.js index 6a2db783..29b4ae87 100644 --- a/plugins/capacitor/index.js +++ b/plugins/capacitor/index.js @@ -2,12 +2,9 @@ import Vue from 'vue' import { AbsAudioPlayer } from './AbsAudioPlayer' import { AbsDownloader } from './AbsDownloader' import { AbsFileSystem } from './AbsFileSystem' +import { AbsLogger } from './AbsLogger' import { Capacitor } from '@capacitor/core' Vue.prototype.$platform = Capacitor.getPlatform() -export { - AbsAudioPlayer, - AbsDownloader, - AbsFileSystem -} \ No newline at end of file +export { AbsAudioPlayer, AbsDownloader, AbsFileSystem, AbsLogger } diff --git a/plugins/init.client.js b/plugins/init.client.js index 14ce222c..fe20ad40 100644 --- a/plugins/init.client.js +++ b/plugins/init.client.js @@ -16,6 +16,12 @@ if (Capacitor.getPlatform() != 'web') { await StatusBar.setStyle({ style: Style.Dark }) } setStatusBarStyleDark() + + const setStatusBarOverlays = async () => { + // Defaults to true in capacitor v7 + await StatusBar.setOverlaysWebView({ overlay: false }) + } + setStatusBarOverlays() } Vue.prototype.$showHideStatusBar = async (show) => { @@ -170,7 +176,6 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => { .replace(windowsReservedRe, replacement) .replace(windowsTrailingRe, replacement) - if (sanitized.length > MAX_FILENAME_LEN) { var lenToRemove = sanitized.length - MAX_FILENAME_LEN var ext = Path.extname(sanitized) @@ -187,8 +192,7 @@ function xmlToJson(xml) { for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) { const key = res[1] || res[3] const value = res[2] && xmlToJson(res[2]) - json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null - + json[key] = (value && Object.keys(value).length ? value : res[2]) || null } return json } @@ -230,14 +234,15 @@ Vue.prototype.$sanitizeSlug = (str) => { str = str.toLowerCase() // remove accents, swap ñ for n, etc - var from = "àáäâèéëêìíïîòóöôùúüûñçěščřžýúůďťň·/,:;" - var to = "aaaaeeeeiiiioooouuuuncescrzyuudtn-----" + var from = 'àáäâèéëêìíïîòóöôùúüûñçěščřžýúůďťň·/,:;' + var to = 'aaaaeeeeiiiioooouuuuncescrzyuudtn-----' for (var i = 0, l = from.length; i < l; i++) { str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) } - str = str.replace('.', '-') // replace a dot by a dash + str = str + .replace('.', '-') // replace a dot by a dash .replace(/[^a-z0-9 -_]/g, '') // remove invalid chars .replace(/\s+/g, '-') // collapse whitespace and replace by a dash .replace(/-+/g, '-') // collapse dashes @@ -291,7 +296,7 @@ export default ({ store, app }, inject) => { if (!canGoBack) { const { value } = await Dialog.confirm({ title: 'Confirm', - message: `Did you want to exit the app?`, + message: `Did you want to exit the app?` }) if (value) { App.exitApp() @@ -310,7 +315,4 @@ export default ({ store, app }, inject) => { }) } -export { - encode, - decode -} +export { encode, decode } diff --git a/plugins/server.js b/plugins/server.js index 504d29f0..409ebe2d 100644 --- a/plugins/server.js +++ b/plugins/server.js @@ -10,6 +10,8 @@ class ServerSocket extends EventEmitter { this.connected = false this.serverAddress = null this.token = null + + this.lastReconnectAttemptTime = 0 } $on(evt, callback) { @@ -35,8 +37,8 @@ class ServerSocket extends EventEmitter { const socketOptions = { transports: ['websocket'], upgrade: false, - path: `${serverPath}/socket.io` - // reconnectionAttempts: 3 + path: `${serverPath}/socket.io`, + reconnectionDelayMax: 15000 } this.socket = io(serverHost, socketOptions) this.setSocketListeners() @@ -54,6 +56,9 @@ class ServerSocket extends EventEmitter { this.socket.on('user_updated', this.onUserUpdated.bind(this)) this.socket.on('user_item_progress_updated', this.onUserItemProgressUpdated.bind(this)) this.socket.on('playlist_added', this.onPlaylistAdded.bind(this)) + this.socket.io.on('reconnect_attempt', this.onReconnectAttempt.bind(this)) + this.socket.io.on('reconnect_error', this.onReconnectError.bind(this)) + this.socket.io.on('reconnect_failed', this.onReconnectFailed.bind(this)) } removeListeners() { @@ -72,6 +77,20 @@ class ServerSocket extends EventEmitter { this.socket.emit('auth', this.token) // Required to connect a user with their socket } + onReconnectAttempt(attemptNumber) { + const timeSinceLastReconnectAttempt = this.lastReconnectAttemptTime ? Date.now() - this.lastReconnectAttemptTime : 0 + this.lastReconnectAttemptTime = Date.now() + console.log(`[SOCKET] Reconnect attempt ${attemptNumber} ${timeSinceLastReconnectAttempt > 0 ? `after ${timeSinceLastReconnectAttempt}ms` : ''}`) + } + + onReconnectError(error) { + console.log('[SOCKET] Reconnect error', error) + } + + onReconnectFailed(error) { + console.log('[SOCKET] Reconnect failed', error) + } + onDisconnect(reason) { console.log('[SOCKET] Socket Disconnected: ' + reason) this.connected = false diff --git a/static/fonts/MaterialIcons-Regular.ttf b/static/fonts/MaterialIcons-Regular.ttf deleted file mode 100644 index 9d09b0fe..00000000 Binary files a/static/fonts/MaterialIcons-Regular.ttf and /dev/null differ diff --git a/static/fonts/MaterialIconsOutlined-Regular.otf b/static/fonts/MaterialIconsOutlined-Regular.otf deleted file mode 100644 index 9dad12bc..00000000 Binary files a/static/fonts/MaterialIconsOutlined-Regular.otf and /dev/null differ diff --git a/static/fonts/MaterialSymbolsRounded.woff2 b/static/fonts/MaterialSymbolsRounded.woff2 new file mode 100644 index 00000000..88a9a81f Binary files /dev/null and b/static/fonts/MaterialSymbolsRounded.woff2 differ diff --git a/strings/ar.json b/strings/ar.json index 8dc38287..44c68dde 100644 --- a/strings/ar.json +++ b/strings/ar.json @@ -20,24 +20,26 @@ "ButtonDisableAutoTimer": "تعطيل المؤقت التلقائي", "ButtonDisconnect": "فصل", "ButtonGoToWebClient": "اذهب إلى عميل الويب", - "ButtonHistory": "تاريخ", + "ButtonHistory": "السجل التاريخي", "ButtonHome": "الرئيسية", "ButtonIssues": "مشاكل", - "ButtonLatest": "أحدث", + "ButtonLatest": "الأحدث", "ButtonLibrary": "المكتبة", - "ButtonLocalMedia": "ملفات الوسائط المحلية", + "ButtonLocalMedia": "الوسائط المحلية", + "ButtonLogs": "سجلات", "ButtonManageLocalFiles": "إدارة الملفات المحلية", "ButtonNewFolder": "مجلد جديد", "ButtonNextEpisode": "الحلقة التالية", + "ButtonOk": "موافق", "ButtonOpenFeed": "فتح التغذية", "ButtonOverride": "تجاوز", - "ButtonPause": "تَوَقَّف", + "ButtonPause": "إيقاف مؤقت", "ButtonPlay": "تشغيل", - "ButtonPlayEpisode": "شغل الحلقة", + "ButtonPlayEpisode": "تشغيل الحلقة", "ButtonPlaylists": "قوائم التشغيل", "ButtonRead": "اقرأ", - "ButtonReadLess": "قلص", - "ButtonReadMore": "المزيد", + "ButtonReadLess": "اقرأ أقل", + "ButtonReadMore": "اقرأ أكثر", "ButtonRemove": "إزالة", "ButtonRemoveFromServer": "إزالة من الخادم", "ButtonSave": "حفظ", @@ -47,21 +49,135 @@ "ButtonSeries": "سلسلة", "ButtonSetTimer": "اضبط مؤقت", "ButtonStream": "بث", - "ButtonSubmit": "تقديم", + "ButtonSubmit": "إرسال", "ButtonSwitchServerUser": "تبديل الخادم/المستخدم", "ButtonUserStats": "إحصائيات المستخدم", "ButtonYes": "نعم", "HeaderAccount": "الحساب", "HeaderAdvanced": "متقدم", - "HeaderAudioTracks": "المسارات الصوتية", + "HeaderAndroidAutoSettings": "إعدادات أندرويد للسيارة", + "HeaderAudioTracks": "المقاطع الصوتية", "HeaderChapters": "الفصول", "HeaderCollection": "مجموعة", "HeaderCollectionItems": "عناصر المجموعة", + "HeaderConnectionStatus": "حالة الاتصال", "HeaderDataSettings": "إعدادات البيانات", "HeaderDetails": "التفاصيل", + "HeaderDownloads": "التنزيلات", "HeaderEbookFiles": "ملفات الكتب الإلكترونية", "HeaderEpisodes": "الحلقات", "HeaderEreaderSettings": "إعدادات القارئ الإلكتروني", "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": "متوسط" } diff --git a/strings/be.json b/strings/be.json index fd4fe123..d9ddbe4c 100644 --- a/strings/be.json +++ b/strings/be.json @@ -3,7 +3,7 @@ "ButtonAddNewServer": "Дадаць новы сервер", "ButtonAuthors": "Аўтары", "ButtonBack": "Назад", - "ButtonCancel": "Адмяніць", + "ButtonCancel": "Скасаваць", "ButtonCancelTimer": "Скасаваць таймер", "ButtonClearFilter": "Ачысціць фільтр", "ButtonCloseFeed": "Закрыць стужку", @@ -12,7 +12,7 @@ "ButtonConnectToServer": "Падлучыцца да сервера", "ButtonCreate": "Ствараць", "ButtonCreateBookmark": "Стварыць закладку", - "ButtonCreateNewPlaylist": "Стварыць новы плэйліст", + "ButtonCreateNewPlaylist": "Стварыць новы спіс прайгравання", "ButtonDelete": "Выдаліць", "ButtonDeleteLocalEpisode": "Выдаліць лакальны эпізод", "ButtonDeleteLocalFile": "Выдаліць лакальны файл", @@ -35,7 +35,7 @@ "ButtonPause": "Паўза", "ButtonPlay": "Прайграць", "ButtonPlayEpisode": "Прайграць эпізод", - "ButtonPlaylists": "Плэйлісты", + "ButtonPlaylists": "Спісы прайгравання", "ButtonRead": "Чытаць", "ButtonReadLess": "Чытаць менш", "ButtonReadMore": "Чытаць больш", @@ -70,16 +70,16 @@ "HeaderLibraries": "Бібліятэкі", "HeaderLocalFolders": "Лакальныя тэчкі", "HeaderLocalLibraryItems": "Элементы лакальнай бібліятэкі", - "HeaderNewPlaylist": "Новы плэйліст", + "HeaderNewPlaylist": "Новы спіс прайгравання", "HeaderOpenRSSFeed": "Адкрыць RSS-стужку", "HeaderPlaybackSettings": "Налады прайгравання", - "HeaderPlaylist": "Плэйліст", - "HeaderPlaylistItems": "Элементы плэйліста", + "HeaderPlaylist": "Спіс прайгравання", + "HeaderPlaylistItems": "Элементы спіса прайгравання", "HeaderProgressSyncFailed": "Не ўдалося сінхранізаваць прагрэс", "HeaderRSSFeed": "RSS-стужка", "HeaderRSSFeedGeneral": "Падрабязнасці RSS", "HeaderRSSFeedIsOpen": "RSS-стужка адкрыта", - "HeaderSelectDownloadLocation": "Абраць месца для спампоўкі", + "HeaderSelectDownloadLocation": "Абраць месцазнаходжання для спампоўкі", "HeaderSettings": "Налады", "HeaderSleepTimer": "Таймер сну", "HeaderSleepTimerSettings": "Налады таймера сну", @@ -88,7 +88,7 @@ "HeaderTableOfContents": "Змест", "HeaderUserInterfaceSettings": "Налады інтэрфейсу карыстальніка", "HeaderYourStats": "Ваша статыстыка", - "LabelAddToPlaylist": "Дадаць у плэйліст", + "LabelAddToPlaylist": "Дадаць у спіс прайгравання", "LabelAddedAt": "Дата дабаўлення", "LabelAddedDate": "Дададзена {0}", "LabelAll": "Усе", @@ -223,7 +223,7 @@ "LabelReadAgain": "Чытаць зноў", "LabelRecentSeries": "Апошнія серыі", "LabelRecentlyAdded": "Нядаўна дададзеныя", - "LabelRemoveFromPlaylist": "Выдаліць з плэйліста", + "LabelRemoveFromPlaylist": "Выдаліць з спіса прайгравання", "LabelScaleElapsedTimeBySpeed": "Прыстасаваць мінулы час да хуткасці", "LabelSeason": "Сезон", "LabelSelectADevice": "Выбраць прыладу", @@ -314,7 +314,7 @@ "MessageNoPodcastsFound": "Падкасты не знойдзены", "MessageNoSeries": "Няма серый", "MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся", - "MessageNoUserPlaylists": "У вас няма плэйлістоў", + "MessageNoUserPlaylists": "У вас няма спісаў прайгравання", "MessageOldServerConnectionWarning": "Канфігурацыя падлучэння да сервера выкарыстоўвае стары ідэнтыфікатар карыстальніка. Калі ласка, выдаліце і зноў дадайце гэта падлучэнне да сервера.", "MessageOldServerConnectionWarningHelp": "Вы першапачаткова наладзілі падключэнне да гэтага сервера да міграцыі базы даных у версіі 2.3.0, якая выйшла ў чэрвені 2023 года. У будучым абнаўленні сервера магчымасць уваходу праз гэтае старое падключэнне будзе выдалена. Калі ласка, выдаліце існуючае падключэнне да сервера і падключыцеся зноў (выкарыстоўваючы той жа адрас сервера і ўліковыя даныя). Калі на гэтай прыладзе ёсць спампаваныя медыяфайлы, іх трэба будзе спампаваць зноў для сінхранізацыі з серверам.", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", @@ -336,7 +336,7 @@ "ToastDownloadNotAllowedOnCellular": "Спампоўванне забаронена праз мабільны інтэрнэт", "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана", "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае", - "ToastPlaylistCreateFailed": "Не ўдалося стварыць плэйліст", + "ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання", "ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст", "ToastPodcastCreateSuccess": "Падкаст паспяхова створаны", "ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку", diff --git a/strings/ca.json b/strings/ca.json index 4411e421..18dfba34 100644 --- a/strings/ca.json +++ b/strings/ca.json @@ -29,6 +29,7 @@ "ButtonManageLocalFiles": "Gestionar fitxers locals", "ButtonNewFolder": "Nova carpeta", "ButtonNextEpisode": "Pròxim episodi", + "ButtonOk": "D’acord", "ButtonOpenFeed": "Obrir font", "ButtonOverride": "Substituir", "ButtonPause": "Pausa", @@ -53,50 +54,55 @@ "ButtonYes": "Sí", "HeaderAccount": "Compte", "HeaderAdvanced": "Avançat", + "HeaderAndroidAutoSettings": "Paràmetres de l'Android Auto", "HeaderAudioTracks": "Pistes d'àudio", "HeaderChapters": "Capítols", "HeaderCollection": "Col·lecció", "HeaderCollectionItems": "Elements de la col·lecció", "HeaderConnectionStatus": "Estat de la connexió", - "HeaderDataSettings": "Configuració de dades", + "HeaderDataSettings": "Paràmetres de dades", "HeaderDetails": "Detalls", - "HeaderDownloads": "Descàrregues", - "HeaderEbookFiles": "Fitxers de llibres electrònics", + "HeaderDownloads": "Baixades", + "HeaderEbookFiles": "Fitxers de llibres digitals", "HeaderEpisodes": "Episodis", - "HeaderEreaderSettings": "Configuració del lector", + "HeaderEreaderSettings": "Paràmetres del lector", "HeaderLatestEpisodes": "Últims episodis", "HeaderLibraries": "Biblioteques", "HeaderLocalFolders": "Carpetes locals", "HeaderLocalLibraryItems": "Elements de la biblioteca local", - "HeaderNewPlaylist": "Nova llista de reproducció", + "HeaderNewPlaylist": "Llista de reproducció nova", "HeaderOpenRSSFeed": "Obrir font RSS", - "HeaderPlaybackSettings": "Configuració de reproducció", + "HeaderPlaybackSettings": "Paràmetres de reproducció", "HeaderPlaylist": "Llista de reproducció", "HeaderPlaylistItems": "Elements de la llista de reproducció", + "HeaderProgressSyncFailed": "Sincronització del progrés fallida", "HeaderRSSFeed": "Font RSS", "HeaderRSSFeedGeneral": "Detalls RSS", "HeaderRSSFeedIsOpen": "Font RSS oberta", "HeaderSelectDownloadLocation": "Seleccionar ubicació de descàrrega", - "HeaderSettings": "Configuració", + "HeaderSettings": "Paràmetres", "HeaderSleepTimer": "Temporitzador de desconnexió", - "HeaderSleepTimerSettings": "Configuració del temporitzador de desconnexió", - "HeaderStatsMinutesListeningChart": "Minuts escoltant (Últims 7 dies)", + "HeaderSleepTimerSettings": "Paràmetres del temporitzador de desconnexió", + "HeaderStatsMinutesListeningChart": "Minuts escoltant (últims 7 dies)", "HeaderStatsRecentSessions": "Sessions recents", - "HeaderTableOfContents": "Taula de continguts", - "HeaderUserInterfaceSettings": "Configuració de la interfície d'usuari", - "HeaderYourStats": "Les teves estadístiques", + "HeaderTableOfContents": "Sumari", + "HeaderUserInterfaceSettings": "Paràmetres de la interfície d'usuari", + "HeaderYourStats": "Les vostres estadístiques", "LabelAddToPlaylist": "Afegit a la llista de reproducció", "LabelAddedAt": "Afegit a", "LabelAddedDate": "Afegits {0}", "LabelAll": "Tots", "LabelAllowSeekingOnMediaControls": "Permetre la cerca en controls de mitjans", "LabelAlways": "Sempre", - "LabelAskConfirmation": "Demanar confirmació", + "LabelAndroidAutoBrowseLimitForGrouping": "Límit del desplegament alfabètic", + "LabelAndroidAutoBrowseLimitForGroupingHelp": "No utilitzeu el desplegament alfabètic quan hi hagi menys d'aquest nombre d'elements a mostrar", + "LabelAndroidAutoBrowseSeriesSequenceOrder": "Ordre dels llibres de la sèrie", + "LabelAskConfirmation": "Demana confirmació", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (Nom Cognom)", "LabelAuthorLastFirst": "Autor (Cognom, Nom)", "LabelAuthors": "Autors", - "LabelAutoDownloadEpisodes": "Descarregar episodis automàticament", + "LabelAutoDownloadEpisodes": "Baixa episodis automàticament", "LabelAutoRewindTime": "Temps de rebobinatge automàtic", "LabelAutoSleepTimer": "Temporitzador de desconnexió automàtic", "LabelAutoSleepTimerAutoRewind": "Temporitzador automàtic amb rebobinatge", @@ -105,7 +111,7 @@ "LabelBooks": "Llibres", "LabelChapterTrack": "Seguiment de capítol", "LabelChapters": "Capítols", - "LabelClosePlayer": "Tancar reproductor", + "LabelClosePlayer": "Tanca el reproductor", "LabelCollapseSeries": "Reduir sèrie", "LabelComplete": "Complet", "LabelContinueBooks": "Continuar llibres", @@ -119,52 +125,88 @@ "LabelDisableAudioFadeOutHelp": "El volum de l'audio començara a reduïr-se quan quede menys d'1 minut del temporitzador d'anar a dormir. Habilita este ajustament per a no reduïr el volum.", "LabelDisableAutoRewind": "Desactivar rebobinatge automàtic", "LabelDisableShakeToReset": "Desactivar sacsejar per reiniciar", + "LabelDisableShakeToResetHelp": "Si sacseges el teu dispositiu quan el temporitzador és actiu o durant els 2 minuts després que hagi expirat, el temporitzador es reinicia. Activa aquesta opció per evitar-ho.", + "LabelDisableVibrateOnReset": "Desactivar la vibració al reiniciar", "LabelDiscover": "Descobrir", "LabelDownload": "Descarregar", + "LabelDownloadUsingCellular": "Descarregar utilitzant dades mòbils", "LabelDownloaded": "Descarregat", "LabelDuration": "Durada", "LabelEbook": "Llibre electrònic", "LabelEbooks": "Llibres electrònics", "LabelEnable": "Activar", + "LabelEnableMp3IndexSeeking": "Activar cerca d'index en mp3", "LabelEnd": "Final", "LabelEndOfChapter": "Final del capítol", + "LabelEndTime": "Hora de finalització", "LabelEpisode": "Episodi", "LabelFeedURL": "Font URL", "LabelFile": "Fitxer", + "LabelFileModified": "Fitxer modificat", + "LabelFilename": "Nom del fitxer", "LabelFinished": "Acabat", "LabelFolder": "Carpeta", + "LabelFontBoldness": "Gruix de la lletra", + "LabelFontScale": "Escala de la lletra", "LabelGenre": "Gènere", "LabelGenres": "Gèneres", - "LabelHapticFeedback": "Retroalimentació tàctil", + "LabelHapticFeedback": "Resposta hàptica", + "LabelHasEbook": "Té llibre electrònic", + "LabelHasSupplementaryEbook": "Té llibre electrònic suplementari", "LabelHigh": "Alt", + "LabelHost": "Amfitrió", "LabelInProgress": "En procés", "LabelIncomplete": "Incomplet", + "LabelInternalAppStorage": "Emmagatzematge intern de l'aplicació", "LabelJumpBackwardsTime": "Saltar enrere", - "LabelLanguage": "Idioma", - "LabelLayout": "Disseny", + "LabelLanguage": "Llengua", + "LabelLayout": "Disposició", + "LabelLayoutAuto": "Automàtic", + "LabelLayoutSinglePage": "Pàgina única", + "LabelLight": "Clar", + "LabelLineSpacing": "Interlineat", "LabelListenAgain": "Escoltar de nou", + "LabelLocalBooks": "Llibres locals", + "LabelLocalPodcasts": "Pòdcasts locals", + "LabelLockOrientation": "Bloca l'orientació", + "LabelLockPlayer": "Bloca el reproductor", + "LabelMediaType": "Tipus de mitjà", "LabelMedium": "Mitjà", "LabelMore": "Més", + "LabelMoreInfo": "Més informació", "LabelName": "Nom", "LabelNarrator": "Narrador", + "LabelNarrators": "Narradors", + "LabelNavigateWithVolume": "Navega amb les tecles de volum", + "LabelNavigateWithVolumeWhilePlayingDisabled": "⭘", + "LabelNavigateWithVolumeWhilePlayingEnabled": "⏽", "LabelNever": "Mai", "LabelNewestAuthors": "Autors més recents", "LabelNewestEpisodes": "Episodis més recents", + "LabelNo": "No", "LabelNotFinished": "No acabat", "LabelNotStarted": "No iniciat", + "LabelNumEpisodes": "{0} episodis", + "LabelNumberOfEpisodes": "Nre. d'episodis", "LabelOff": "Apagat", "LabelPassword": "Contrasenya", - "LabelPath": "Ruta de carpeta", + "LabelPath": "Camí", "LabelPlaybackSpeed": "Velocitat de reproducció", - "LabelPodcast": "Podcast", - "LabelPodcasts": "Podcasts", + "LabelPodcast": "Pòdcast", + "LabelPodcasts": "Pòdcasts", "LabelProgress": "Progrés", "LabelPubDate": "Data de publicació", "LabelPublishYear": "Any de publicació", + "LabelRandomly": "A l'atzar", "LabelRead": "Llegit", "LabelReadAgain": "Tornar a llegir", + "LabelRecentSeries": "Sèries recents", + "LabelRecentlyAdded": "Addicions recents", "LabelRemoveFromPlaylist": "Eliminar de la llista de reproducció", + "LabelSelectADevice": "Seleccioneu un dispositiu", "LabelSeries": "Sèries", + "LabelServerAddress": "Adreça del servidor", + "LabelShowAll": "Mostra-ho tot", "LabelSize": "Mida", "LabelSleepTimer": "Temporitzador de desconnexió", "LabelStart": "Iniciar", @@ -174,22 +216,53 @@ "LabelStatsItemsFinished": "Elements acabats", "LabelStatsMinutes": "Minuts", "LabelStatsWeekListening": "Temps escoltant setmanal", + "LabelTag": "Etiqueta", "LabelTags": "Etiquetes", "LabelTheme": "Tema", + "LabelThemeBlack": "Negre", + "LabelThemeDark": "Fosc", + "LabelThemeLight": "Clar", "LabelTimeRemaining": "{0} restant", "LabelTitle": "Títol", + "LabelTotalSize": "Mida total", "LabelTracks": "Pistes", "LabelType": "Tipus", + "LabelUnknown": "Desconegut", "LabelUnlockPlayer": "Desbloquejar reproductor", "LabelUseBookshelfView": "Usar vista de prestatgeria", "LabelUser": "Usuari", + "LabelUsername": "Nom d'usuari", "LabelVeryHigh": "Molt alt", "LabelVeryLow": "Molt baix", - "LabelYourBookmarks": "Els teus marcadors", - "LabelYourProgress": "El teu progrés", - "MessageConfirmDeleteLocalEpisode": "Eliminar episodi local \"{0}\" del dispositiu?", - "MessageDownloadCompleteProcessing": "Descarrega completada. Processant...", + "LabelYourBookmarks": "Els vostres marcadors", + "LabelYourProgress": "El vostre progrés", + "MessageConfirmDeleteLocalEpisode": "Voleu eliminar l'episodi local «{0}» del dispositiu? No s'afectarà el fitxer al servidor.", + "MessageConfirmDiscardProgress": "Segur que voleu reinicialitzar el vostre progrés?", + "MessageDiscardProgress": "Descarta el progrés", + "MessageDownloadCompleteProcessing": "S'ha completat la baixada. S'està processant...", + "MessageDownloading": "S'està baixant...", + "MessageDownloadingEpisode": "S'està baixant l'episodi", + "MessageFetching": "S'està recuperant...", + "MessageFollowTheProjectOnGithub": "Seguiu el projecte al GitHub", + "MessageLoading": "S'està carregant...", + "MessageLoadingServerData": "S'estan carregant les dades del servidor...", "MessageMarkAsFinished": "Marcar com acabat", "MessageMediaLinkedToServer": "Enllaçat al servidor {0}", - "MessageNoItemsFound": "Cap element trobat" + "MessageMediaLinkedToThisServer": "La multimèdia baixada està enllaçada a aquest servidor", + "MessageNoChapters": "Cap capítol", + "MessageNoCollections": "Cap col·lecció", + "MessageNoItems": "Cap element", + "MessageNoItemsFound": "Cap element trobat", + "MessageNoListeningSessions": "Cap sessió d'escolta", + "MessageNoMediaFolders": "Cap carpeta multimèdia", + "MessageNoNetworkConnection": "Cap connexió de xarxa", + "MessageNoPodcastsFound": "No s'ha trobat cap pòdcast", + "MessageNoSeries": "Cap sèrie", + "MessageNoUpdatesWereNecessary": "No calia cap actualització", + "MessageNoUserPlaylists": "No teniu cap llista de reproducció", + "ToastItemMarkedAsFinishedFailed": "No s'ha pogut marcar com a finalitzat", + "ToastItemMarkedAsNotFinishedFailed": "No s'ha pogut marcar com a no finalitzat", + "ToastPlaylistCreateFailed": "No s'ha pogut crear la llista de reproducció", + "ToastPodcastCreateFailed": "No s'ha pogut crear el pòdcast", + "ToastPodcastCreateSuccess": "S'ha creat el pòdcast correctament" } diff --git a/strings/cs.json b/strings/cs.json index 387dc1bd..289aa5a7 100644 --- a/strings/cs.json +++ b/strings/cs.json @@ -26,6 +26,7 @@ "ButtonLatest": "Nejnovější", "ButtonLibrary": "Knihovna", "ButtonLocalMedia": "Místní média", + "ButtonLogs": "Záznamy", "ButtonManageLocalFiles": "Spravovat místní soubory", "ButtonNewFolder": "Nová složka", "ButtonNextEpisode": "Další epizoda", @@ -66,7 +67,7 @@ "HeaderEbookFiles": "Soubory e-knih", "HeaderEpisodes": "Epizody", "HeaderEreaderSettings": "Nastavení čtečky e-knih", - "HeaderLatestEpisodes": "Nejnovější epizody", + "HeaderLatestEpisodes": "Nové epizody", "HeaderLibraries": "Knihovny", "HeaderLocalFolders": "Místní složky", "HeaderLocalLibraryItems": "Místní položky knihovny", @@ -75,7 +76,7 @@ "HeaderPlaybackSettings": "Nastavení přehrávání", "HeaderPlaylist": "Seznam skladeb", "HeaderPlaylistItems": "Položky seznamu přehrávání", - "HeaderProgressSyncFailed": "Chyba při synchronizaci", + "HeaderProgressSyncFailed": "Chyba při synchronizaci pokroku", "HeaderRSSFeed": "RSS kanál", "HeaderRSSFeedGeneral": "Detaily RSS", "HeaderRSSFeedIsOpen": "Kanál RSS je otevřen", @@ -94,6 +95,9 @@ "LabelAll": "Vše", "LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií", "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í", "LabelAuthor": "Autor", "LabelAuthorFirstLast": "Autor (jméno a příjmení)", @@ -134,7 +138,7 @@ "LabelEbooks": "E-knihy", "LabelEnable": "Povolit", "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", "LabelEndOfChapter": "Konec kapitoly", "LabelEndTime": "Do", @@ -161,6 +165,7 @@ "LabelInternalAppStorage": "Interní úložiště aplikace", "LabelJumpBackwardsTime": "Délka skoku zpět v čase", "LabelJumpForwardsTime": "Délka skoku vpřed v čase", + "LabelKeepScreenAwake": "Nezhasínat obrazovku", "LabelLanguage": "Jazyk", "LabelLayout": "Rozvržení", "LabelLayoutAuto": "Automatické", @@ -175,6 +180,7 @@ "LabelLow": "Nízké", "LabelMediaType": "Typ média", "LabelMedium": "Střední", + "LabelMissing": "Chybějící", "LabelMore": "Více", "LabelMoreInfo": "Více informací", "LabelName": "Jméno", @@ -192,6 +198,8 @@ "LabelNotFinished": "Nedokončeno", "LabelNotStarted": "Nezahájeno", "LabelNumEpisodes": "{0} epizod", + "LabelNumEpisodesIncomplete": "{0} epizod, {1} nekompletní", + "LabelNumberOfEpisodes": "Počet epizod", "LabelOff": "Vypnout", "LabelOn": "Zapnuto", "LabelPassword": "Heslo", @@ -220,6 +228,8 @@ "LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti", "LabelSeason": "Sezóna", "LabelSelectADevice": "Vyberte zařízení", + "LabelSequenceAscending": "Řadit vzestupně", + "LabelSequenceDescending": "Řadit sestupně", "LabelSeries": "Série", "LabelServerAddress": "Adresa serveru", "LabelSetEbookAsPrimary": "Nastavit jako primární", @@ -243,6 +253,7 @@ "LabelTag": "Štítek", "LabelTags": "Štítky", "LabelTheme": "Téma", + "LabelThemeBlack": "Černé", "LabelThemeDark": "Tmavé", "LabelThemeLight": "Světlé", "LabelTimeRemaining": "{0} zbývá", @@ -269,6 +280,7 @@ "MessageBookshelfEmpty": "Knihovna je prázdná", "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.", + "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?", "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?", @@ -283,8 +295,10 @@ "MessageFetching": "Načítání...", "MessageFollowTheProjectOnGithub": "Sledujte projekt na GitHubu", "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...", "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é", "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.", @@ -303,7 +317,10 @@ "MessageNoSeries": "Žádné série", "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", "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", + "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", "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}?", diff --git a/strings/da.json b/strings/da.json index 691a9ab3..5deb0060 100644 --- a/strings/da.json +++ b/strings/da.json @@ -252,6 +252,7 @@ "LabelTag": "Mærke", "LabelTags": "Mærker", "LabelTheme": "Tema", + "LabelThemeBlack": "Sort", "LabelThemeDark": "Mørk", "LabelThemeLight": "Lys", "LabelTimeRemaining": "{0} tilbage", @@ -296,6 +297,7 @@ "MessageItemMissing": "Genstand mangler og skal rettes på serveren. En genstand markeres typisk som manglende hvis filstier ikke er tilgængelige.", "MessageLoading": "Indlæser...", "MessageLoadingServerData": "Indlæser server information...", + "MessageLocalFolderDescription": "'Internt App lager' er kun tilgængelig for denne app. Appen har kun support for medie hentet direkte gennem appen. Delte mapper kan anvendes af andre apps for at få adgang til medie hentet igennem denne app.", "MessageMarkAsFinished": "Markér som afsluttet", "MessageMediaLinkedToADifferentServer": "Mediet er tilsluttet en anden Audiobookshelf server på en anden adresse ({0}). Fremgang vil blive synkroniseret, når denne server adresse bliver tilsluttet.", "MessageMediaLinkedToADifferentUser": "Mediet er tilsluttet til denne server, men var downloadet af en anden bruger. Fremgang vil blive synkroniseret, til den bruger der har downloadet dette.", diff --git a/strings/de.json b/strings/de.json index 9ca7caf2..c5ad6d71 100644 --- a/strings/de.json +++ b/strings/de.json @@ -26,6 +26,7 @@ "ButtonLatest": "Neueste", "ButtonLibrary": "Bibliothek", "ButtonLocalMedia": "Lokale Medien", + "ButtonLogs": "Protokolle", "ButtonManageLocalFiles": "Verwalte lokale Dateien", "ButtonNewFolder": "Neuer Ordner", "ButtonNextEpisode": "Nächste Episode", @@ -95,6 +96,7 @@ "LabelAllowSeekingOnMediaControls": "Erlaube Vor- und Zurückspulen auf dem Medienkontrollelement bei den Benachrichtigungen", "LabelAlways": "Immer", "LabelAndroidAutoBrowseLimitForGrouping": "Alphabetisches Drawdown-Limit", + "LabelAndroidAutoBrowseLimitForGroupingHelp": "Verwenden Sie keine alphabetische Auflistung, wenn weniger als diese Anzahl von Artikeln angezeigt werden soll", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Reihenfolge der Bücher der Serie", "LabelAskConfirmation": "Bestätigung anfordern", "LabelAuthor": "Autor", @@ -251,6 +253,7 @@ "LabelTag": "Schlagwort", "LabelTags": "Schlagwörter", "LabelTheme": "Farbschema", + "LabelThemeBlack": "Schwarz", "LabelThemeDark": "Dunkel", "LabelThemeLight": "Hell", "LabelTimeRemaining": "{0} verbleibend", @@ -277,6 +280,7 @@ "MessageBookshelfEmpty": "Bücherregal leer", "MessageConfirmDeleteLocalEpisode": "Soll die lokale Episode \"{0}\" von deinem Gerät entfernt werden? Die Datei auf dem Server bleibt davon unberührt.", "MessageConfirmDeleteLocalFiles": "Sollen lokale Dateien dieses Elements von deinem Gerät entfernt werden? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.", + "MessageConfirmDisableAutoTimer": "Bist du sicher, dass du den automatischen Timer für den Rest des Tages deaktivieren möchtest? Der Timer wird am Ende dieser automatischen Schlaf-Timer-Periode oder beim Neustart der App wieder aktiviert.", "MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?", "MessageConfirmDownloadUsingCellular": "Du bist dabei, über mobile Daten herunterzuladen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?", "MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?", @@ -291,8 +295,10 @@ "MessageFetching": "Wird abgerufen …", "MessageFollowTheProjectOnGithub": "Folge dem Projekt auf GitHub", "MessageItemDownloadCompleteFailedToCreate": "Element-Download abgeschlossen, aber Bibliothekselement kann nicht erstellt werden", + "MessageItemMissing": "Die Datei fehlt und muss auf dem Server korrigiert werden. Typischerweise wird eine Datei als fehlend markiert, weil die Dateipfade nicht zugänglich sind.", "MessageLoading": "Wird geladen …", "MessageLoadingServerData": "Lade Server Daten...", + "MessageLocalFolderDescription": "Der \"Interne App-Speicher\" ist nur für diese App zugänglich. Diese App unterstützt nur Medien, die direkt über die App heruntergeladen werden. Gemeinsame Speicherordner können verwendet werden, um anderen Apps den Zugriff auf die von dieser App heruntergeladenen Medien zu ermöglichen.", "MessageMarkAsFinished": "Als beendet markieren", "MessageMediaLinkedToADifferentServer": "Die Medien sind mit einem Audiobookshelf-Server mit einer anderen Adresse ({0}) verknüpft. Der Fortschritt wird synchronisiert, wenn eine Verbindung zu dieser Serveradresse besteht.", "MessageMediaLinkedToADifferentUser": "Das Medium ist mit diesem Server verknüpft, wurde aber von einem anderen Benutzer heruntergeladen. Die Fortschritte werden nur mit dem Benutzer synchronisiert, der sie heruntergeladen hat.", @@ -312,7 +318,7 @@ "MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig", "MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden", "MessageOldServerConnectionWarning": "Diese Serververbindung nutzt eine alte Nutzer-ID (user ID). Bitte entferne diese Serververbindung und füge sie wieder neu hinzu.", - "MessageOldServerConnectionWarningHelp": "Du hast diese Serververbindung vor der Datenbankmigration in 2.3.0, veröffentlicht Juni 2023, eingerichtet. Ein zukünftiges Serverupdate wird die Möglichkeit sich mit dieser Verbindung anzumelden entfernen. Bitte lösche die existierende Serververbindung und melde dich neu an (mit der gleichen Serveraddresse und Zugangsdaten. Wenn du irgendwelche Medien auf dieses Gerät heruntergeladen hast musst du sie anschließend erneut herunterladen um den Status zu synchronisieren.", + "MessageOldServerConnectionWarningHelp": "Du hast diese Serververbindung vor der Datenbankmigration in 2.3.0, veröffentlicht im Juni 2023, eingerichtet. Ein zukünftiges Serverupdate wird die Möglichkeit entfernen, sich mit dieser Verbindung anzumelden. Bitte lösche die existierende Serververbindung und melde dich neu an (mit der gleichen Serveradresse und Zugangsdaten). Wenn du irgendwelche Medien auf dieses Gerät heruntergeladen hast, musst du sie anschließend erneut herunterladen, um den Status zu synchronisieren.", "MessagePodcastSearchField": "Suchbegriff oder RSS-Feed URL eingeben", "MessageProgressSyncFailed": "Der letzte Versuch den aktuellen Hörfortschritt an den Server zu melden ist fehlgeschlagen. Die Anfragen den Fortschritt zu synchronisieren wird alle 15 Sekunden bis 1 Minute versucht, während Medien abgespielt werden.", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken", diff --git a/strings/en-us.json b/strings/en-us.json index d00a1e1f..25c742cd 100644 --- a/strings/en-us.json +++ b/strings/en-us.json @@ -6,6 +6,7 @@ "ButtonCancel": "Cancel", "ButtonCancelTimer": "Cancel Timer", "ButtonClearFilter": "Clear Filter", + "ButtonClearLogs": "Clear Logs", "ButtonCloseFeed": "Close Feed", "ButtonCollections": "Collections", "ButtonConnect": "Connect", @@ -26,7 +27,9 @@ "ButtonLatest": "Latest", "ButtonLibrary": "Library", "ButtonLocalMedia": "Local Media", + "ButtonLogs": "Logs", "ButtonManageLocalFiles": "Manage Local Files", + "ButtonMaskServerAddress": "Mask server address", "ButtonNewFolder": "New Folder", "ButtonNextEpisode": "Next Episode", "ButtonOk": "Ok", @@ -50,6 +53,7 @@ "ButtonStream": "Stream", "ButtonSubmit": "Submit", "ButtonSwitchServerUser": "Switch Server/User", + "ButtonUnmaskServerAddress": "Unmask server address", "ButtonUserStats": "User Stats", "ButtonYes": "Yes", "HeaderAccount": "Account", @@ -137,7 +141,7 @@ "LabelEbooks": "Ebooks", "LabelEnable": "Enable", "LabelEnableMp3IndexSeeking": "Enable mp3 index seeking", - "LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.", + "LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable bitrate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.", "LabelEnd": "End", "LabelEndOfChapter": "End of Chapter", "LabelEndTime": "End time", @@ -237,6 +241,8 @@ "LabelShowAll": "Show All", "LabelSize": "Size", "LabelSleepTimer": "Sleep timer", + "LabelSleepTimerAlmostDoneChime": "Play a chime when almost finished", + "LabelSleepTimerAlmostDoneChimeHelp": "Play a chime when the sleep timer has 30 seconds remaining", "LabelStart": "Start", "LabelStartTime": "Start time", "LabelStatsBestDay": "Best Day", @@ -252,6 +258,7 @@ "LabelTag": "Tag", "LabelTags": "Tags", "LabelTheme": "Theme", + "LabelThemeBlack": "Black", "LabelThemeDark": "Dark", "LabelThemeLight": "Light", "LabelTimeRemaining": "{0} remaining", @@ -309,6 +316,7 @@ "MessageNoItems": "No Items", "MessageNoItemsFound": "No items found", "MessageNoListeningSessions": "No Listening Sessions", + "MessageNoLogs": "No logs", "MessageNoMediaFolders": "No Media Folders", "MessageNoNetworkConnection": "No network connection", "MessageNoPodcastsFound": "No podcasts found", diff --git a/strings/es.json b/strings/es.json index 086b9df5..02c965d1 100644 --- a/strings/es.json +++ b/strings/es.json @@ -1,36 +1,36 @@ { - "ButtonAdd": "Agregar", - "ButtonAddNewServer": "Agregar nuevo servidor", + "ButtonAdd": "Añadir", + "ButtonAddNewServer": "Añadir servidor nuevo", "ButtonAuthors": "Autores", "ButtonBack": "Atrás", "ButtonCancel": "Cancelar", - "ButtonCancelTimer": "Cancelar el temporizador", + "ButtonCancelTimer": "Cancelar temporizador", "ButtonClearFilter": "Quitar filtros", - "ButtonCloseFeed": "Cerrar fuente", + "ButtonCloseFeed": "Cerrar suministro", "ButtonCollections": "Colecciones", "ButtonConnect": "Conectar", "ButtonConnectToServer": "Conectar al servidor", "ButtonCreate": "Crear", "ButtonCreateBookmark": "Crear marcador", - "ButtonCreateNewPlaylist": "Crear nueva lista de reproducción", + "ButtonCreateNewPlaylist": "Crear lista de reproducción nueva", "ButtonDelete": "Eliminar", - "ButtonDeleteLocalEpisode": "Borrar episodio local", - "ButtonDeleteLocalFile": "Borrar archivo local", - "ButtonDeleteLocalItem": "Borrar elemento local", + "ButtonDeleteLocalEpisode": "Eliminar episodio local", + "ButtonDeleteLocalFile": "Eliminar archivo local", + "ButtonDeleteLocalItem": "Eliminar elemento local", "ButtonDisableAutoTimer": "Desactivar temporizador automático", "ButtonDisconnect": "Desconectar", "ButtonGoToWebClient": "Ir al cliente web", "ButtonHistory": "Historial", "ButtonHome": "Inicio", "ButtonIssues": "Problemas", - "ButtonLatest": "Últimos", + "ButtonLatest": "Más recientes", "ButtonLibrary": "Biblioteca", "ButtonLocalMedia": "Medios locales", "ButtonManageLocalFiles": "Gestionar archivos locales", - "ButtonNewFolder": "Nueva carpeta", + "ButtonNewFolder": "Carpeta nueva", "ButtonNextEpisode": "Próximo episodio", - "ButtonOk": "Bueno", - "ButtonOpenFeed": "Abrir fuente", + "ButtonOk": "Aceptar", + "ButtonOpenFeed": "Abrir suministro", "ButtonOverride": "Sustituir", "ButtonPause": "Pausar", "ButtonPlay": "Reproducir", @@ -39,8 +39,8 @@ "ButtonRead": "Leer", "ButtonReadLess": "Leer menos", "ButtonReadMore": "Leer más", - "ButtonRemove": "Eliminar", - "ButtonRemoveFromServer": "Eliminar del Servidor", + "ButtonRemove": "Quitar", + "ButtonRemoveFromServer": "Quitar del servidor", "ButtonSave": "Guardar", "ButtonSaveOrder": "Guardar pedido", "ButtonSearch": "Buscar", @@ -60,35 +60,35 @@ "HeaderCollection": "Colección", "HeaderCollectionItems": "Elementos en la colección", "HeaderConnectionStatus": "Estado de la conexión", - "HeaderDataSettings": "Ajustes de los datos", + "HeaderDataSettings": "Configuración de datos", "HeaderDetails": "Detalles", "HeaderDownloads": "Descargas", "HeaderEbookFiles": "Archivos de libros digitales", "HeaderEpisodes": "Episodios", - "HeaderEreaderSettings": "Ajustes del lector", - "HeaderLatestEpisodes": "Últimos episodios", + "HeaderEreaderSettings": "Configuración del lector", + "HeaderLatestEpisodes": "Episodios más recientes", "HeaderLibraries": "Bibliotecas", "HeaderLocalFolders": "Carpetas locales", "HeaderLocalLibraryItems": "Elementos de la biblioteca local", "HeaderNewPlaylist": "Nueva lista de reproducción", - "HeaderOpenRSSFeed": "Abrir fuente RSS", - "HeaderPlaybackSettings": "Ajustes de reproducción", + "HeaderOpenRSSFeed": "Abrir suministro RSS", + "HeaderPlaybackSettings": "Configuración de reproducción", "HeaderPlaylist": "Lista de reproducción", "HeaderPlaylistItems": "Elementos de lista de reproducción", "HeaderProgressSyncFailed": "Falló la sincronización del progreso", - "HeaderRSSFeed": "Fuente RSS", - "HeaderRSSFeedGeneral": "Detalles RSS", - "HeaderRSSFeedIsOpen": "Fuente RSS está abierta", + "HeaderRSSFeed": "Suministro RSS", + "HeaderRSSFeedGeneral": "Detalles de RSS", + "HeaderRSSFeedIsOpen": "El suministro RSS está abierto", "HeaderSelectDownloadLocation": "Seleccionar ubicación de descarga", - "HeaderSettings": "Configuraciones", + "HeaderSettings": "Configuración", "HeaderSleepTimer": "Temporizador de apagado", "HeaderSleepTimerSettings": "Ajustes del temporizador para dormir", - "HeaderStatsMinutesListeningChart": "Minutos escuchando (Últimos 7 días)", + "HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)", "HeaderStatsRecentSessions": "Sesiones recientes", - "HeaderTableOfContents": "Tabla de contenidos", - "HeaderUserInterfaceSettings": "Ajustes de la interfaz de usuario", - "HeaderYourStats": "Tus estadísticas", - "LabelAddToPlaylist": "Añadido a la lista de reproducción", + "HeaderTableOfContents": "Sumario", + "HeaderUserInterfaceSettings": "Configuración de interfaz de usuario", + "HeaderYourStats": "Sus estadísticas", + "LabelAddToPlaylist": "Añadir a lista de reproducción", "LabelAddedAt": "Añadido", "LabelAddedDate": "{0} Añadido", "LabelAll": "Todos", @@ -135,30 +135,30 @@ "LabelDuration": "Duración", "LabelEbook": "Libro electrónico", "LabelEbooks": "Libros electrónicos", - "LabelEnable": "Habilitar", + "LabelEnable": "Activar", "LabelEnableMp3IndexSeeking": "Activar la búsqueda de índices mp3", "LabelEnableMp3IndexSeekingHelp": "Esta configuración solo debe habilitarse si tienes archivos mp3 que no se están buscando correctamente. La búsqueda inexacta probablemente se deba a archivos MP3 de tasa de bits variable (VBR). Esta configuración forzará la búsqueda de índice, en la que se construye un mapeo de tiempo a bytes mientras se lee el archivo. En algunos casos, con archivos MP3 grandes, puede haber un retraso al buscar hacia el final del archivo.", "LabelEnd": "Fin", "LabelEndOfChapter": "Fin del capítulo", "LabelEndTime": "Hora de finalización", "LabelEpisode": "Episodio", - "LabelFeedURL": "Fuente de URL", + "LabelFeedURL": "URL del suministro", "LabelFile": "Archivo", "LabelFileBirthtime": "Archivo creado en", "LabelFileModified": "Archivo modificado", "LabelFilename": "Nombre del archivo", "LabelFinished": "Terminado", "LabelFolder": "Carpeta", - "LabelFontBoldness": "Nivel de negrilla en fuente", - "LabelFontScale": "Tamaño de fuente", + "LabelFontBoldness": "Peso tipográfico", + "LabelFontScale": "Escala de letra", "LabelGenre": "Género", "LabelGenres": "Géneros", - "LabelHapticFeedback": "Respuesta táctil", + "LabelHapticFeedback": "Respuesta háptica", "LabelHasEbook": "Tiene un libro", "LabelHasSupplementaryEbook": "Tiene un libro complementario", "LabelHeavy": "Pesado", "LabelHigh": "Alto", - "LabelHost": "Host", + "LabelHost": "Anfitrión", "LabelInProgress": "En proceso", "LabelIncomplete": "Incompleto", "LabelInternalAppStorage": "Almacenamiento interno de aplicaciones", @@ -166,19 +166,20 @@ "LabelJumpForwardsTime": "Salto adelante en el tiempo", "LabelKeepScreenAwake": "Mantener la pantalla encendida", "LabelLanguage": "Idioma", - "LabelLayout": "Diseño", + "LabelLayout": "Disposición", "LabelLayoutAuto": "Automático", "LabelLayoutSinglePage": "Página única", "LabelLight": "Claro", "LabelLineSpacing": "Interlineado", "LabelListenAgain": "Volver a escuchar", "LabelLocalBooks": "Libros Locales", - "LabelLocalPodcasts": "Podcasts Locales", + "LabelLocalPodcasts": "Pódcast locales", "LabelLockOrientation": "Bloquear orientación", "LabelLockPlayer": "Bloquear el reproductor", "LabelLow": "Bajo", "LabelMediaType": "Tipo de multimedia", "LabelMedium": "Medio", + "LabelMissing": "Falta", "LabelMore": "Más", "LabelMoreInfo": "Más información", "LabelName": "Nombre", @@ -190,25 +191,25 @@ "LabelNavigateWithVolumeWhilePlayingDisabled": "Apagado", "LabelNavigateWithVolumeWhilePlayingEnabled": "Encender", "LabelNever": "Nunca", - "LabelNewestAuthors": "Autores más recientes", - "LabelNewestEpisodes": "Episodios más recientes", + "LabelNewestAuthors": "Autores más nuevos", + "LabelNewestEpisodes": "Episodios más nuevos", "LabelNo": "No", "LabelNotFinished": "No terminado", "LabelNotStarted": "Sin iniciar", "LabelNumEpisodes": "{0} episodios", "LabelNumEpisodesIncomplete": "{0} episodios, {1} incompletos", - "LabelNumberOfEpisodes": "# de Episodios", + "LabelNumberOfEpisodes": "N.º de episodios", "LabelOff": "Apagado", "LabelOn": "Encendido", "LabelPassword": "Contraseña", - "LabelPath": "Ruta de carpeta", + "LabelPath": "Ruta", "LabelPlaybackDirect": "Directo", "LabelPlaybackLocal": "Local", "LabelPlaybackSpeed": "Velocidad de reproducción", "LabelPlaybackTranscode": "Transcodificar", - "LabelPodcast": "Podcast", - "LabelPodcasts": "Podcasts", - "LabelPreventIndexing": "Evite que su fuente sea indexada por los directorios de podcasts de iTunes y Google", + "LabelPodcast": "Pódcast", + "LabelPodcasts": "Pódcast", + "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indicen su suministro", "LabelProgress": "Progreso", "LabelPubDate": "Fecha de publicación", "LabelPublishYear": "Año de publicación", @@ -216,19 +217,19 @@ "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedPreventIndexing": "Prevenir indexado", - "LabelRSSFeedSlug": "Fuente RSS Slug", + "LabelRSSFeedSlug": "«Slug» de suministro RSS", "LabelRandomly": "Aleatorio", "LabelRead": "Leído", "LabelReadAgain": "Volver a leer", - "LabelRecentSeries": "Series Recientes", - "LabelRecentlyAdded": "Añadido Recientemente", + "LabelRecentSeries": "Series recientes", + "LabelRecentlyAdded": "Añadidos recientemente", "LabelRemoveFromPlaylist": "Eliminar de la Lista de Reproducción", "LabelScaleElapsedTimeBySpeed": "Escala el tiempo transcurrido según la velocidad", "LabelSeason": "Temporada", "LabelSelectADevice": "Selecciona un dispositivo", "LabelSequenceAscending": "Secuencia Ascendente", "LabelSequenceDescending": "Secuencia Descendente", - "LabelSeries": "Series", + "LabelSeries": "Serie", "LabelServerAddress": "Dirección del servidor", "LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsSupplementary": "Establecer como suplementario", @@ -251,16 +252,17 @@ "LabelTag": "Etiqueta", "LabelTags": "Etiquetas", "LabelTheme": "Tema", + "LabelThemeBlack": "Negro", "LabelThemeDark": "Oscuro", "LabelThemeLight": "Claro", "LabelTimeRemaining": "{0} restante", "LabelTitle": "Título", - "LabelTotalSize": "Tamaño Total", + "LabelTotalSize": "Tamaño total", "LabelTotalTrack": "Pista Total", "LabelTracks": "Pistas", "LabelType": "Tipo", "LabelUnknown": "Desconocido", - "LabelUnlockPlayer": "Desbloquear Reproductor", + "LabelUnlockPlayer": "Desbloquear reproductor", "LabelUseBookshelfView": "Usar la Vista de Estantería", "LabelUser": "Usuario", "LabelUsername": "Nombre de Usuario", @@ -269,25 +271,25 @@ "LabelYearReviewHide": "Ocultar Resumen del año", "LabelYearReviewShow": "Resumen del año", "LabelYourBookmarks": "Tus Marcadores", - "LabelYourProgress": "Tu Progreso", + "LabelYourProgress": "Su progreso", "MessageAndroid10Downloads": "Android 10 e inferiores utilizarán el almacenamiento interno de aplicaciones para las descargas.", "MessageAttemptingServerConnection": "Intentando conectar con el servidor...", "MessageAudiobookshelfServerNotConnected": "Servidor de Audiobookshelf no conectado", "MessageAudiobookshelfServerRequired": "¡Importante! Esta aplicación está diseñada para trabajar con un servidor Audiobookshelf que usted o alguien que usted conoce es el anfitrión. Esta aplicación no proporciona ningún contenido.", "MessageBookshelfEmpty": "Estantería vacía", - "MessageConfirmDeleteLocalEpisode": "¿Eliminar episodio local \"{0}\" de su dispositivo? El archivo en el servidor no se verá afectado.", - "MessageConfirmDeleteLocalFiles": "¿Eliminar los archivos locales de este elemento de tu dispositivo? Los archivos del servidor y tu progreso no se verán afectados.", - "MessageConfirmDiscardProgress": "¿Estás seguro de que quieres reiniciar tu progreso?", + "MessageConfirmDeleteLocalEpisode": "¿Quiere eliminar el episodio local «{0}» del dispositivo? El archivo en el servidor no se verá afectado.", + "MessageConfirmDeleteLocalFiles": "¿Quiere quitar los archivos locales de este elemento del dispositivo? Los archivos del servidor y su progreso no se verán afectados.", + "MessageConfirmDiscardProgress": "¿Confirma que quiere restablecer su progreso?", "MessageConfirmDownloadUsingCellular": "Estas a punto de realizar una descarga utilizando datos móviles. Esto puede incluir cargos de datos del operador. ¿Deseas continuar?", - "MessageConfirmMarkAsFinished": "¿Está seguro de que desea marcar este artículo como terminado?", - "MessageConfirmRemoveBookmark": "¿Estás seguro de que quieres eliminar el marcador?", + "MessageConfirmMarkAsFinished": "¿Confirma que quiere marcar este elemento como terminado?", + "MessageConfirmRemoveBookmark": "¿Confirma que quiere quitar el marcador?", "MessageConfirmStreamingUsingCellular": "Estás a punto de hacer streaming utilizando datos móviles. Esto puede incluir cargos de datos del operador. ¿Deseas continuar?", - "MessageDiscardProgress": "Descartar Progreso", + "MessageDiscardProgress": "Descartar progreso", "MessageDownloadCompleteProcessing": "Descarga Completada. Procesando...", "MessageDownloading": "Descargando...", - "MessageDownloadingEpisode": "Descargando Capitulo", + "MessageDownloadingEpisode": "Descargando episodio", "MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar", - "MessageFeedURLWillBe": "URL de la fuente será {0}", + "MessageFeedURLWillBe": "El URL del suministro será {0}", "MessageFetching": "Buscando...", "MessageFollowTheProjectOnGithub": "Sigue el proyecto en GitHub", "MessageItemDownloadCompleteFailedToCreate": "Se ha completado la descarga del elemento, pero no se ha podido crear el elemento de la biblioteca", @@ -307,13 +309,13 @@ "MessageNoListeningSessions": "Ninguna sesión escuchada", "MessageNoMediaFolders": "Sin carpetas multimedia", "MessageNoNetworkConnection": "Sin conexión de red", - "MessageNoPodcastsFound": "Ningún podcast encontrado", + "MessageNoPodcastsFound": "No se encontró ningún pódcast", "MessageNoSeries": "Ninguna serie", "MessageNoUpdatesWereNecessary": "No fue necesario actualizar", - "MessageNoUserPlaylists": "No tienes ninguna lista de reproducción", + "MessageNoUserPlaylists": "No tiene ninguna lista de reproducción", "MessageOldServerConnectionWarning": "La configuración de la conexión al servidor está utilizando un ID de usuario antiguo. Por favor, elimine y vuelva a añadir esta conexión al servidor.", "MessageOldServerConnectionWarningHelp": "Usted configuró originalmente la conexión a este servidor antes de la migración de la base de datos en la versión 2.3.0, publicada en junio de 2023. Una futura actualización del servidor eliminará la posibilidad de iniciar sesión con esta conexión antigua. Por favor, elimine la conexión existente al servidor y conéctese de nuevo (utilizando la misma dirección del servidor y las mismas credenciales). Si tiene algún medio descargado en este dispositivo, será necesario descargarlo de nuevo para sincronizarlo con el servidor.", - "MessagePodcastSearchField": "Introduzca el término de búsqueda o la URL de la fuente RSS", + "MessagePodcastSearchField": "Introduzca el término de búsqueda o el URL del suministro RSS", "MessageProgressSyncFailed": "El último intento de informar al servidor sobre el progreso de la escucha ha fallado. Las solicitudes de sincronización de progreso seguirán intentándose cada 15 segundos a 1 minuto mientras se reproduce el contenido multimedia.", "MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en", "MessageSeriesAlreadyDownloaded": "Ya has descargado todos los libros de esta serie.", @@ -324,7 +326,7 @@ "MessageSocketConnectedOverUnmeteredCellular": "Socket conectado a través de red celular sin tarificación por consumo", "MessageSocketConnectedOverUnmeteredWifi": "Socket conectado a través de una red Wi-Fi sin tarificación por consumo", "MessageSocketNotConnected": "Socket no conectado", - "NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que la URL de la fuente RSS use HTTPS", + "NoteRSSFeedPodcastAppsHttps": "Atención: la mayoría de las aplicaciones de pódcast requieren que el URL del suministro RSS use HTTPS", "NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.", "ToastBookmarkCreateFailed": "Error al crear marcador", "ToastBookmarkRemoveFailed": "Error al eliminar marcador", @@ -333,9 +335,9 @@ "ToastItemMarkedAsFinishedFailed": "Error al marcar como terminado", "ToastItemMarkedAsNotFinishedFailed": "No se ha podido marcar como no finalizado", "ToastPlaylistCreateFailed": "Error al crear la lista de reproducción", - "ToastPodcastCreateFailed": "Error al crear podcast", - "ToastPodcastCreateSuccess": "Podcast creado", - "ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS", - "ToastRSSFeedCloseSuccess": "Fuente RSS cerrada", + "ToastPodcastCreateFailed": "No se pudo crear el pódcast", + "ToastPodcastCreateSuccess": "Se creó el pódcast correctamente", + "ToastRSSFeedCloseFailed": "Error al cerrar el suministro RSS", + "ToastRSSFeedCloseSuccess": "Suministro RSS cerrado", "ToastStreamingNotAllowedOnCellular": "El streaming no está permitido con datos móviles" } diff --git a/strings/fi.json b/strings/fi.json index 5ab3c279..935210e7 100644 --- a/strings/fi.json +++ b/strings/fi.json @@ -63,7 +63,7 @@ "HeaderDataSettings": "Tietojen asetukset", "HeaderDetails": "Yksityiskohdat", "HeaderDownloads": "Lataukset", - "HeaderEbookFiles": "E-kirjatiedostot", + "HeaderEbookFiles": "S-kirjatiedostot", "HeaderEpisodes": "Jaksot", "HeaderEreaderSettings": "E-lukijan asetukset", "HeaderLatestEpisodes": "Viimeisimmät jaksot", @@ -133,8 +133,8 @@ "LabelDownloadUsingCellular": "Lataa käyttäen mobiilidataa", "LabelDownloaded": "Ladatut", "LabelDuration": "Kesto", - "LabelEbook": "E-kirja", - "LabelEbooks": "E-kirjat", + "LabelEbook": "S-kirja", + "LabelEbooks": "S-kirjat", "LabelEnable": "Ota käyttöön", "LabelEnableMp3IndexSeeking": "Ota mp3-hakemistohaku käyttöön", "LabelEnableMp3IndexSeekingHelp": "Tämä asetus tulee ottaa käyttöön vain, jos sinulla on mp3-tiedostoja, jotka eivät etsi oikein. Epätarkka haku johtuu todennäköisesti muuttuvan bittinopeuden (Variable bitrate (VBR)) MP3-tiedostoista. Tämä asetus pakottaa indeksin etsimisen, jossa aika-tavu-kartoitus rakennetaan tiedostoa luettaessa. Joissakin tapauksissa suurilla MP3-tiedostoilla on viive etsimisessä tiedoston loppua kohti.", @@ -154,8 +154,8 @@ "LabelGenre": "Lajityyppi", "LabelGenres": "Lajityypit", "LabelHapticFeedback": "Tuntopalaute", - "LabelHasEbook": "Sillä on e-kirja", - "LabelHasSupplementaryEbook": "Sillä on täydentävän e-kirjan", + "LabelHasEbook": "Sillä on s-kirja", + "LabelHasSupplementaryEbook": "Sillä on täydentävän s-kirjan", "LabelHeavy": "Raskas", "LabelHigh": "Korkea", "LabelHost": "Isäntä", @@ -252,6 +252,7 @@ "LabelTag": "Tägi", "LabelTags": "Tägit", "LabelTheme": "Teema", + "LabelThemeBlack": "Musta", "LabelThemeDark": "Tumma", "LabelThemeLight": "Kirkas", "LabelTimeRemaining": "{0} jäljellä", diff --git a/strings/fr.json b/strings/fr.json index 0b0aa73f..7346cded 100644 --- a/strings/fr.json +++ b/strings/fr.json @@ -39,7 +39,7 @@ "ButtonRead": "Lire", "ButtonReadLess": "Lire moins", "ButtonReadMore": "Lire plus", - "ButtonRemove": "Supprimer", + "ButtonRemove": "Retirer", "ButtonRemoveFromServer": "Retirer du serveur", "ButtonSave": "Sauvegarder", "ButtonSaveOrder": "Sauvegarder l’ordre", @@ -137,10 +137,10 @@ "LabelEbooks": "Livres numériques", "LabelEnable": "Activer", "LabelEnableMp3IndexSeeking": "Activer la recherche par index pour mp3", - "LabelEnableMp3IndexSeekingHelp": "Ce paramètres ne devrai être activer que si vous avez des problèmes avec des fichiers mp3 que vous ne pouvez pas chercher. La mauvaise indexation est probablement du au Bitrate Variable (VBR) du fichier MP3. Ce paramètre va forcer l'indexation. Une correspondance Temps-Octet est construit durant la lecture. Dans certains cas, avec de gros fichier MP3, il y aura du délais lors de la recherche vers la fin du fichier.", + "LabelEnableMp3IndexSeekingHelp": "Ce paramètre ne devrait être activé que si vous avez des fichiers mp3 qui ne cherchent pas correctement. La recherche inexacte est probablement due aux fichiers MP3 à débit variable (VBR). Ce paramètre forcera la recherche de l'index, dans lequel une cartographie temporelle est construite pendant que le fichier est lu. Dans certains cas, il y aura un retard dans la recherche vers la fin du dossier.", "LabelEnd": "Fin", "LabelEndOfChapter": "Fin du chapitre", - "LabelEndTime": "Heure de Fin", + "LabelEndTime": "Heure de fin", "LabelEpisode": "Épisode", "LabelFeedURL": "URL du flux", "LabelFile": "Fichier", @@ -252,6 +252,7 @@ "LabelTag": "Étiquette", "LabelTags": "Étiquettes", "LabelTheme": "Thème", + "LabelThemeBlack": "Noir", "LabelThemeDark": "Sombre", "LabelThemeLight": "Clair", "LabelTimeRemaining": "{0} restantes", @@ -278,7 +279,7 @@ "MessageBookshelfEmpty": "Bibliothèque vide", "MessageConfirmDeleteLocalEpisode": "Retirer l’épisode local « {0} » de votre appareil ? Le fichier sur le serveur ne sera pas affecté.", "MessageConfirmDeleteLocalFiles": "Supprimer les fichiers locaux de cet élément de votre appareil ? Les fichiers sur le serveur ainsi que votre progression ne serons pas affectés.", - "MessageConfirmDisableAutoTimer": "Êtes-vous sûr de vouloir désactiver le minuteur automatique pour le reste de la journée? Le minuteur sera réactivé à la fin de cette période de minuterie automatique, ou si vous redémarrez l'application.", + "MessageConfirmDisableAutoTimer": "Êtes-vous sûr·e de vouloir désactiver le minuteur automatique pour le reste de la journée ? Le minuteur sera réactivé à la fin de cette période de décompte automatique, ou si vous redémarrez l'application.", "MessageConfirmDiscardProgress": "Êtes-vous sûr·e de vouloir supprimer votre progression ?", "MessageConfirmDownloadUsingCellular": "Vous êtes sur le point d’effectuer un téléchargement en utilisant des données mobiles.Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer ?", "MessageConfirmMarkAsFinished": "Êtes-vous sûr·e de vouloir marquer cette élement comme terminé ?", @@ -332,7 +333,7 @@ "NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.", "ToastBookmarkCreateFailed": "Échec de la création de marque-page", "ToastBookmarkRemoveFailed": "Échec de la suppression de marque-page", - "ToastBookmarkUpdateFailed": "Échec de la mise à jour de marsue-page", + "ToastBookmarkUpdateFailed": "Échec de la mise à jour du marque-page", "ToastDownloadNotAllowedOnCellular": "Le téléchargement sur les données mobile n’est pas autorisé", "ToastItemMarkedAsFinishedFailed": "Échec de l’annotation terminée", "ToastItemMarkedAsNotFinishedFailed": "Échec de l’annotation non-terminée", diff --git a/strings/hi.json b/strings/hi.json index 03115ae7..c55a17a5 100644 --- a/strings/hi.json +++ b/strings/hi.json @@ -1,10 +1,14 @@ { "ButtonAdd": "जोड़ें", + "ButtonAddNewServer": "नया सर्वर जोड़ें", "ButtonAuthors": "लेखक", + "ButtonBack": "पीछे", "ButtonCancel": "रद्द करें", + "ButtonCancelTimer": "टाइमर रद्द करें", "ButtonClearFilter": "लागू फ़िल्टर साफ़ करें", "ButtonCloseFeed": "फ़ीड बंद करें", "ButtonCollections": "संग्रह", + "ButtonConnect": "जोड़ना", "ButtonCreate": "बनाएं", "ButtonDelete": "हटाएं", "ButtonHome": "घर", diff --git a/strings/hr.json b/strings/hr.json index b4e9a7d8..4499401d 100644 --- a/strings/hr.json +++ b/strings/hr.json @@ -252,6 +252,7 @@ "LabelTag": "Oznaka", "LabelTags": "Oznake", "LabelTheme": "Tema", + "LabelThemeBlack": "Crna", "LabelThemeDark": "Tamna", "LabelThemeLight": "Svijetla", "LabelTimeRemaining": "preostalo {0}", diff --git a/strings/it.json b/strings/it.json index a8d217af..4886e5af 100644 --- a/strings/it.json +++ b/strings/it.json @@ -153,7 +153,7 @@ "LabelFontScale": "Dimensione font", "LabelGenre": "Genere", "LabelGenres": "Generi", - "LabelHapticFeedback": "Feedback tattile", + "LabelHapticFeedback": "Feedback aptico", "LabelHasEbook": "Ha un libro", "LabelHasSupplementaryEbook": "Ha un libro supplementale", "LabelHeavy": "Forte", @@ -252,6 +252,7 @@ "LabelTag": "Etichetta", "LabelTags": "Etichette", "LabelTheme": "Tema", + "LabelThemeBlack": "Nero", "LabelThemeDark": "Scuro", "LabelThemeLight": "Chiaro", "LabelTimeRemaining": "{0} rimanente", diff --git a/strings/ja.json b/strings/ja.json index 80af12d8..1457265a 100644 --- a/strings/ja.json +++ b/strings/ja.json @@ -1,3 +1,14 @@ { - "ButtonAdd": "追加" + "ButtonAdd": "追加", + "ButtonCancel": "キャンセル", + "ButtonCancelTimer": "キャンセルタイマー", + "ButtonOk": "はい", + "ButtonPlay": "プレイ", + "ButtonRead": "野村", + "ButtonYes": "はい", + "LabelBooks": "ほん", + "LabelLanguage": "言語", + "LabelName": "名", + "LabelPassword": "パスワード", + "LabelPodcast": "ポッドキャスト" } diff --git a/strings/pt-br.json b/strings/pt-br.json index 54b34531..9ebad3ec 100644 --- a/strings/pt-br.json +++ b/strings/pt-br.json @@ -29,6 +29,7 @@ "ButtonManageLocalFiles": "Gerenciar Arquivos Locais", "ButtonNewFolder": "Nova Pasta", "ButtonNextEpisode": "Próximo Episódio", + "ButtonOk": "Ok", "ButtonOpenFeed": "Abrir Feed", "ButtonOverride": "Sobrepor", "ButtonPause": "Pausar", @@ -53,6 +54,7 @@ "ButtonYes": "Sim", "HeaderAccount": "Conta", "HeaderAdvanced": "Avançado", + "HeaderAndroidAutoSettings": "Configurações do Android Auto", "HeaderAudioTracks": "Trilhas de áudio", "HeaderChapters": "Capítulos", "HeaderCollection": "Coleção", @@ -73,6 +75,7 @@ "HeaderPlaybackSettings": "Configurações de Reprodução", "HeaderPlaylist": "Lista de Reprodução", "HeaderPlaylistItems": "Itens da lista de reprodução", + "HeaderProgressSyncFailed": "Sincronização de Progresso Falhou", "HeaderRSSFeed": "Feed RSS", "HeaderRSSFeedGeneral": "Detalhes RSS", "HeaderRSSFeedIsOpen": "Feed RSS está Aberto", @@ -87,6 +90,7 @@ "HeaderYourStats": "Suas Estatísticas", "LabelAddToPlaylist": "Adicionar à Lista de Reprodução", "LabelAddedAt": "Acrescentado Em", + "LabelAddedDate": "Adicionado {0}", "LabelAll": "Todos", "LabelAllowSeekingOnMediaControls": "Permitir busca de posição nos controles de notificação de mídia", "LabelAlways": "Sempre", diff --git a/strings/ru.json b/strings/ru.json index 69bf9dd9..f72d84b7 100644 --- a/strings/ru.json +++ b/strings/ru.json @@ -252,6 +252,7 @@ "LabelTag": "Тег", "LabelTags": "Теги", "LabelTheme": "Тема", + "LabelThemeBlack": "Черный", "LabelThemeDark": "Темная", "LabelThemeLight": "Светлая", "LabelTimeRemaining": "{0} осталось", @@ -296,6 +297,7 @@ "MessageItemMissing": "Элемент отсутствует и должен быть исправлен на сервере. Обычно элемент помечается как отсутствующий, поскольку пути к файлам недоступны.", "MessageLoading": "Загрузка...", "MessageLoadingServerData": "Загрузка данных сервера...", + "MessageLocalFolderDescription": "\"Внутреннее хранилище приложения\" доступно только через это приложение. Это приложение поддерживает только мультимедийные файлы, загруженные непосредственно через приложение. Общие папки для хранения можно использовать, чтобы другие приложения могли получать доступ к мультимедийным файлам, загруженным через это приложение.", "MessageMarkAsFinished": "Отметить, как завершенную", "MessageMediaLinkedToADifferentServer": "Медиафайлы связаны с сервером Audiobookshelf по другому адресу ({0}). Прогресс будет синхронизирован при подключении к этому адресу сервера.", "MessageMediaLinkedToADifferentUser": "Медиафайл связан с этим сервером, но был загружен другим пользователем. Прогресс будет синхронизирован только с пользователем, который его скачал.", diff --git a/strings/sl.json b/strings/sl.json index 01d31db7..ead2fcb8 100644 --- a/strings/sl.json +++ b/strings/sl.json @@ -137,7 +137,7 @@ "LabelEbooks": "E-knjige", "LabelEnable": "Omogoči", "LabelEnableMp3IndexSeeking": "Omogoči iskanje po indeksu mp3", - "LabelEnableMp3IndexSeekingHelp": "To nastavitev naj bo omogočena le, če imate datoteke mp3, ki ne iščejo pravilno. Netočno iskanje je najverjetneje posledica datotek MP3 s spremenljivo hitrostjo (VBR). Ta nastavitev bo vsilila iskanje po indeksu, pri katerem se med branjem datoteke gradi preslikava časa v podatke. V nekaterih primerih bo pri velikih datotekah MP3 prišlo do zakasnitve pri iskanju proti koncu datoteke.", + "LabelEnableMp3IndexSeekingHelp": "Ta nastavitev naj bo omogočena le, če imate datoteke mp3, ki ne iščejo pravilno. Netočno iskanje je najverjetneje posledica datotek MP3 s spremenljivo hitrostjo (VBR). Ta nastavitev bo vsilila iskanje po indeksu, pri katerem se med branjem datoteke gradi preslikava časa v podatke. V nekaterih primerih bo pri velikih datotekah MP3 prišlo do zakasnitve pri iskanju proti koncu datoteke.", "LabelEnd": "Konec", "LabelEndOfChapter": "Konec poglavja", "LabelEndTime": "Končni čas", @@ -252,6 +252,7 @@ "LabelTag": "Oznaka", "LabelTags": "Oznake", "LabelTheme": "Tema", + "LabelThemeBlack": "Črna", "LabelThemeDark": "Temna", "LabelThemeLight": "Svetla", "LabelTimeRemaining": "Še {0}", diff --git a/strings/sv.json b/strings/sv.json index ad39c35a..7a06e4dc 100644 --- a/strings/sv.json +++ b/strings/sv.json @@ -19,10 +19,10 @@ "ButtonDeleteLocalItem": "Radera lokalt objekt", "ButtonDisableAutoTimer": "Inaktivera Automatisk Timer", "ButtonDisconnect": "Koppla ur", - "ButtonGoToWebClient": "Öppna i Webbläsare", + "ButtonGoToWebClient": "Öppna i webbläsare", "ButtonHistory": "Histora", "ButtonHome": "Hem", - "ButtonIssues": "Problem", + "ButtonIssues": "Objekt med problem", "ButtonLatest": "Senaste", "ButtonLibrary": "Bibliotek", "ButtonLocalMedia": "Lokal media", @@ -37,6 +37,8 @@ "ButtonPlayEpisode": "Spela Episod", "ButtonPlaylists": "Spellistor", "ButtonRead": "Läs", + "ButtonReadLess": "Visa mindre", + "ButtonReadMore": "Visa mer", "ButtonRemove": "Ta bort", "ButtonRemoveFromServer": "Ta bort från Server", "ButtonSave": "Spara", @@ -53,7 +55,7 @@ "HeaderAccount": "Konto", "HeaderAdvanced": "Avancerad", "HeaderAndroidAutoSettings": "Android Auto Inställningar", - "HeaderAudioTracks": "Ljudspår", + "HeaderAudioTracks": "Ljudfiler", "HeaderChapters": "Kapitel", "HeaderCollection": "Samling", "HeaderCollectionItems": "Böcker i samlingen", @@ -73,6 +75,7 @@ "HeaderPlaybackSettings": "Uppspelningsinställningar", "HeaderPlaylist": "Spellista", "HeaderPlaylistItems": "Böcker i spellistan", + "HeaderProgressSyncFailed": "Progress Sync misslyckades", "HeaderRSSFeed": "RSS flöde", "HeaderRSSFeedGeneral": "RSS-information", "HeaderRSSFeedIsOpen": "RSS-flödet är öppet", @@ -91,6 +94,7 @@ "LabelAll": "Alla", "LabelAllowSeekingOnMediaControls": "Tillåt positionssökning på medieaviseringskontroller", "LabelAlways": "Alltid", + "LabelAndroidAutoBrowseLimitForGroupingHelp": "Använd inte alfabetisk neddragning när det finns mindre än denna mängd objekt att visa", "LabelAskConfirmation": "Fråga efter bekräftelse", "LabelAuthor": "Författare", "LabelAuthorFirstLast": "Författare (Förnamn Efternamn)", @@ -126,12 +130,12 @@ "LabelDownload": "Ladda ner", "LabelDownloadUsingCellular": "Ladda ner med mobildata", "LabelDownloaded": "Nedladdat", - "LabelDuration": "Varaktighet", + "LabelDuration": "Längd", "LabelEbook": "E-bok", "LabelEbooks": "E-böcker", "LabelEnable": "Aktivera", "LabelEnableMp3IndexSeeking": "Aktivera mp3 index sökning", - "LabelEnableMp3IndexSeekingHelp": "Den här inställningen bör endast aktiveras om du har mp3-filer som inte söker korrekt. Felaktig sökning beror med största sannolikhet på MP3-filer med variabel birate (VBR). Den här inställningen tvingar fram indexsökning, där en tid-till-byte-mappning byggs upp när filen läses. I vissa fall med stora MP3-filer blir det en fördröjning vid sökning mot slutet av filen.", + "LabelEnableMp3IndexSeekingHelp": "Den här inställningen bör endast aktiveras om du har mp3-filer som inte söker korrekt. Felaktig sökning beror med största sannolikhet på mp3-filer med variabel bitrate (VBR). Den här inställningen tvingar fram indexsökning, där en tid-till-byte-mappning byggs upp när filen läses. I vissa fall med stora mp3-filer blir det en fördröjning vid sökning mot slutet av filen.", "LabelEnd": "Slut", "LabelEndOfChapter": "Slut av kapitel", "LabelEndTime": "Slut tid", @@ -158,11 +162,12 @@ "LabelInternalAppStorage": "Intern App Lagring", "LabelJumpBackwardsTime": "hoppa-bakåt-tid", "LabelJumpForwardsTime": "hoppa-framåt-tid", + "LabelKeepScreenAwake": "Håll skärmen vaken", "LabelLanguage": "Språk", "LabelLayout": "Layout", "LabelLayoutAuto": "Automatisk", "LabelLayoutSinglePage": "En sida", - "LabelLight": "Ljust", + "LabelLight": "Lätt", "LabelLineSpacing": "Radavstånd", "LabelListenAgain": "Lyssna igen", "LabelLocalBooks": "Lokala böcker", @@ -172,6 +177,7 @@ "LabelLow": "Låg", "LabelMediaType": "Mediatyp", "LabelMedium": "Medel", + "LabelMissing": "Saknar", "LabelMore": "Mer", "LabelMoreInfo": "Mer information", "LabelName": "Namn", @@ -179,12 +185,18 @@ "LabelNarrators": "Berättare", "LabelNavigateWithVolume": "Navigera med volymknapparna", "LabelNavigateWithVolumeMirrored": "Speglad", + "LabelNavigateWithVolumeWhilePlaying": "Tillåt volymknappar att navigera medan du spelar", + "LabelNavigateWithVolumeWhilePlayingDisabled": "Av", + "LabelNavigateWithVolumeWhilePlayingEnabled": "På", "LabelNever": "Aldrig", "LabelNewestAuthors": "Senaste författarna", "LabelNewestEpisodes": "Senaste avsnitten", "LabelNo": "Nej", "LabelNotFinished": "Ej avslutad", "LabelNotStarted": "Ej påbörjad", + "LabelNumEpisodes": "{0} avsnitt", + "LabelNumEpisodesIncomplete": "{0} avsnitt, {1} ofärdiga", + "LabelNumberOfEpisodes": "# av Avsnitt", "LabelOff": "Av", "LabelOn": "På", "LabelPassword": "Lösenord", @@ -198,7 +210,7 @@ "LabelPreventIndexing": "Förhindra att ditt flöde indexeras av sökmotorer från iTunes och Google", "LabelProgress": "Framsteg", "LabelPubDate": "Publiceringsdatum", - "LabelPublishYear": "Publiceringsår", + "LabelPublishYear": "Utgivningsår", "LabelPublishedDate": "Publicerad {0}", "LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", @@ -213,6 +225,7 @@ "LabelScaleElapsedTimeBySpeed": "Skaländra uppspelningsposition efter uppspelningshastighet.", "LabelSeason": "Säsong", "LabelSelectADevice": "Välj en enhet", + "LabelSequenceAscending": "Stigande", "LabelSeries": "Serie", "LabelServerAddress": "Server-adress", "LabelSetEbookAsPrimary": "Ange som primär", diff --git a/strings/tr.json b/strings/tr.json index 2301da63..1d1f8987 100644 --- a/strings/tr.json +++ b/strings/tr.json @@ -45,7 +45,7 @@ "ButtonSaveOrder": "Kaydetme Sırası", "ButtonSearch": "Ara", "ButtonSendEbookToDevice": "Ekitabı Cihaza Gönder", - "ButtonSeries": "Dizi", + "ButtonSeries": "Seriler", "ButtonSetTimer": "Zamanlayıcı Kur", "ButtonStream": "Yayın", "ButtonSubmit": "Gönder", @@ -222,6 +222,7 @@ "LabelRemoveFromPlaylist": "Oynatma Listesinden Kaldır", "LabelSeason": "Sezon", "LabelSelectADevice": "Cihaz seçiniz", + "LabelSeries": "Seriler", "LabelServerAddress": "Sunucu adresi", "LabelSetEbookAsPrimary": "Birincil olarak ayarla", "LabelSetEbookAsSupplementary": "Yedek olarak ayarla", @@ -242,11 +243,13 @@ "LabelTag": "Etiket", "LabelTags": "Etiketler", "LabelTheme": "Tema", + "LabelThemeBlack": "Siyah", "LabelThemeDark": "Koyu", "LabelThemeLight": "Açık", "LabelTimeRemaining": "{0} kalan", "LabelTitle": "Başlık", "LabelTotalSize": "Toplam Boyut", + "LabelTotalTrack": "Toplam Parça", "LabelTracks": "Parçalar", "LabelType": "Tür", "LabelUnknown": "Bilinmeyen", @@ -256,7 +259,10 @@ "LabelUsername": "Kullanıcı Adı", "LabelVeryHigh": "Çok Yüksek", "LabelVeryLow": "Çok Düşük", + "LabelYearReviewHide": "Yıla Bakışı Sakla", + "LabelYearReviewShow": "Yıla Bakışı Göster", "LabelYourBookmarks": "Yer İşaretleriniz", + "LabelYourProgress": "Gelişiminiz", "MessageAndroid10Downloads": "Android 10 ve aşağısı indirmeler için dahili uygulama deposunu kullanacaktır.", "MessageAttemptingServerConnection": "Sunucusu bağlantısı deneniyor...", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf sunucusu bağlı değil", diff --git a/strings/uk.json b/strings/uk.json index bd3e533c..1679d822 100644 --- a/strings/uk.json +++ b/strings/uk.json @@ -26,6 +26,7 @@ "ButtonLatest": "Останні", "ButtonLibrary": "Бібліотека", "ButtonLocalMedia": "Локальні файли", + "ButtonLogs": "Журнали", "ButtonManageLocalFiles": "Керування локальними файлами", "ButtonNewFolder": "Нова тека", "ButtonNextEpisode": "Наступний епізод", @@ -137,7 +138,7 @@ "LabelEbooks": "Електронні книги", "LabelEnable": "Увімкнути", "LabelEnableMp3IndexSeeking": "Увімкнути індексоване перемотування mp3", - "LabelEnableMp3IndexSeekingHelp": "Цей параметр слід увімкнути, якщо у вас є mp3-файли, які перемотуються некоректно. Неточність, найімовірніше, спричинена MP3-файлами зі змінним бітрейтом (VBR). Цей параметр увімкне пошук за індексом, у якому під час зчитування файлу індексується прогрес. У деяких випадках у великих MP3-файлах може виникнути затримка під час перемотування в кінці файлу.", + "LabelEnableMp3IndexSeekingHelp": "Цей параметр слід увімкнути, лише якщо у вас є файли mp3, пошук яких не здійснюється належним чином. Неточний пошук, швидше за все, пов’язаний із файлами MP3 зі змінним бітрейтом (VBR). Цей параметр примусово шукатиме індекс, у якому відображення часу в байт будується під час читання файлу. У деяких випадках з великими файлами MP3 буде затримка під час пошуку до кінця файлу.", "LabelEnd": "Кінець", "LabelEndOfChapter": "Кінець глави", "LabelEndTime": "Час завершення", @@ -252,6 +253,7 @@ "LabelTag": "Мітка", "LabelTags": "Мітки", "LabelTheme": "Тема", + "LabelThemeBlack": "Чорний", "LabelThemeDark": "Темна", "LabelThemeLight": "Світла", "LabelTimeRemaining": "Лишилося: {0}", diff --git a/strings/zh-cn.json b/strings/zh-cn.json index c13d230f..7619b34d 100644 --- a/strings/zh-cn.json +++ b/strings/zh-cn.json @@ -252,6 +252,7 @@ "LabelTag": "标签", "LabelTags": "标签", "LabelTheme": "主题", + "LabelThemeBlack": "黑色", "LabelThemeDark": "黑暗", "LabelThemeLight": "明亮", "LabelTimeRemaining": "剩余 {0}",