Compare commits

...
Author SHA1 Message Date
advplyr 7031c895b5 iOS version bump 0.9.64 2023-05-21 18:41:18 -05:00
advplyr 7c739bf3bf Version bump 0.9.64-beta 2023-05-21 18:01:28 -05:00
advplyr facf56930e iOS add deviceId 2023-05-21 17:19:21 -05:00
advplyr 2c3dff3544 Add:EBook download and offline reading #187 #243 2023-05-21 15:02:49 -05:00
advplyr b1bf68b8cd Add:Series progress to series cards #410 2023-05-21 10:25:10 -05:00
advplyr 78aade7694 Fix:Android delete empty author folder when removing item #701 2023-05-20 18:13:19 -05:00
advplyr 83a4474375 Add:Podcast episode table sort options #376 2023-05-20 17:12:36 -05:00
advplyr 1b47c38efa Fix:Android sleep timer getting set to time left in media #659 2023-05-20 16:51:16 -05:00
advplyr 922d9f22ce Fix chapters button clickable when no chapters 2023-05-20 16:31:51 -05:00
advplyr d7feb52dd8 Fix:Discard progress for local and server media progress #646 2023-05-20 16:29:54 -05:00
advplyr fafaad8b5f Update:Play buttons on item pages show Play/Pause instead of disabled and showing Playing/Streaming #697 2023-05-20 14:49:55 -05:00
advplyr 928f33005a Update:Add play button to collections and playlist table #666 2023-05-20 14:22:53 -05:00
advplyr 0448d20583 Update:Android disable check for updated progress when playing 2023-05-20 13:22:04 -05:00
advplyrandGitHub f9d408b33f Merge pull request #692 from HStep20/master
Added Auto Rewind on Sleep Timer setting to Android
2023-05-20 12:18:17 -05:00
advplyr 9386ae95a3 Fix set sleepTimerSessionId 2023-05-20 12:17:41 -05:00
advplyr c530e0f7e3 Remove test log 2023-05-20 12:00:19 -05:00
advplyr e39a5e1ba3 Fix saveSettings function call 2023-05-20 11:58:27 -05:00
advplyr cb9e317e74 Initialize auto rewind time and update description 2023-05-20 11:56:36 -05:00
advplyr 8061ac0ced Fix auto rewind when check resetting sleep timer 2023-05-20 11:41:39 -05:00
advplyr daff8355d7 Merge branch 'master' into HStep20/master 2023-05-20 10:18:16 -05:00
advplyr 231289e9ac Update:Add podcast type, episode type, episode number, season and support html episode description #677 2023-05-19 17:57:36 -05:00
advplyr 87df022af0 Add:Narrators in search results 2023-05-19 17:18:31 -05:00
advplyr 07fdac9746 Update:Podcast episodes on home page dont show an Episode number if none is set 2023-05-19 17:03:54 -05:00
advplyr 0d482aaa42 Update:Android remove auto-open most recent listened item 2023-05-19 16:59:10 -05:00
advplyr d48d1fdde3 Update:Audio player play method color for visibility #706 2023-05-17 17:59:47 -05:00
hstephens 65b69cc020 fixed if statement syntax 2023-05-12 20:19:18 -06:00
hstephens a3794a6a6a added check before setting state 2023-04-29 17:20:04 -06:00
hstephens 695ab84940 changed behavior for first timer set 2023-04-29 16:48:43 -06:00
hstephens 5399ec67d7 Added Auto Rewind on Sleep Timer setting 2023-04-29 15:38:51 -06:00
advplyr d9a6ec65df Add deviceId to DeviceInfo for Android 2023-04-22 16:48:22 -05:00
advplyr e842de78dd Fix:Audiobook tracks table for local only books 2023-03-28 17:30:54 -05:00
advplyr 260363bf4f Fix:Android media widget initialize DB 2023-03-28 17:30:20 -05:00
advplyrandGitHub 6d7dffd4de Merge pull request #649 from maltejur/largeHeap
Add `android:largeHeap="true"` to `AndroidManifest.xml`
2023-03-27 17:02:09 -05:00
advplyr ef1d8a8eb4 Update:iOS HLS stream buffer set to 50s 2023-03-27 16:54:19 -05:00
Malte Jürgens cdd7a1a636 add android:largeHeap to AndroidManifest.xml 2023-03-27 21:00:52 +02:00
advplyr 6584cd1522 Fix:Android auto sleep timer window #648 2023-03-26 16:43:24 -05:00
advplyr d01b602306 Update epub reader media progress update 2023-03-26 13:50:21 -05:00
advplyr c90f2c136a Add:Support epub progress saving 2023-03-25 17:40:46 -05:00
advplyr 15ccc7192e Fix:Android sleep timer reset volume when shake #626 2023-03-25 16:51:24 -05:00
advplyr e4345a4438 Update:Raw cover on library item page, Android download raw cover #634 2023-03-19 17:13:33 -05:00
advplyr 3f4d564bad Fix:Add try/catch to initCastManager #480 2023-03-10 16:51:45 -06:00
advplyr 3311f66da8 Update readme screenshot 2023-03-10 16:13:04 -06:00
advplyr e711ada0b3 Fix:Center play button when locking ui #624 2023-03-10 15:37:42 -06:00
60 changed files with 1188 additions and 433 deletions
+2 -2
View File
@@ -35,8 +35,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 94
versionName "0.9.63-beta"
versionCode 95
versionName "0.9.64-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+2 -1
View File
@@ -21,7 +21,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" >
android:usesCleartextTraffic="true"
android:largeHeap="true" >
<!-- Android app widget -->
<receiver
@@ -16,6 +16,7 @@ import androidx.media.session.MediaButtonReceiver
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.WidgetEventEmitter
import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
@@ -37,6 +38,8 @@ class MediaPlayerWidget : AppWidgetProvider() {
override fun onEnabled(context: Context) {
Log.i(tag, "onEnabled check context ${context.packageName}")
DbManager.initialize(context)
DeviceManager.deviceData.lastPlaybackSession?.let {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
@@ -96,7 +99,7 @@ internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManage
views.setViewVisibility(R.id.widgetButtonContainer, if (isAppClosed) View.GONE else View.VISIBLE)
views.setOnClickPendingIntent(R.id.widgetBackground, wholeWidgetClickPI)
val imageUri = playbackSession?.getCoverUri() ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.widgetAlbumArt, views, appWidgetId) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
@@ -126,6 +126,7 @@ class Book(
var audioFiles:List<AudioFile>?,
var chapters:List<BookChapter>?,
var tracks:MutableList<AudioTrack>?,
var ebookFile: EBookFile?,
var size:Long?,
var duration:Double?,
var numTracks:Int?
@@ -179,7 +180,7 @@ class Book(
}
@JsonIgnore
override fun getLocalCopy(): Book {
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
}
}
@@ -48,6 +48,20 @@ data class LocalFile(
if (mimeType == "video/mp4") return true
return mimeType?.startsWith("audio") == true
}
@JsonIgnore
fun isEBookFile():Boolean {
return getEBookFormat() != null
}
@JsonIgnore
fun getEBookFormat():String? {
if (mimeType == "application/epub+zip") return "epub"
if (mimeType == "application/pdf") return "pdf"
if (mimeType == "application/x-mobipocket-ebook") return "mobi"
if (mimeType == "application/vnd.comicbook+zip") return "cbz"
if (mimeType == "application/vnd.comicbook-rar") return "cbr"
if (mimeType == "application/vnd.amazon.mobi8-ebook") return "azw3"
return null
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -74,6 +88,7 @@ open class LibraryItemWrapper(var id:String) {
@JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceInfo(
var deviceId:String,
var manufacturer:String,
var model:String,
var brand:String,
@@ -102,6 +117,8 @@ data class DeviceSettings(
var autoSleepTimer: Boolean,
var autoSleepTimerStartTime: String,
var autoSleepTimerEndTime: String,
var autoSleepTimerAutoRewind: Boolean,
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean
@@ -122,6 +139,8 @@ data class DeviceSettings(
autoSleepTimerStartTime = "22:00",
autoSleepTimerEndTime = "06:00",
sleepTimerLength = 900000L, // 15 minutes
autoSleepTimerAutoRewind = false,
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false
)
@@ -141,6 +160,7 @@ data class DeviceSettings(
@get:JsonIgnore
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
@JsonIgnore
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
@@ -0,0 +1,13 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class EBookFile(
var ino:String,
var metadata:FileMetadata?,
var ebookFormat:String,
var isLocal:Boolean,
var localFileId:String?,
var contentUrl:String?
)
@@ -18,6 +18,7 @@ data class LocalMediaItem(
var basePath:String,
var absolutePath:String,
var audioTracks:MutableList<AudioTrack>,
var ebookFile:EBookFile?,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?
@@ -61,7 +62,7 @@ data class LocalMediaItem(
val mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
val chapters = getAudiobookChapters()
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size)
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,ebookFile,getTotalSize(),getDuration(),audioTracks.size)
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
} else {
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
@@ -13,6 +13,8 @@ class LocalMediaProgress(
progress:Double, // 0 to 1
currentTime:Double,
isFinished:Boolean,
var ebookLocation:String?, // cfi tag
var ebookProgress:Double?, // 0 to 1
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?,
@@ -58,11 +60,20 @@ class LocalMediaProgress(
finishedAt = if (isFinished) lastUpdate else null
}
@JsonIgnore
fun updateEbookProgress(ebookLocation:String, ebookProgress:Double) {
lastUpdate = System.currentTimeMillis()
this.ebookProgress = ebookProgress
this.ebookLocation = ebookLocation
}
@JsonIgnore
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
isFinished = serverMediaProgress.isFinished
progress = serverMediaProgress.progress
currentTime = serverMediaProgress.currentTime
ebookProgress = serverMediaProgress.ebookProgress
ebookLocation = serverMediaProgress.ebookLocation
duration = serverMediaProgress.duration
lastUpdate = serverMediaProgress.lastUpdate
finishedAt = serverMediaProgress.finishedAt
@@ -12,6 +12,8 @@ class MediaProgress(
progress:Double, // 0 to 1
currentTime:Double,
isFinished:Boolean,
var ebookLocation:String?, // cfi tag
var ebookProgress:Double?, // 0 to 1
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?
@@ -264,6 +264,6 @@ class PlaybackSession(
@JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress {
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
}
}
@@ -39,6 +39,10 @@ object DeviceManager {
if (deviceData.deviceSettings?.shakeSensitivity == null) {
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
}
// Initialize auto sleep timer auto rewind added in v0.9.64
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
}
}
fun getBase64Id(id:String):String {
@@ -72,7 +72,7 @@ class FolderScanner(var ctx: Context) {
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
// Do not scan folders that have no media items and not an existing item already
if (existingItem != null || filesInFolder.isNotEmpty()) {
@@ -110,6 +110,8 @@ class FolderScanner(var ctx: Context) {
var startOffset = 0.0
var coverContentUrl:String? = null
var coverAbsolutePath:String? = null
var hasEBookFile = false
var newEBookFile:EBookFile? = null
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
@@ -122,7 +124,6 @@ class FolderScanner(var ctx: Context) {
filesInFolder.forEach { file ->
val mimeType = file.mimeType ?: ""
val filename = file.name ?: ""
val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
val localFileId = DeviceManager.getBase64Id(file.id)
@@ -132,7 +133,7 @@ class FolderScanner(var ctx: Context) {
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
if (isAudio) {
if (localFile.isAudioFile()) {
val audioTrackToAdd:AudioTrack?
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
@@ -174,6 +175,15 @@ class FolderScanner(var ctx: Context) {
startOffset += audioTrackToAdd.duration
index++
audioTracks.add(audioTrackToAdd)
} else if (localFile.isEBookFile()) {
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
if (localFolder.mediaType == "book") {
hasEBookFile = true
if (existingLocalFile == null) {
newEBookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
}
}
} else {
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
@@ -198,7 +208,7 @@ class FolderScanner(var ctx: Context) {
}
}
if (existingItem != null && audioTracks.isEmpty()) {
if (existingItem != null && audioTracks.isEmpty() && !hasEBookFile) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
return ItemScanResult.REMOVED
@@ -210,9 +220,9 @@ class FolderScanner(var ctx: Context) {
existingItem.updateFromScan(audioTracks,localFiles)
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
return ItemScanResult.UPDATED
} else if (audioTracks.isNotEmpty()) {
} else if (audioTracks.isNotEmpty() || newEBookFile != null) {
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,newEBookFile,localFiles,coverContentUrl,coverAbsolutePath)
val localLibraryItem = localMediaItem.getLocalLibraryItem()
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
return ItemScanResult.ADDED
@@ -257,7 +267,7 @@ class FolderScanner(var ctx: Context) {
// Search for files in media item folder
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localEpisodeId:String? = null
@@ -274,6 +284,7 @@ class FolderScanner(var ctx: Context) {
}
val audioTracks:MutableList<AudioTrack> = mutableListOf()
var foundEBookFile = false
filesFound.forEach { docFile ->
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
@@ -304,6 +315,16 @@ class FolderScanner(var ctx: Context) {
localEpisodeId = newEpisode.id
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
}
} else if (itemPart.ebookFile != null) { // Ebook
foundEBookFile = true
Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}")
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.localFiles.add(localFile)
val ebookFile = EBookFile(itemPart.ebookFile.ino, itemPart.ebookFile.metadata, itemPart.ebookFile.ebookFormat, true, localFileId, localFile.contentUrl)
(localLibraryItem.media as Book).ebookFile = ebookFile
Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
} else { // Cover image
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
@@ -314,8 +335,8 @@ class FolderScanner(var ctx: Context) {
}
}
if (audioTracks.isEmpty()) {
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
if (audioTracks.isEmpty() && !foundEBookFile) {
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}")
return cb(null)
}
@@ -350,6 +371,8 @@ class FolderScanner(var ctx: Context) {
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = false,
ebookLocation = mediaProgress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
@@ -381,7 +404,7 @@ class FolderScanner(var ctx: Context) {
var wasUpdated = false
// Search for files in media item folder
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
filesFound.forEach {
@@ -427,7 +450,7 @@ class FolderScanner(var ctx: Context) {
val audioProbeResult = probeAudioFile(localFile.absolutePath)
val existingTrack = existingAudioTracks.find { audioTrack ->
audioTrack.localFileId == localFile.id
audioTrack.localFileId == localFileId
}
if (existingTrack == null) {
@@ -446,6 +469,16 @@ class FolderScanner(var ctx: Context) {
wasUpdated = true
}
} else if (localFile.isEBookFile()) {
if (localLibraryItem.mediaType == "book") {
val existingEbookFile = (localLibraryItem.media as Book).ebookFile
if (existingEbookFile == null || existingEbookFile.localFileId != localFileId) {
val ebookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
(localLibraryItem.media as Book).ebookFile = ebookFile
Log.d(tag, "scanLocalLibraryItem: Ebook file added to lli ${localFile.contentUrl}")
wasUpdated = true
}
}
} else { // Check if cover is empty
if (localLibraryItem.coverContentUrl == null) {
Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}")
@@ -1,6 +1,7 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.media.metrics.PlaybackSession
import android.os.*
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
@@ -21,6 +22,8 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
private var sleepTimerElapsed:Long = 0L
private var sleepTimerFinishedAt:Long = 0L
private var isAutoSleepTimer:Boolean = false // When timer was auto-set
private var isFirstAutoSleepTimer: Boolean = true
private var sleepTimerSessionId:String = ""
private fun getCurrentTime():Long {
return playerNotificationService.getCurrentTime()
@@ -61,6 +64,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
sleepTimerRunning = true
sleepTimerFinishedAt = 0L
sleepTimerElapsed = 0L
setVolume(1f)
// Register shake sensor
playerNotificationService.registerSensor()
@@ -80,10 +84,6 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
} else {
sleepTimerLength = time
sleepTimerEndTime = 0L
if (sleepTimerLength + getCurrentTime() > getDuration()) {
sleepTimerLength = getDuration() - getCurrentTime()
}
}
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
@@ -107,17 +107,14 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
clearSleepTimer()
sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 60) {
if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut == true) {
// Set volume to 1 in case setting was enabled while fading
setVolume(1f)
} else {
// Start fading out audio down to 10% volume
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
val volume = 1f - (percentToReduce * 0.9f)
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
}
} else if (sleepTimeSecondsRemaining <= 60 && DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut != true) {
// Start fading out audio down to 10% volume
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
val volume = 1f - (percentToReduce * 0.9f)
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
} else {
setVolume(1f)
}
}
}
@@ -125,7 +122,8 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
return true
}
fun setManualSleepTimer(time: Long, isChapterTime:Boolean):Boolean {
fun setManualSleepTimer(playbackSessionId:String, time: Long, isChapterTime:Boolean):Boolean {
sleepTimerSessionId = playbackSessionId
isAutoSleepTimer = false
return setSleepTimer(time, isChapterTime)
}
@@ -135,7 +133,6 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
sleepTimerTask = null
sleepTimerEndTime = 0
sleepTimerRunning = false
isAutoSleepTimer = false
playerNotificationService.unregisterSensor()
setVolume(1f)
@@ -229,6 +226,17 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
return
}
// Automatically Rewind in the book if settings is enabled
if (isAutoSleepTimer) {
DeviceManager.deviceData.deviceSettings?.let { deviceSettings ->
if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) {
Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms")
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
}
isFirstAutoSleepTimer = false
}
}
// Set sleep timer
// When sleepTimerLength is 0 then use end of chapter/track time
if (sleepTimerLength == 0L) {
@@ -327,18 +335,31 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
// In cases where end time is earlier then start time then we add a day to end time
// e.g. start time 22:00 and end time 06:00. End time will be treated as 6am the next day.
// e.g. start time 08:00 and end time 22:00. Start and end time will be the same day.
val currentCalendar = Calendar.getInstance()
// In cases where end time is before start time then we shift the time window forward or backward based on the current time.
// e.g. start time 22:00 and end time 06:00.
// If current time is less than start time (e.g. 00:30) then start time will be the previous day.
// If current time is greater than start time (e.g. 23:00) then end time will be the next day.
if (endCalendar.before(startCalendar)) {
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
if (currentCalendar.before(startCalendar)) { // Shift start back a day
startCalendar.add(Calendar.DAY_OF_MONTH, -1)
} else { // Shift end forward a day
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
}
}
val currentCalendar = Calendar.getInstance()
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
// Automatically Rewind in the book if settings is enabled
if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) {
Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms")
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
}
isFirstAutoSleepTimer = false
// Set sleep timer
// When sleepTimerLength is 0 then use end of chapter/track time
if (deviceSettings.sleepTimerLength == 0L) {
@@ -354,13 +375,23 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
setSleepTimer(deviceSettings.sleepTimerLength, false)
}
} else {
isFirstAutoSleepTimer = true
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
}
}
}
fun handleMediaPlayEvent() {
checkShouldResetSleepTimer()
fun handleMediaPlayEvent(playbackSessionId:String) {
// Check if the playback session has changed
// If it hasn't changed OR the sleep timer is running then check reset the timer
// e.g. You set a manual sleep timer for 10 mins, then decide to change books, the sleep timer will stay on and reset to 10 mins
if (sleepTimerSessionId == playbackSessionId || sleepTimerRunning) {
checkShouldResetSleepTimer()
} else {
isFirstAutoSleepTimer = true
}
sleepTimerSessionId = playbackSessionId
checkAutoSleepTimer()
}
}
@@ -4,6 +4,7 @@ import android.app.DownloadManager
import android.net.Uri
import android.util.Log
import com.audiobookshelf.app.data.AudioTrack
import com.audiobookshelf.app.data.EBookFile
import com.audiobookshelf.app.data.LocalFolder
import com.audiobookshelf.app.data.PodcastEpisode
import com.audiobookshelf.app.device.DeviceManager
@@ -20,6 +21,7 @@ data class DownloadItemPart(
val localFolderName: String,
val localFolderUrl: String,
val localFolderId: String,
val ebookFile: EBookFile?,
val audioTrack: AudioTrack?,
val episode: PodcastEpisode?,
var completed:Boolean,
@@ -35,12 +37,12 @@ data class DownloadItemPart(
var bytesDownloaded: Long
) {
companion object {
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
val destinationUri = Uri.fromFile(destinationFile)
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg&raw=1" // For cover images force to jpeg
val downloadUri = Uri.parse(downloadUrl)
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
return DownloadItemPart(
@@ -53,6 +55,7 @@ data class DownloadItemPart(
localFolderName = localFolder.name,
localFolderUrl = localFolder.contentUrl,
localFolderId = localFolder.id,
ebookFile = ebookFile,
audioTrack = audioTrack,
episode = episode,
completed = false,
@@ -7,7 +7,7 @@ import com.audiobookshelf.app.device.DeviceManager
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
//const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
var tag = "PlayerListener"
@@ -74,12 +74,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
Log.d(tag, "SeekBackTime $seekBackTime")
}
// TODO: this needs to be reworked so that the audio doesn't start playing before it checks for updated progress
// Check if playback session still exists or sync media progress if updated
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
if (!shouldCarryOn) return
}
// val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
// if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
// val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
// if (!shouldCarryOn) return
// }
if (seekBackTime > 0L) {
playerNotificationService.seekBackward(seekBackTime)
@@ -92,12 +93,15 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
// Start/stop progress sync interval
if (isPlaying) {
// Handles auto-starting sleep timer and resetting sleep timer
playerNotificationService.sleepTimerManager.handleMediaPlayEvent()
player.volume = 1F // Volume on sleep timer might have decreased this
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) }
playbackSession?.let {
// Handles auto-starting sleep timer and resetting sleep timer
playerNotificationService.sleepTimerManager.handleMediaPlayEvent(it.id)
player.volume = 1F // Volume on sleep timer might have decreased this
playerNotificationService.mediaProgressSyncer.play(it)
}
} else {
playerNotificationService.mediaProgressSyncer.pause {
Log.d(tag, "Media Progress Syncer paused and synced")
@@ -1,5 +1,6 @@
package com.audiobookshelf.app.player
import android.annotation.SuppressLint
import android.app.*
import android.appwidget.AppWidgetManager
import android.content.ComponentName
@@ -16,6 +17,7 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.*
import android.provider.MediaStore
import android.provider.Settings
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
@@ -31,7 +33,6 @@ import androidx.core.content.ContextCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.MediaPlayerWidget
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceInfo
@@ -894,6 +895,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
}
@SuppressLint("HardwareIds")
private fun getDeviceInfo(): DeviceInfo {
/* EXAMPLE
manufacturer: Google
@@ -902,7 +904,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
sdkVersion: 32
appVersion: 0.9.46-beta
*/
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
}
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
@@ -37,7 +37,11 @@ class AbsAudioPlayer : Plugin() {
mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity)
initCastManager()
try {
initCastManager()
} catch(e:Exception) {
Log.e(tag, "initCastManager exception ${e.printStackTrace()}")
}
val foregroundServiceReady : () -> Unit = {
playerNotificationService = mainActivity.foregroundService
@@ -176,7 +180,7 @@ 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")
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
if (libraryItemId.isEmpty()) {
Log.e(tag, "Invalid call to play library item no library item id")
@@ -337,7 +341,8 @@ class AbsAudioPlayer : Plugin() {
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
Handler(Looper.getMainLooper()).post {
val success:Boolean = playerNotificationService.sleepTimerManager.setManualSleepTimer(time, isChapterTime)
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
val success:Boolean = playerNotificationService.sleepTimerManager.setManualSleepTimer(playbackSession?.id ?: "", time, isChapterTime)
val ret = JSObject()
ret.put("success", success)
call.resolve(ret)
@@ -265,6 +265,8 @@ class AbsDatabase : Plugin() {
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = mediaProgress.isFinished,
ebookLocation = mediaProgress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
@@ -345,6 +347,8 @@ class AbsDatabase : Plugin() {
progress = if (isFinished) 1.0 else 0.0,
currentTime = 0.0,
isFinished = isFinished,
ebookLocation = null,
ebookProgress = null,
lastUpdate = currentTime,
startedAt = if (isFinished) currentTime else 0L,
finishedAt = if (isFinished) currentTime else null,
@@ -389,6 +393,55 @@ class AbsDatabase : Plugin() {
}
}
@PluginMethod
fun updateLocalEbookProgress(call:PluginCall) {
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
val ebookLocation = call.getString("ebookLocation", "").toString()
val ebookProgress = call.getDouble("ebookProgress") ?: 0.0
val localMediaProgressId = localLibraryItemId
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
if (localMediaProgress == null) {
Log.d(tag, "updateLocalEbookProgress Local Media Progress not found $localMediaProgressId - Creating new")
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
?: return call.resolve(JSObject("{\"error\":\"Library Item not found\"}"))
val book = localLibraryItem.media as Book
localMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = null,
duration = book.duration ?: 0.0,
progress = 0.0,
currentTime = 0.0,
isFinished = false,
ebookLocation = ebookLocation,
ebookProgress = ebookProgress,
lastUpdate = System.currentTimeMillis(),
startedAt = 0L,
finishedAt = null,
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
serverAddress = localLibraryItem.serverAddress,
serverUserId = localLibraryItem.serverUserId,
libraryItemId = localLibraryItem.libraryItemId,
episodeId = null)
} else {
localMediaProgress.updateEbookProgress(ebookLocation, ebookProgress)
}
// Save local media progress locally
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
Log.d(tag, "updateLocalEbookProgress: Local Media Progress String $lmpstring")
val jsobj = JSObject()
jsobj.put("localMediaProgress", JSObject(lmpstring))
call.resolve(jsobj)
}
@PluginMethod
fun updateLocalTrackOrder(call:PluginCall) {
val localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
@@ -2,7 +2,6 @@ package com.audiobookshelf.app.plugins
import android.app.DownloadManager
import android.content.Context
import android.os.Build
import android.os.Environment
import android.util.Log
import com.audiobookshelf.app.MainActivity
@@ -142,6 +141,28 @@ class AbsDownloader : Plugin() {
val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder"
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
val book = libraryItem.media as Book
book.ebookFile?.let { ebookFile ->
val fileSize = ebookFile.metadata?.size ?: 0
val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(ebookFile.metadata?.relPath ?: "")}"
val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "TEMP ebook file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "ebook file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
// Create download item part for each audio track
tracks.forEach { audioTrack ->
val fileSize = audioTrack.metadata?.size ?: 0
@@ -162,7 +183,7 @@ class AbsDownloader : Plugin() {
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null)
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
@@ -187,7 +208,7 @@ class AbsDownloader : Plugin() {
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null)
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
@@ -216,7 +237,7 @@ class AbsDownloader : Plugin() {
finalDestinationFile.delete()
}
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode)
downloadItem.downloadItemParts.add(downloadItemPart)
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
@@ -232,7 +253,7 @@ class AbsDownloader : Plugin() {
if (finalDestinationFile.exists()) {
Log.d(tag, "Podcast cover already exists - not downloading cover again")
} else {
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
}
@@ -23,6 +23,7 @@ import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
@CapacitorPlugin(name = "AbsFileSystem")
class AbsFileSystem : Plugin() {
@@ -226,8 +227,37 @@ class AbsFileSystem : Plugin() {
val contentUrl = call.data.getString("contentUrl", "").toString()
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
var subfolderPathToDelete = ""
// Check if should delete subfolder
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
localLibraryItem?.folderId?.let { folderId ->
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
folder?.absolutePath?.let { folderPath ->
val splitAbsolutePath = absolutePath.split("/")
val fullSubDir = splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
if (fullSubDir != folderPath) {
val subdirHasAnItem = DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
if (_localLibraryItem.id == localLibraryItemId) {
false
} else {
_localLibraryItem.absolutePath.startsWith(fullSubDir)
}
}
subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir
}
}
}
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
val success = docfile?.delete() == true
if (subfolderPathToDelete != "") {
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
val docfilesub = DocumentFileCompat.fromFullPath(mainActivity, subfolderPathToDelete)
docfilesub?.delete()
}
if (success) {
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
}
@@ -356,6 +356,16 @@ class ApiHandler(var ctx:Context) {
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}")
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
// Patch ebook progress to server
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")
}
}
}
}
+14
View File
@@ -117,6 +117,20 @@ Bookshelf Label
filter: blur(20px);
}
.episode-subtitle {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 32px;
/* fallback */
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
+7 -3
View File
@@ -12,7 +12,7 @@
<div class="top-6 right-4 absolute cursor-pointer">
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
</div>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
</div>
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
@@ -60,13 +60,13 @@
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
</div>
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="showChapterModal = true">format_list_bulleted</span>
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
</div>
</div>
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" />
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center justify-between max-w-full">
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
@@ -369,6 +369,10 @@ export default {
}
},
methods: {
clickChaptersBtn() {
if (!this.chapters.length) return
this.showChapterModal = true
},
async coverImageLoaded(fullCoverUrl) {
if (!fullCoverUrl) return
+2
View File
@@ -214,6 +214,8 @@ export default {
if (startTime !== undefined && startTime !== null) {
// seek to start time
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
} else if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.play()
}
return
}
-20
View File
@@ -7,26 +7,6 @@
<p class="truncate text-sm">{{ name }}</p>
</div>
</div>
<!-- <div class="flex h-full px-1 overflow-hidden shadow-sm">
<div style="max-height: 48px; max-width: 48px" class="w-12 h-12 bg-primary overflow-hidden rounded">
<svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
<path
d="M105.87 29.6508C80.857 17.6515 50.8784 28.1923 38.879 53.2056C26.8797 78.219 37.4205 108.198 62.4338 120.197C87.4472 132.196 117.426 121.656 129.425 96.6422C141.425 71.6288 130.884 41.6502 105.87 29.6508ZM106.789 85.783C112.761 73.3329 107.461 58.2599 95.0112 52.2874C82.5611 46.3148 67.4881 51.6147 61.5156 64.0648C55.543 76.5149 60.8429 91.5879 73.293 97.5604C85.7431 103.533 100.816 98.2331 106.789 85.783Z"
fill="white"
/>
<path
d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01ZM181.725 108.497C179.624 108.491 177.436 109.326 175.835 110.918L160.415 126.257L191.848 157.856L207.268 142.517C210.554 139.248 210.568 133.954 207.299 130.667L187.685 110.95C186.009 109.264 183.91 108.502 181.725 108.497ZM151.399 135.226L58.2034 227.931L58.1203 259.447L89.6359 259.53L182.831 166.825L151.399 135.226Z"
fill="white"
/>
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
</svg>
</div>
<div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ author }}</p>
</div>
</div> -->
</template>
<script>
+4 -1
View File
@@ -10,7 +10,7 @@
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div>
</div>
</template>
@@ -78,11 +78,14 @@ export default {
var lastPart = this.matchText.substr(totalLenSoFar)
html += lastPart
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
if (this.matchKey === 'narrators') return `<p class="truncate">Narrator: ${html}</p>`
return `${html}`
}
},
+11 -4
View File
@@ -62,8 +62,10 @@
</div>
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
<div v-if="recentEpisodeNumber !== null && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
@@ -221,7 +223,7 @@ export default {
if (this.recentEpisode.episode) {
return this.recentEpisode.episode.replace(/^#/, '')
}
return this.recentEpisode.index
return ''
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
@@ -274,8 +276,13 @@ export default {
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
useEBookProgress() {
if (!this.userProgress || this.userProgress.progress) return false
return this.userProgress.ebookProgress > 0
},
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0)
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0
},
itemIsFinished() {
return this.userProgress ? !!this.userProgress.isFinished : false
+23
View File
@@ -5,6 +5,8 @@
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
@@ -53,6 +55,27 @@ export default {
books() {
return this.series ? this.series.books || [] : []
},
seriesBookProgress() {
return this.books
.map((libraryItem) => {
return this.store.getters['user/getUserMediaProgress'](libraryItem.id)
})
.filter((p) => !!p)
},
seriesBooksFinished() {
return this.seriesBookProgress.filter((p) => p.isFinished)
},
hasSeriesBookInProgress() {
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
},
seriesPercentInProgress() {
let totalFinishedAndInProgress = this.seriesBooksFinished.length
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
},
isSeriesFinished() {
return this.books.length === this.seriesBooksFinished.length
},
store() {
return this.$store || this.$nuxt.$store
},
+34
View File
@@ -0,0 +1,34 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
</div>
<div class="flex-grow px-2 narratorSearchCardContent h-full">
<p class="truncate text-sm">{{ narrator }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
narrator: String
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
<style scoped>
.narratorSearchCardContent {
width: calc(100% - 40px);
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
@@ -0,0 +1,95 @@
<template>
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<p class="text-white text-2xl truncate">Auto Rewind Time</p>
</div>
</template>
<div
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
@click="
show = false
manualTimerModal = false
"
>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div v-if="manualTimerModal" class="p-4">
<div class="flex mb-4" @click="manualTimerModal = false">
<span class="material-icons text-3xl">arrow_back</span>
</div>
<div class="flex my-2 justify-between">
<ui-btn @click="decreaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">remove</span></ui-btn>
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
</div>
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="timeout in timeouts">
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg">{{ timeout }} min</span>
</div>
</li>
</template>
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
<div class="flex items-center justify-center">
<span class="font-normal block truncate text-lg text-center">Custom time</span>
</div>
</li>
</ul>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
manualTimerModal: null,
manualTimeoutMin: 1
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
timeouts() {
return [5, 10, 15, 30, 45, 60, 90]
}
},
methods: {
async clickedChapterOption() {
await this.$hapticsImpact()
this.show = false
this.$nextTick(() => this.$emit('change', 0))
},
async clickedOption(timeoutMin) {
await this.$hapticsImpact()
const timeout = timeoutMin * 1000 * 60
this.show = false
this.manualTimerModal = false
this.$nextTick(() => this.$emit('change', timeout))
},
async increaseManualTimeout() {
await this.$hapticsImpact()
this.manualTimeoutMin++
},
async decreaseManualTimeout() {
await this.$hapticsImpact()
if (this.manualTimeoutMin > 1) this.manualTimeoutMin--
}
},
mounted() {}
}
</script>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" width="90%" height="100%">
<template #outer>
<div v-show="selected !== 'all'" class="absolute top-6 left-4 z-40">
<div v-show="selected !== 'all'" class="absolute top-10 left-4 z-40">
<ui-btn class="text-xl border-yellow-400 border-opacity-40" @click="clearSelected">Clear</ui-btn>
</div>
</template>
+1 -1
View File
@@ -2,7 +2,7 @@
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
<div class="absolute z-40 top-6 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
<div class="absolute z-40 top-10 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
<span class="material-icons text-4xl">close</span>
</div>
<slot name="outer" />
+21 -1
View File
@@ -22,7 +22,8 @@ export default {
props: {
value: Boolean,
orderBy: String,
descending: Boolean
descending: Boolean,
episodes: Boolean
},
data() {
return {
@@ -89,6 +90,24 @@ export default {
text: 'File Modified',
value: 'mtimeMs'
}
],
episodeItems: [
{
text: 'Pub Date',
value: 'publishedAt'
},
{
text: 'Title',
value: 'title'
},
{
text: 'Season',
value: 'season'
},
{
text: 'Episode',
value: 'episode'
}
]
}
},
@@ -121,6 +140,7 @@ export default {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
},
items() {
if (this.episodes) return this.episodeItems
if (this.isPodcast) return this.podcastItems
return this.bookItems
}
+210 -61
View File
@@ -1,6 +1,7 @@
<template>
<div id="epub-frame" class="w-full">
<div id="viewer" class="border border-gray-100 bg-white shadow-md h-full w-full"></div>
<div id="viewer" class="h-full w-full"></div>
<div class="fixed left-0 h-8 w-full bg-bg px-2 flex items-center" :style="{ bottom: playerLibraryItemId ? '120px' : '0px' }">
<p class="text-xs">epub</p>
<div class="flex-grow" />
@@ -15,18 +16,20 @@ import ePub from 'epubjs'
export default {
props: {
url: String
url: String,
libraryItem: {
type: Object,
default: () => {}
},
isLocal: Boolean
},
data() {
return {
/** @type {ePub.Book} */
book: null,
/** @type {ePub.Rendition} */
rendition: null,
chapters: [],
title: '',
author: '',
progress: 0,
hasNext: true,
hasPrev: false
progress: 0
}
},
watch: {
@@ -35,11 +38,48 @@ export default {
}
},
computed: {
/** @returns {string} */
libraryItemId() {
return this.libraryItem?.id
},
localLibraryItem() {
if (this.isLocal) return this.libraryItem
return this.libraryItem.localLibraryItem || null
},
localLibraryItemId() {
return this.localLibraryItem?.id
},
serverLibraryItemId() {
if (!this.isLocal) return this.libraryItem.id
// Check if local library item is connected to the current server
if (!this.libraryItem.serverAddress || !this.libraryItem.libraryItemId) return null
if (this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress) {
return this.libraryItem.libraryItemId
}
return null
},
playerLibraryItemId() {
return this.$store.state.playerLibraryItemId
},
readerHeightOffset() {
return this.playerLibraryItemId ? 196 : 96
},
/** @returns {Array<ePub.NavItem>} */
chapters() {
return this.book ? this.book.navigation.toc : []
},
userItemProgress() {
if (this.isLocal) return this.localItemProgress
return this.serverItemProgress
},
localItemProgress() {
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId)
},
serverItemProgress() {
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
},
localStorageLocationsKey() {
return `ebookLocations-${this.libraryItemId}`
}
},
methods: {
@@ -58,71 +98,180 @@ export default {
this.rendition.next()
}
},
initEpub() {
var book = ePub(this.url)
this.book = book
/**
* @param {object} payload
* @param {string} payload.ebookLocation - CFI of the current location
* @param {string} payload.ebookProgress - eBook Progress Percentage
*/
async updateProgress(payload) {
// Update local item
if (this.localLibraryItemId) {
const localPayload = {
localLibraryItemId: this.localLibraryItemId,
...payload
}
const localResponse = await this.$db.updateLocalEbookProgress(localPayload)
if (localResponse.localMediaProgress) {
this.$store.commit('globals/updateLocalMediaProgress', localResponse.localMediaProgress)
}
}
this.rendition = book.renderTo('viewer', {
// Update server item
if (this.serverLibraryItemId) {
this.$axios.$patch(`/api/me/progress/${this.serverLibraryItemId}`, payload).catch((error) => {
console.error('EpubReader.updateProgress failed:', error)
})
}
},
getAllEbookLocationData() {
const locations = []
let totalSize = 0 // Total in bytes
for (const key in localStorage) {
if (!localStorage.hasOwnProperty(key) || !key.startsWith('ebookLocations-')) {
continue
}
try {
const ebookLocations = JSON.parse(localStorage[key])
if (!ebookLocations.locations) throw new Error('Invalid locations object')
ebookLocations.key = key
ebookLocations.size = (localStorage[key].length + key.length) * 2
locations.push(ebookLocations)
totalSize += ebookLocations.size
} catch (error) {
console.error('Failed to parse ebook locations', key, error)
localStorage.removeItem(key)
}
}
// Sort by oldest lastAccessed first
locations.sort((a, b) => a.lastAccessed - b.lastAccessed)
return {
locations,
totalSize
}
},
/** @param {string} locationString */
checkSaveLocations(locationString) {
const maxSizeInBytes = 3000000 // Allow epub locations to take up to 3MB of space
const newLocationsSize = JSON.stringify({ lastAccessed: Date.now(), locations: locationString }).length * 2
// Too large overall
if (newLocationsSize > maxSizeInBytes) {
console.error('Epub locations are too large to store. Size =', newLocationsSize)
return
}
const ebookLocationsData = this.getAllEbookLocationData()
let availableSpace = maxSizeInBytes - ebookLocationsData.totalSize
// Remove epub locations until there is room for locations
while (availableSpace < newLocationsSize && ebookLocationsData.locations.length) {
const oldestLocation = ebookLocationsData.locations.shift()
console.log(`Removing cached locations for epub "${oldestLocation.key}" taking up ${oldestLocation.size} bytes`)
availableSpace += oldestLocation.size
localStorage.removeItem(oldestLocation.key)
}
console.log(`Cacheing epub locations with key "${this.localStorageLocationsKey}" taking up ${newLocationsSize} bytes`)
this.saveLocations(locationString)
},
/** @param {string} locationString */
saveLocations(locationString) {
localStorage.setItem(
this.localStorageLocationsKey,
JSON.stringify({
lastAccessed: Date.now(),
locations: locationString
})
)
},
loadLocations() {
const locationsObjString = localStorage.getItem(this.localStorageLocationsKey)
if (!locationsObjString) return null
const locationsObject = JSON.parse(locationsObjString)
// Remove invalid location objects
if (!locationsObject.locations) {
console.error('Invalid epub locations stored', this.localStorageLocationsKey)
localStorage.removeItem(this.localStorageLocationsKey)
return null
}
// Update lastAccessed
this.saveLocations(locationsObject.locations)
return locationsObject.locations
},
/** @param {string} location - CFI of the new location */
relocated(location) {
if (this.userItemProgress?.ebookLocation === location.start.cfi) {
return
}
if (location.end.percentage) {
this.updateProgress({
ebookLocation: location.start.cfi,
ebookProgress: location.end.percentage
})
this.progress = Math.round(location.end.percentage * 100)
} else {
this.updateProgress({
ebookLocation: location.start.cfi
})
}
},
initEpub() {
this.progress = Math.round((this.userItemProgress?.ebookProgress || 0) * 100)
/** @type {EpubReader} */
const reader = this
/** @type {ePub.Book} */
reader.book = new ePub(reader.url, {
width: window.innerWidth,
height: window.innerHeight - this.readerHeightOffset
})
/** @type {ePub.Rendition} */
reader.rendition = reader.book.renderTo('viewer', {
width: window.innerWidth,
height: window.innerHeight - this.readerHeightOffset,
snap: true,
manager: 'continuous',
flow: 'paginated'
})
const displayed = this.rendition.display()
book.ready
.then(() => {
console.log('Book ready')
return book.locations.generate(1600)
})
.then((locations) => {
// console.log('Loaded locations', locations)
// Wait for book to be rendered to get current page
displayed.then(() => {
// Get the current CFI
var currentLocation = this.rendition.currentLocation()
if (!currentLocation.start) {
console.error('No Start', currentLocation)
} else {
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
console.log('current page', currentPage)
}
// load saved progress
reader.rendition.display(this.userItemProgress?.ebookLocation || reader.book.locations.start)
// load style
reader.rendition.themes.default({ '*': { color: '#fff!important' } })
reader.book.ready.then(() => {
// set up event listeners
reader.rendition.on('relocated', reader.relocated)
// load ebook cfi locations
const savedLocations = this.loadLocations()
if (savedLocations) {
reader.book.locations.load(savedLocations)
} else {
reader.book.locations.generate().then(() => {
this.checkSaveLocations(reader.book.locations.save())
})
})
book.loaded.navigation.then((toc) => {
var _chapters = []
toc.forEach((chapter) => {
_chapters.push(chapter)
})
this.chapters = _chapters
})
// book.loaded.metadata.then((metadata) => {
// this.author = metadata.creator
// this.title = metadata.title
// })
// const spine_get = book.spine.get.bind(book.spine)
// book.spine.get = function (target) {
// var t = spine_get(target)
// console.log(t, target)
// // while (t == null && target.includes('#')) {
// // target = target.split('#')[0]
// // t = spine_get(target)
// // }
// return t
// }
this.rendition.on('relocated', (location) => {
var percent = book.locations.percentageFromCfi(location.start.cfi)
this.progress = Math.floor(percent * 100)
this.hasNext = !location.atEnd
this.hasPrev = !location.atStart
}
})
}
},
beforeDestroy() {
this.book?.destroy()
},
mounted() {
this.initEpub()
}
+14 -4
View File
@@ -5,11 +5,13 @@
<div class="flex-grow" />
<span class="material-icons text-xl text-white" @click.stop="show = false">close</span>
</div>
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" />
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" :library-item="selectedLibraryItem" :is-local="isLocal" />
</div>
</template>
<script>
import { Capacitor } from '@capacitor/core'
export default {
data() {
return {
@@ -66,11 +68,10 @@ export default {
return this.selectedLibraryItem ? this.selectedLibraryItem.libraryId : null
},
ebookFile() {
return this.media ? this.media.ebookFile : null
return this.media?.ebookFile || null
},
ebookFormat() {
if (!this.ebookFile) return null
return this.ebookFile.ebookFormat
return this.ebookFile?.ebookFormat || null
},
ebookType() {
if (this.isMobi) return 'mobi'
@@ -91,8 +92,17 @@ export default {
isComic() {
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
},
isLocal() {
return !!this.ebookFile?.isLocal
},
localContentUrl() {
return this.ebookFile?.contentUrl
},
ebookUrl() {
if (!this.ebookFile) return null
if (this.localContentUrl) {
return Capacitor.convertFileSrc(this.localContentUrl)
}
let filepath = ''
if (this.selectedLibraryItem.isFile) {
filepath = this.$encodeUriPath(this.ebookFile.metadata.filename)
+1 -1
View File
@@ -19,7 +19,7 @@
</tr>
<template v-for="track in tracks">
<tr :key="track.index">
<td>{{ track.metadata.filename }}</td>
<td>{{ (track.metadata && track.metadata.filename) || track.title || 'Unknown' }}</td>
<td class="font-mono text-center w-16">
{{ $secondsToTimestamp(track.duration) }}
</td>
+30 -15
View File
@@ -1,16 +1,21 @@
<template>
<div class="w-full px-2 py-2 overflow-hidden relative">
<nuxt-link v-if="book" :to="`/item/${book.id}`" class="flex w-full">
<nuxt-link v-if="book" :to="`/item/${book.id}`" class="flex items-center w-full">
<div class="h-full relative" :style="{ width: bookWidth + 'px' }">
<covers-book-cover :library-item="book" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="flex-grow book-table-content h-full px-2 flex items-center">
<div class="book-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ bookTitle }}</p>
<p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p>
<p class="text-xxs text-gray-500">{{ bookDuration }}</p>
</div>
</div>
<div class="w-8 min-w-8 flex justify-center">
<button v-if="showPlayBtn" class="w-8 h-8 rounded-full border border-white border-opacity-20 flex items-center justify-center" @click.stop.prevent="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
</button>
</div>
</nuxt-link>
</div>
</template>
@@ -31,6 +36,9 @@ export default {
}
},
computed: {
libraryItemId() {
return this.book.id
},
media() {
return this.book.media || {}
},
@@ -59,22 +67,29 @@ export default {
isMissing() {
return this.book.isMissing
},
isIncomplete() {
return this.book.isIncomplete
},
numTracks() {
return this.book.numTracks
},
isStreaming() {
return this.$store.getters['getIsItemStreaming'](this.book.id)
isInvalid() {
return this.book.isInvalid
},
showPlayBtn() {
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks
return !this.isMissing && !this.isInvalid && this.tracks.length
},
isStreaming() {
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId)
},
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
}
},
methods: {
clickEdit() {
this.$emit('edit', this.book)
async playClick() {
await this.$hapticsImpact()
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId
})
}
}
},
mounted() {}
@@ -83,7 +98,7 @@ export default {
<style>
.book-table-content {
width: calc(100% - 50px);
max-width: calc(100% - 50px);
width: calc(100% - 82px);
max-width: calc(100% - 82px);
}
</style>
+29 -9
View File
@@ -1,16 +1,21 @@
<template>
<div class="w-full px-2 py-2 overflow-hidden relative">
<nuxt-link v-if="libraryItem" :to="`/item/${libraryItem.id}`" class="flex w-full">
<nuxt-link v-if="libraryItem" :to="`/item/${libraryItem.id}`" class="flex items-center w-full">
<div class="h-full relative" :style="{ width: '50px' }">
<covers-book-cover :library-item="libraryItem" :width="50" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="flex-grow item-table-content h-full px-2 flex items-center">
<div class="item-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ itemTitle }}</p>
<p class="truncate block text-gray-400 text-xs">{{ bookAuthorName }}</p>
<p class="text-xxs text-gray-500">{{ itemDuration }}</p>
</div>
</div>
<div class="w-8 min-w-8 flex justify-center">
<button v-if="showPlayBtn" class="w-8 h-8 rounded-full border border-white border-opacity-20 flex items-center justify-center" @click.stop.prevent="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
</button>
</div>
</nuxt-link>
</div>
</template>
@@ -83,21 +88,36 @@ export default {
isInvalid() {
return this.libraryItem.isInvalid
},
isStreaming() {
return this.$store.getters['getIsItemStreaming'](this.item.id)
},
showPlayBtn() {
return !this.isMissing && !this.isInvalid && !this.isStreaming && (this.tracks.length || this.episode)
return !this.isMissing && !this.isInvalid && (this.tracks.length || this.episode)
},
isStreaming() {
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItem.id, this.episodeId)
},
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
}
},
methods: {
async playClick() {
await this.$hapticsImpact()
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItem.id,
episodeId: this.episodeId
})
}
}
},
methods: {},
mounted() {}
}
</script>
<style>
.item-table-content {
width: calc(100% - 50px);
max-width: calc(100% - 50px);
width: calc(100% - 82px);
max-width: calc(100% - 82px);
}
</style>
+21 -7
View File
@@ -17,7 +17,15 @@
{{ title }}
</p>
<p class="text-sm text-gray-200 line-clamp-2 mt-1.5 mb-0.5">{{ subtitle }}</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<div v-if="episodeNumber || season || episodeType" class="flex py-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
</div>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
@@ -90,7 +98,17 @@ export default {
return this.episode.title || ''
},
subtitle() {
return this.episode.subtitle || ''
return this.episode.subtitle || this.episode.description || ''
},
episodeNumber() {
return this.episode.episode
},
season() {
return this.episode.season
},
episodeType() {
if (this.episode.episodeType === 'full') return null // only show Trailer/Bonus
return this.episode.episodeType
},
duration() {
return this.$secondsToTimestamp(this.episode.duration)
@@ -260,11 +278,7 @@ export default {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
var lmp = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
console.log('toggleFinished Check LMP', this.libraryItemId, this.episode.id, JSON.stringify(lmp))
var serverUpdated = payload.server
if (serverUpdated) {
if (payload.server) {
this.$toast.success(`Local & Server Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
} else {
this.$toast.success(`Local Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
+18 -7
View File
@@ -49,6 +49,8 @@
<modals-dialog v-model="showFiltersModal" title="Episode Filter" :items="filterItems" :selected="filterKey" @action="setFilter" />
<modals-podcast-episodes-feed-modal v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
<modals-order-modal v-model="showSortModal" :order-by.sync="sortKey" :descending.sync="sortDesc" episodes />
</div>
</template>
@@ -76,6 +78,7 @@ export default {
return {
episodesCopy: [],
showFiltersModal: false,
showSortModal: false,
sortKey: 'publishedAt',
sortDesc: true,
filterKey: 'incomplete',
@@ -167,10 +170,19 @@ export default {
},
episodesSorted() {
return this.episodesFiltered.sort((a, b) => {
if (this.sortDesc) {
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
let aValue = a[this.sortKey]
let bValue = b[this.sortKey]
// Sort episodes with no pub date as the oldest
if (this.sortKey === 'publishedAt') {
if (!aValue) aValue = Number.MAX_VALUE
if (!bValue) bValue = Number.MAX_VALUE
}
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
if (this.sortDesc) {
return String(bValue).localeCompare(String(aValue), undefined, { numeric: true, sensitivity: 'base' })
}
return String(aValue).localeCompare(String(bValue), undefined, { numeric: true, sensitivity: 'base' })
})
},
// Map of local episodes where server episode id is key
@@ -185,9 +197,8 @@ export default {
},
sortText() {
if (!this.sortKey) return ''
var _sel = this.episodeSortItems.find((i) => i.value === this.sortKey)
if (!_sel) return ''
return _sel.text
const _sel = this.episodeSortItems.find((i) => i.value === this.sortKey)
return _sel?.text || ''
}
},
methods: {
@@ -249,7 +260,7 @@ export default {
this.showFiltersModal = true
},
clickSort() {
this.sortDesc = !this.sortDesc
this.showSortModal = true
},
getEpisodeProgress(episode) {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, episode.id)
+22 -7
View File
@@ -16,7 +16,16 @@
<p class="text-sm font-semibold">
{{ title }}
</p>
<p class="text-sm text-gray-200 line-clamp-2 mt-1.5 mb-0.5">{{ subtitle }}</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle" />
<div v-if="episodeNumber || season || episodeType" class="flex pt-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-50 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
</div>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
@@ -92,7 +101,17 @@ export default {
return this.episode.title || ''
},
subtitle() {
return this.episode.subtitle || ''
return this.episode.subtitle || this.episode.description || ''
},
episodeNumber() {
return this.episode.episode
},
season() {
return this.episode.season
},
episodeType() {
if (this.episode.episodeType === 'full') return null // only show Trailer/Bonus
return this.episode.episodeType
},
duration() {
return this.$secondsToTimestamp(this.episode.duration)
@@ -265,11 +284,7 @@ export default {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
var lmp = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
console.log('toggleFinished Check LMP', this.libraryItemId, this.episode.id, JSON.stringify(lmp))
var serverUpdated = payload.server
if (serverUpdated) {
if (payload.server) {
this.$toast.success(`Local & Server Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
} else {
this.$toast.success(`Local Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
+4 -4
View File
@@ -720,12 +720,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 21;
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.63;
MARKETING_VERSION = 0.9.64;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -744,12 +744,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20;
CURRENT_PROJECT_VERSION = 21;
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.63;
MARKETING_VERSION = 0.9.64;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -102,12 +102,6 @@
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "icon-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "icon-120.png",
"idiom" : "car",
@@ -120,6 +114,72 @@
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "icon-16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon-32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon-32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon-64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon-128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon-256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon-256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon-512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon-512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon-1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{
"filename" : "icon-48.png",
"idiom" : "watch",
@@ -195,6 +255,13 @@
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"filename" : "icon-172.png",
"idiom" : "watch",
@@ -226,71 +293,18 @@
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"filename" : "icon-1024.png",
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "icon-16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon-32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon-32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon-64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon-128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon-256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon-256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon-512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon-512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon-1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
+5 -5
View File
@@ -22,6 +22,11 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSUserActivityTypes</key>
<array>
<string>INPlayMediaIntent</string>
@@ -53,11 +58,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
@@ -44,6 +44,7 @@ class PlaybackSession: Object, Codable, Deletable {
}
}
return [
"deviceId": UIDevice.current.identifierForVendor?.uuidString,
"manufacturer": "Apple",
"model": modelCode,
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
+3
View File
@@ -88,6 +88,9 @@ class AudioPlayer: NSObject {
for track in playbackSession.audioTracks {
if let playerAsset = createAsset(itemId: playbackSession.libraryItemId!, track: track) {
let playerItem = AVPlayerItem(asset: playerAsset)
if (playbackSession.playMethod == PlayMethod.transcode.rawValue) {
playerItem.preferredForwardBufferDuration = 50
}
self.allPlayerItems.append(playerItem)
}
}
+1
View File
@@ -165,6 +165,7 @@ class ApiClient {
"forceTranscode": forceTranscode ? "1" : "",
"mediaPlayer": "AVPlayer",
"deviceInfo": [
"deviceId": UIDevice.current.identifierForVendor?.uuidString,
"manufacturer": "Apple",
"model": modelCode,
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-app",
"version": "0.9.63-beta",
"version": "0.9.64-beta",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-app",
"version": "0.9.63-beta",
"version": "0.9.64-beta",
"dependencies": {
"@capacitor/android": "^4.0.0",
"@capacitor/app": "^4.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.63-beta",
"version": "0.9.64-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
-58
View File
@@ -37,7 +37,6 @@ export default {
data() {
return {
shelves: [],
isSettingsLoaded: false,
isFirstNetworkConnection: true,
lastServerFetch: 0,
lastServerFetchLibraryId: null,
@@ -258,11 +257,6 @@ export default {
return cat
})
// TODO: iOS has its own implementation of this. Android & iOS should be consistent here.
if (!this.isIos) {
this.openMediaPlayerWithMostRecentListening()
}
// Only add the local shelf with the same media type
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
this.shelves.push(...localShelves)
@@ -277,53 +271,6 @@ export default {
this.isLoading = false
},
async waitForSettings() {
// Wait up to 3 seconds
for (let i = 0; i < 6; i++) {
if (this.isSettingsLoaded) return true
await new Promise((resolve) => setTimeout(resolve, 500))
}
return false
},
async openMediaPlayerWithMostRecentListening() {
// If we don't already have a player open
// Try opening the first book from continue-listening without playing it
if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
// Wait for settings to load to prevent race condition when setting playback speed.
if (!this.isSettingsLoaded) {
await this.waitForSettings()
}
const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening')
const mostRecentEntity = continueListeningShelf?.entities?.[0]
if (mostRecentEntity) {
const playObject = {
libraryItemId: mostRecentEntity.id,
episodeId: mostRecentEntity.recentEpisode?.id || null,
paused: true
}
// Check if there is a local copy
if (mostRecentEntity.localLibraryItem) {
if (mostRecentEntity.recentEpisode) {
// Check if the podcast episode has a local copy
const localEpisode = mostRecentEntity.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === mostRecentEntity.recentEpisode.id)
if (localEpisode) {
playObject.libraryItemId = mostRecentEntity.localLibraryItem.id
playObject.episodeId = localEpisode.id
playObject.serverLibraryItemId = mostRecentEntity.id
playObject.serverEpisodeId = mostRecentEntity.recentEpisode.id
}
} else {
playObject.libraryItemId = mostRecentEntity.localLibraryItem.id
playObject.serverLibraryItemId = mostRecentEntity.id
}
}
this.$eventBus.$emit('play-item', playObject)
}
},
libraryChanged() {
if (this.currentLibraryId) {
console.log(`[categories] libraryChanged so fetching categories`)
@@ -370,16 +317,11 @@ export default {
}
})
},
settingsUpdated() {
this.isSettingsLoaded = true
},
initListeners() {
this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('user-settings', this.settingsUpdated)
},
removeListeners() {
this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('user-settings', this.settingsUpdated)
}
},
mounted() {
+68 -24
View File
@@ -16,6 +16,14 @@
{{ title }}
</p>
<div v-if="episodeNumber || season || episodeType" class="flex py-2 items-center -mx-0.5">
<div v-if="episodeNumber" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-gray-200">Episode #{{ episodeNumber }}</div>
<div v-if="season" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-gray-200">Season #{{ season }}</div>
<div v-if="episodeType" class="px-2 pt-px pb-0.5 mx-0.5 bg-primary bg-opacity-60 rounded-full text-xs font-light text-gray-200 capitalize">
{{ episodeType }}
</div>
</div>
<!-- user progress card -->
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
@@ -25,9 +33,9 @@
<!-- action buttons -->
<div class="flex mt-4 -mx-1">
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? (playerIsLocal ? 'Playing' : 'Streaming') : localEpisodeId ? 'Play' : 'Stream' }}</span>
<ui-btn color="success" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<span class="material-icons">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? 'Pause' : localEpisodeId ? 'Play' : 'Stream' }}</span>
</ui-btn>
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mx-1" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
@@ -136,10 +144,23 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
localLibraryItemId() {
if (this.localLibraryItem) return this.localLibraryItem.id
isConnectedToServer() {
if (!this.isLocal) return true
if (!this.libraryItem.serverAddress) return false
return this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress
},
serverLibraryItemId() {
if (!this.isLocal) return this.libraryItemId
// Check if local library item is connected to the current server
if (!this.libraryItem.libraryItemId) return null
if (this.isConnectedToServer) {
return this.libraryItem.libraryItemId
}
return null
},
localLibraryItemId() {
return this.localLibraryItem?.id || null
},
localEpisode() {
if (this.isLocal) return this.episode
return this.episode.localEpisode
@@ -148,6 +169,13 @@ export default {
if (this.localEpisode) return this.localEpisode.id
return null
},
serverEpisodeId() {
if (!this.isLocal) return this.episode.id
if (this.isConnectedToServer) {
return this.episode.serverEpisodeId
}
return null
},
podcast() {
return this.libraryItem.media
},
@@ -160,6 +188,16 @@ export default {
description() {
return this.episode.description || ''
},
episodeNumber() {
return this.episode.episode
},
season() {
return this.episode.season
},
episodeType() {
if (this.episode.episodeType === 'full') return null // only show Trailer/Bonus
return this.episode.episodeType
},
duration() {
return this.$secondsToTimestamp(this.episode.duration)
},
@@ -184,16 +222,16 @@ export default {
return this.$store.state.playerIsPlaying && this.isPlaying
},
userItemProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
if (this.isLocal) return this.localItemProgress
return this.serverItemProgress
},
localMediaProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
else if (this.localLibraryItemId && this.localEpisodeId) {
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId, this.localEpisodeId)
} else {
return null
}
localItemProgress() {
if (!this.localLibraryItemId || !this.localEpisodeId) return null
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId, this.localEpisodeId)
},
serverItemProgress() {
if (!this.serverLibraryItemId || !this.serverEpisodeId) return null
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId, this.serverEpisodeId)
},
progressPercent() {
return this.userItemProgress ? this.userItemProgress.progress : 0
@@ -275,13 +313,15 @@ export default {
this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItemId,
episodeId: this.localEpisodeId,
serverLibraryItemId: this.libraryItemId,
serverEpisodeId: this.episode.id
serverLibraryItemId: this.serverLibraryItemId,
serverEpisodeId: this.serverEpisodeId
})
} else {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: this.episode.id
episodeId: this.episode.id,
serverLibraryItemId: this.serverLibraryItemId,
serverEpisodeId: this.serverEpisodeId
})
}
}
@@ -377,22 +417,26 @@ export default {
})
if (value) {
this.resettingProgress = true
if (this.isLocal) {
// TODO: If connected to server also sync with server
await this.$db.removeLocalMediaProgress(this.localMediaProgress.id)
this.$store.commit('globals/removeLocalMediaProgressForItem', this.localMediaProgress.id)
} else {
const serverItemProgressId = this.serverItemProgress?.id
if (this.localItemProgress) {
await this.$db.removeLocalMediaProgress(this.localItemProgress.id)
this.$store.commit('globals/removeLocalMediaProgress', this.localItemProgress.id)
}
if (serverItemProgressId) {
await this.$axios
.$delete(`/api/me/progress/${this.userItemProgress.id}`)
.$delete(`/api/me/progress/${serverItemProgressId}`)
.then(() => {
console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.$store.commit('user/removeMediaProgress', this.userItemProgress.id)
this.$store.commit('user/removeMediaProgress', serverItemProgressId)
})
.catch((error) => {
console.error('Progress reset failed', error)
})
}
this.resettingProgress = false
}
},
+69 -51
View File
@@ -16,7 +16,7 @@
</div>
</div>
<div class="relative" @click="showFullscreenCover = true">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg @imageLoaded="coverImageLoaded" />
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw @imageLoaded="coverImageLoaded" />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
</div>
</div>
@@ -33,28 +33,15 @@
<!-- action buttons -->
<div class="col-span-full">
<div v-if="isLocal" class="flex mt-4 -mx-1">
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play' }}</span>
<div v-if="showPlay || showRead" class="flex mt-4 -mx-1">
<ui-btn v-if="showPlay" color="success" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<span class="material-icons">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? 'Pause' : isPodcast ? 'Next Episode' : hasLocal ? 'Play' : 'Stream' }}</span>
</ui-btn>
<ui-btn v-if="showRead" color="info" class="flex items-center justify-center mx-1" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn>
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="moreButtonPress">
<span class="material-icons">more_vert</span>
</ui-btn>
</div>
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4 -mx-1">
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mx-1" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isPodcast ? 'Next Episode' : hasLocal ? 'Play' : 'Stream' }}</span>
</ui-btn>
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mx-1" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn>
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mx-1" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
</ui-btn>
@@ -65,8 +52,8 @@
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 text-center" :class="resettingProgress ? 'opacity-25' : ''">
<p>Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
<p v-if="!useEBookProgress && !userIsFinished" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else-if="userIsFinished" class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
</div>
</div>
@@ -86,6 +73,9 @@
</template>
</div>
<div v-if="podcastType" class="text-white text-opacity-60 uppercase text-sm">Type</div>
<div v-if="podcastType" class="text-sm capitalize">{{ podcastType }}</div>
<div v-if="series && series.length" class="text-white text-opacity-60 uppercase text-sm">Series</div>
<div v-if="series && series.length" class="truncate text-sm">
<template v-for="(series, index) in seriesList">
@@ -214,12 +204,12 @@ export default {
return this.libraryItem.localLibraryItem || null
},
localLibraryItemId() {
return this.localLibraryItem ? this.localLibraryItem.id : null
return this.localLibraryItem?.id || null
},
localLibraryItemEpisodes() {
if (!this.isPodcast || !this.localLibraryItem) return []
var podcastMedia = this.localLibraryItem.media
return podcastMedia ? podcastMedia.episodes || [] : []
return podcastMedia?.episodes || []
},
serverLibraryItemId() {
if (!this.isLocal) return this.libraryItem.id
@@ -260,6 +250,9 @@ export default {
publishedYear() {
return this.mediaMetadata.publishedYear
},
podcastType() {
return this.mediaMetadata.type
},
podcastAuthor() {
if (!this.isPodcast) return null
return this.mediaMetadata.author || ''
@@ -300,8 +293,16 @@ export default {
},
userItemProgress() {
if (this.isPodcast) return null
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
if (this.isLocal) return this.localItemProgress
return this.serverItemProgress
},
localItemProgress() {
if (this.isPodcast) return null
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId)
},
serverItemProgress() {
if (this.isPodcast) return null
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
},
userIsFinished() {
return this.userItemProgress ? !!this.userItemProgress.isFinished : false
@@ -311,7 +312,12 @@ export default {
const duration = this.userItemProgress.duration || this.duration
return duration - this.userItemProgress.currentTime
},
useEBookProgress() {
if (!this.userItemProgress || this.userItemProgress.progress) return false
return this.userItemProgress.ebookProgress > 0
},
progressPercent() {
if (this.useEBookProgress) return Math.max(Math.min(1, this.userItemProgress.ebookProgress), 0)
return this.userItemProgress ? Math.max(Math.min(1, this.userItemProgress.progress), 0) : 0
},
userProgressFinishedAt() {
@@ -324,6 +330,9 @@ export default {
if (this.localLibraryItemId && this.$store.getters['getIsItemStreaming'](this.localLibraryItemId)) return true
return this.$store.getters['getIsItemStreaming'](this.libraryItemId)
},
playerIsPlaying() {
return this.$store.state.playerIsPlaying && (this.isStreaming || this.isPlaying)
},
tracks() {
return this.media.tracks || []
},
@@ -337,18 +346,18 @@ export default {
isMissing() {
return this.libraryItem.isMissing
},
isIncomplete() {
return this.libraryItem.isIncomplete
isInvalid() {
return this.libraryItem.isInvalid
},
showPlay() {
return !this.isMissing && !this.isIncomplete && (this.numTracks || this.episodes.length)
return !this.isMissing && !this.isInvalid && (this.numTracks || this.episodes.length)
},
showRead() {
return this.ebookFile
},
showDownload() {
if (this.isPodcast) return false
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
if (this.isPodcast || this.hasLocal) return false
return this.user && this.userCanDownload && (this.showPlay || (this.showRead && !this.isIos))
},
ebookFile() {
return this.media.ebookFile
@@ -472,17 +481,25 @@ export default {
this.showMoreMenu = true
},
readBook() {
this.$store.commit('openReader', this.libraryItem)
if (this.localLibraryItem?.media?.ebookFile) {
// Has local ebook file
this.$store.commit('openReader', this.localLibraryItem)
} else {
this.$store.commit('openReader', this.libraryItem)
}
},
playAtTimestamp(seconds) {
this.play(seconds)
},
playClick() {
this.play()
async playClick() {
await this.$hapticsImpact()
if (this.playerIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.play()
}
},
async play(startTime = null) {
await this.$hapticsImpact()
if (this.isPodcast) {
this.episodes.sort((a, b) => {
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
@@ -550,18 +567,19 @@ export default {
})
if (value) {
this.resettingProgress = true
if (this.isLocal) {
// TODO: If connected to server also sync with server
await this.$db.removeLocalMediaProgress(this.libraryItemId)
this.$store.commit('globals/removeLocalMediaProgressForItem', this.libraryItemId)
} else {
var progressId = this.userItemProgress.id
const serverMediaProgressId = this.serverItemProgress?.id
if (this.localLibraryItemId) {
await this.$db.removeLocalMediaProgress(this.localLibraryItemId)
this.$store.commit('globals/removeLocalMediaProgressForItem', this.localLibraryItemId)
}
if (this.serverLibraryItemId && serverMediaProgressId) {
await this.$axios
.$delete(`/api/me/progress/${this.libraryItemId}`)
.$delete(`/api/me/progress/${serverMediaProgressId}`)
.then(() => {
console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.$store.commit('user/removeMediaProgress', progressId)
this.$store.commit('user/removeMediaProgress', serverMediaProgressId)
})
.catch((error) => {
console.error('Progress reset failed', error)
@@ -594,9 +612,6 @@ export default {
if (this.downloadItem) {
return
}
if (!this.numTracks) {
return
}
await this.$hapticsImpact()
if (this.isIos) {
// no local folders on iOS
@@ -634,7 +649,14 @@ export default {
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" with ${this.numTracks} audio track${this.numTracks == 1 ? '' : 's'} to folder ${localFolder.name}?`
let startDownloadMessage = `Start download for "${this.title}" with ${this.numTracks} audio track${this.numTracks == 1 ? '' : 's'} to folder ${localFolder.name}?`
if (!this.isIos && this.showRead) {
if (this.numTracks > 0) {
startDownloadMessage = `Start download for "${this.title}" with ${this.numTracks} audio track${this.numTracks == 1 ? '' : 's'} and ebook file to folder ${localFolder.name}?`
} else {
startDownloadMessage = `Start download for "${this.title}" with ebook file to folder ${localFolder.name}?`
}
}
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
@@ -691,11 +713,7 @@ export default {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
var lmp = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
console.log('toggleFinished Check LMP', this.libraryItemId, JSON.stringify(lmp))
var serverUpdated = payload.server
if (serverUpdated) {
if (payload.server) {
this.$toast.success(`Local & Server Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
} else {
this.$toast.success(`Local Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
+19 -7
View File
@@ -41,7 +41,16 @@
<template v-for="authorResult in authorResults">
<div :key="authorResult.id" class="w-full h-14 py-1">
<nuxt-link :to="`/bookshelf/library?filter=authors.${$encode(authorResult.id)}`">
<cards-author-search-card :key="authorResult.id" :author="authorResult" />
<cards-author-search-card :author="authorResult" />
</nuxt-link>
</div>
</template>
<p v-if="narratorResults.length" class="font-semibold text-sm mb-1 mt-2">Narrators</p>
<template v-for="narrator in narratorResults">
<div :key="narrator.name" class="w-full h-14 py-1">
<nuxt-link :to="`/bookshelf/library?filter=narrators.${$encode(narrator.name)}`">
<cards-narrator-search-card :narrator="narrator.name" />
</nuxt-link>
</div>
</template>
@@ -60,7 +69,8 @@ export default {
bookResults: [],
podcastResults: [],
seriesResults: [],
authorResults: []
authorResults: [],
narratorResults: []
}
},
computed: {
@@ -71,7 +81,7 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length + this.narratorResults.length
}
},
methods: {
@@ -86,6 +96,7 @@ export default {
this.podcastResults = []
this.seriesResults = []
this.authorResults = []
this.narratorResults = []
return
}
this.isFetching = true
@@ -101,10 +112,11 @@ export default {
this.isFetching = false
this.bookResults = results ? results.book || [] : []
this.podcastResults = results ? results.podcast || [] : []
this.seriesResults = results ? results.series || [] : []
this.authorResults = results ? results.authors || [] : []
this.bookResults = results?.book || []
this.podcastResults = results?.podcast || []
this.seriesResults = results?.series || []
this.authorResults = results?.authors || []
this.narratorResults = results?.narrators || []
},
updateSearch(val) {
clearTimeout(this.searchTimeout)
+40 -1
View File
@@ -95,9 +95,23 @@
<ui-text-input :value="sleepTimerLengthOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div>
</div>
<div v-if="settings.autoSleepTimer" class="flex items-center py-3" @click="toggleAutoSleepTimerAutoRewind">
<div class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.autoSleepTimerAutoRewind" @input="saveSettings" />
</div>
<p class="pl-4">Auto Sleep Timer Auto Rewind</p>
<span class="material-icons-outlined ml-2" @click.stop="showInfo('autoSleepTimerAutoRewind')">info</span>
</div>
<div v-if="settings.autoSleepTimerAutoRewind" class="py-3 flex items-center">
<p class="pr-4 w-36">Auto Rewind Time</p>
<div @click.stop="showAutoSleepTimerRewindOptions">
<ui-text-input :value="autoSleepTimerRewindLengthOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div>
</div>
<modals-dialog v-model="showMoreMenuDialog" :items="moreMenuItems" @action="clickMenuAction" />
<modals-sleep-timer-length-modal v-model="showSleepTimerLengthModal" @change="sleepTimerLengthModalSelection" />
<modals-auto-sleep-timer-rewind-length-modal v-model="showAutoSleepTimerRewindLengthModal" @change="showAutoSleepTimerRewindLengthModalSelection" />
</div>
</template>
@@ -110,6 +124,7 @@ export default {
deviceData: null,
showMoreMenuDialog: false,
showSleepTimerLengthModal: false,
showAutoSleepTimerRewindLengthModal: false,
moreMenuSetting: '',
settings: {
disableAutoRewind: false,
@@ -125,7 +140,9 @@ export default {
autoSleepTimerEndTime: '06:00',
sleepTimerLength: 900000, // 15 minutes
disableSleepTimerFadeOut: false,
disableSleepTimerResetFeedback: false
disableSleepTimerResetFeedback: false,
autoSleepTimerAutoRewind: false,
autoSleepTimerAutoRewindTime: 300000 // 5 minutes
},
lockCurrentOrientation: false,
settingInfo: {
@@ -144,6 +161,10 @@ export default {
disableSleepTimerResetFeedback: {
name: 'Disable vibrate on reset',
message: 'When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.'
},
autoSleepTimerAutoRewind: {
name: 'Enable sleep timer auto rewind',
message: 'When the auto sleep timer finishes, playing the item again will automatically rewind your position.'
}
},
hapticFeedbackItems: [
@@ -234,6 +255,10 @@ export default {
const minutes = Number(this.settings.sleepTimerLength) / 1000 / 60
return `${minutes} min`
},
autoSleepTimerRewindLengthOption() {
const minutes = Number(this.settings.autoSleepTimerAutoRewindTime) / 1000 / 60
return `${minutes} min`
},
moreMenuItems() {
if (this.moreMenuSetting === 'shakeSensitivity') return this.shakeSensitivityItems
else if (this.moreMenuSetting === 'hapticFeedback') return this.hapticFeedbackItems
@@ -245,9 +270,16 @@ export default {
this.settings.sleepTimerLength = value
this.saveSettings()
},
showAutoSleepTimerRewindLengthModalSelection(value) {
this.settings.autoSleepTimerAutoRewindTime = value
this.saveSettings()
},
showSleepTimerOptions() {
this.showSleepTimerLengthModal = true
},
showAutoSleepTimerRewindOptions() {
this.showAutoSleepTimerRewindLengthModal = true
},
showHapticFeedbackOptions() {
this.moreMenuSetting = 'hapticFeedback'
this.showMoreMenuDialog = true
@@ -287,6 +319,10 @@ export default {
this.settings.autoSleepTimer = !this.settings.autoSleepTimer
this.saveSettings()
},
toggleAutoSleepTimerAutoRewind() {
this.settings.autoSleepTimerAutoRewind = !this.settings.autoSleepTimerAutoRewind
this.saveSettings()
},
toggleDisableSleepTimerFadeOut() {
this.settings.disableSleepTimerFadeOut = !this.settings.disableSleepTimerFadeOut
this.saveSettings()
@@ -364,6 +400,9 @@ 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.autoSleepTimerAutoRewind = !!deviceSettings.autoSleepTimerAutoRewind
this.settings.autoSleepTimerAutoRewindTime = !isNaN(deviceSettings.autoSleepTimerAutoRewindTime) ? deviceSettings.autoSleepTimerAutoRewindTime : 300000 // 5 minutes
}
},
mounted() {
+16 -3
View File
@@ -1,4 +1,5 @@
import { registerPlugin, WebPlugin } from '@capacitor/core';
import { registerPlugin, WebPlugin } from '@capacitor/core'
import { nanoid } from 'nanoid'
const { PlayerState } = require('../constants')
var $axios = null
@@ -42,6 +43,15 @@ class AbsAudioPlayerWeb extends WebPlugin {
return this.player && !this.player.paused
}
getDeviceId() {
let deviceId = localStorage.getItem('absDeviceId')
if (!deviceId) {
deviceId = nanoid()
localStorage.setItem('absDeviceId', deviceId)
}
return deviceId
}
// PluginMethod
async prepareLibraryItem({ libraryItemId, episodeId, playWhenReady, startTime }) {
console.log('[AbsAudioPlayer] Prepare library item', libraryItemId)
@@ -49,8 +59,11 @@ class AbsAudioPlayerWeb extends WebPlugin {
if (libraryItemId.startsWith('local_')) {
// Fetch Local - local not implemented on web
} else {
var route = !episodeId ? `/api/items/${libraryItemId}/play` : `/api/items/${libraryItemId}/play/${episodeId}`
var playbackSession = await $axios.$post(route, { mediaPlayer: 'html5-mobile', forceDirectPlay: true })
const route = !episodeId ? `/api/items/${libraryItemId}/play` : `/api/items/${libraryItemId}/play/${episodeId}`
const deviceInfo = {
deviceId: this.getDeviceId()
}
const playbackSession = await $axios.$post(route, { deviceInfo, mediaPlayer: 'html5-mobile', forceDirectPlay: true })
if (playbackSession) {
if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime
this.setAudioPlayer(playbackSession, playWhenReady)
+6 -1
View File
@@ -86,11 +86,16 @@ class DbService {
return AbsDatabase.updateLocalTrackOrder(payload)
}
// input: { localMediaProgressId:String, isFinished:Boolean }
// input: { localLibraryItemId:String, localEpisodeId:String, isFinished:Boolean }
updateLocalMediaProgressFinished(payload) {
return AbsDatabase.updateLocalMediaProgressFinished(payload)
}
// input: { localLibraryItemId:String, ebookLocation:String, ebookProgress:Double }
updateLocalEbookProgress(payload) {
return AbsDatabase.updateLocalEbookProgress(payload)
}
updateDeviceSettings(payload) {
return AbsDatabase.updateDeviceSettings(payload)
}
+1 -1
View File
@@ -22,7 +22,7 @@ Join us on [discord](https://discord.gg/pJsjuNCKRq)
**Requires an Audiobookshelf server to connect with**
<img alt="Screenshot1" src="https://github.com/advplyr/audiobookshelf-app/raw/master/screenshots/BookshelfViews.png" />
<img alt="Screenshot" src="https://github.com/advplyr/audiobookshelf-app/raw/master/screenshots/DeviceDemoScreens.png" />
## Contributing
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

File diff suppressed because one or more lines are too long