mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-29 14:28:34 +02:00
Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app into advplyr-master
This commit is contained in:
commit
ac71d39265
12 changed files with 362 additions and 109 deletions
|
@ -16,6 +16,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.getcapacitor.*
|
import com.getcapacitor.*
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
import com.google.android.gms.cast.CastDevice
|
import com.google.android.gms.cast.CastDevice
|
||||||
|
import com.google.android.gms.common.ConnectionResult
|
||||||
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsAudioPlayer")
|
@CapacitorPlugin(name = "AbsAudioPlayer")
|
||||||
|
@ -25,7 +27,7 @@ class AbsAudioPlayer : Plugin() {
|
||||||
|
|
||||||
private lateinit var mainActivity: MainActivity
|
private lateinit var mainActivity: MainActivity
|
||||||
private lateinit var apiHandler:ApiHandler
|
private lateinit var apiHandler:ApiHandler
|
||||||
lateinit var castManager:CastManager
|
var castManager:CastManager? = null
|
||||||
|
|
||||||
lateinit var playerNotificationService: PlayerNotificationService
|
lateinit var playerNotificationService: PlayerNotificationService
|
||||||
|
|
||||||
|
@ -95,6 +97,24 @@ class AbsAudioPlayer : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initCastManager() {
|
private fun initCastManager() {
|
||||||
|
val googleApi = GoogleApiAvailability.getInstance()
|
||||||
|
val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity)
|
||||||
|
|
||||||
|
if (statusCode != ConnectionResult.SUCCESS) {
|
||||||
|
if (statusCode == ConnectionResult.SERVICE_MISSING) {
|
||||||
|
Log.w(tag, "initCastManager: Google Api Missing")
|
||||||
|
} else if (statusCode == ConnectionResult.SERVICE_DISABLED) {
|
||||||
|
Log.w(tag, "initCastManager: Google Api Disabled")
|
||||||
|
} else if (statusCode == ConnectionResult.SERVICE_INVALID) {
|
||||||
|
Log.w(tag, "initCastManager: Google Api Invalid")
|
||||||
|
} else if (statusCode == ConnectionResult.SERVICE_UPDATING) {
|
||||||
|
Log.w(tag, "initCastManager: Google Api Updating")
|
||||||
|
} else if (statusCode == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED) {
|
||||||
|
Log.w(tag, "initCastManager: Google Api Update Required")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val connListener = object: CastManager.ChromecastListener() {
|
val connListener = object: CastManager.ChromecastListener() {
|
||||||
override fun onReceiverAvailableUpdate(available: Boolean) {
|
override fun onReceiverAvailableUpdate(available: Boolean) {
|
||||||
Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available")
|
Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available")
|
||||||
|
@ -128,7 +148,7 @@ class AbsAudioPlayer : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
castManager = CastManager(mainActivity)
|
castManager = CastManager(mainActivity)
|
||||||
castManager.startRouteScan(connListener)
|
castManager?.startRouteScan(connListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
|
@ -144,7 +164,7 @@ class AbsAudioPlayer : Plugin() {
|
||||||
val libraryItemId = call.getString("libraryItemId", "").toString()
|
val libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
val episodeId = call.getString("episodeId", "").toString()
|
val episodeId = call.getString("episodeId", "").toString()
|
||||||
val playWhenReady = call.getBoolean("playWhenReady") == true
|
val playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
var playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
||||||
|
|
||||||
if (libraryItemId.isEmpty()) {
|
if (libraryItemId.isEmpty()) {
|
||||||
Log.e(tag, "Invalid call to play library item no library item id")
|
Log.e(tag, "Invalid call to play library item no library item id")
|
||||||
|
@ -322,7 +342,11 @@ class AbsAudioPlayer : Plugin() {
|
||||||
// Need to make sure the player service has been started
|
// Need to make sure the player service has been started
|
||||||
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
Log.d(tag, "CAST REQUEST SESSION PLUGIN")
|
||||||
call.resolve()
|
call.resolve()
|
||||||
castManager.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() {
|
if (castManager == null) {
|
||||||
|
Log.e(tag, "Cast Manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
castManager?.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() {
|
||||||
override fun onError(errorCode: Int) {
|
override fun onError(errorCode: Int) {
|
||||||
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
|
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,17 +206,68 @@ class AbsDatabase : Plugin() {
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun updateLocalMediaProgressFinished(call:PluginCall) {
|
fun updateLocalMediaProgressFinished(call:PluginCall) {
|
||||||
var localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
||||||
var isFinished = call.getBoolean("isFinished", false) == true
|
var localEpisodeId:String? = call.getString("localEpisodeId", "").toString()
|
||||||
|
if (localEpisodeId.isNullOrEmpty()) localEpisodeId = null
|
||||||
|
|
||||||
|
val localMediaProgressId = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
|
val isFinished = call.getBoolean("isFinished", false) == true
|
||||||
|
|
||||||
Log.d(tag, "updateLocalMediaProgressFinished $localMediaProgressId | Is Finished:$isFinished")
|
Log.d(tag, "updateLocalMediaProgressFinished $localMediaProgressId | Is Finished:$isFinished")
|
||||||
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
|
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
|
||||||
if (localMediaProgress == null) {
|
|
||||||
Log.e(tag, "updateLocalMediaProgressFinished Local Media Progress not found $localMediaProgressId")
|
if (localMediaProgress == null) { // Create new local media progress if does not exist
|
||||||
call.resolve(JSObject("{\"error\":\"Progress not found\"}"))
|
Log.d(tag, "updateLocalMediaProgressFinished Local Media Progress not found $localMediaProgressId - Creating new")
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
|
||||||
|
if (localLibraryItem == null) {
|
||||||
|
return call.resolve(JSObject("{\"error\":\"Library Item not found\"}"))
|
||||||
|
}
|
||||||
|
if (localLibraryItem.mediaType != "podcast" && !localEpisodeId.isNullOrEmpty()) {
|
||||||
|
return call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var duration = 0.0
|
||||||
|
var podcastEpisode:PodcastEpisode? = null
|
||||||
|
if (!localEpisodeId.isNullOrEmpty()) {
|
||||||
|
val podcast = localLibraryItem.media as Podcast
|
||||||
|
podcastEpisode = podcast.episodes?.find { episode ->
|
||||||
|
episode.id == localEpisodeId
|
||||||
|
}
|
||||||
|
if (podcastEpisode == null) {
|
||||||
|
return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}"))
|
||||||
|
}
|
||||||
|
duration = podcastEpisode.duration ?: 0.0
|
||||||
|
} else {
|
||||||
|
val book = localLibraryItem.media as Book
|
||||||
|
duration = book.duration ?: 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
localMediaProgress = LocalMediaProgress(
|
||||||
|
id = localMediaProgressId,
|
||||||
|
localLibraryItemId = localLibraryItemId,
|
||||||
|
localEpisodeId = localEpisodeId,
|
||||||
|
duration = duration,
|
||||||
|
progress = if (isFinished) 1.0 else 0.0,
|
||||||
|
currentTime = 0.0,
|
||||||
|
isFinished = isFinished,
|
||||||
|
lastUpdate = currentTime,
|
||||||
|
startedAt = if (isFinished) currentTime else 0L,
|
||||||
|
finishedAt = if (isFinished) currentTime else null,
|
||||||
|
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
|
||||||
|
serverAddress = localLibraryItem.serverAddress,
|
||||||
|
serverUserId = localLibraryItem.serverUserId,
|
||||||
|
libraryItemId = localLibraryItem.libraryItemId,
|
||||||
|
episodeId = podcastEpisode?.serverEpisodeId)
|
||||||
} else {
|
} else {
|
||||||
localMediaProgress.updateIsFinished(isFinished)
|
localMediaProgress.updateIsFinished(isFinished)
|
||||||
|
}
|
||||||
|
|
||||||
var lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
|
// Save local media progress locally
|
||||||
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
|
||||||
|
val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
|
||||||
Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring")
|
Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring")
|
||||||
|
|
||||||
// Send update to server media progress is linked to a server and user is logged into that server
|
// Send update to server media progress is linked to a server and user is logged into that server
|
||||||
|
@ -233,12 +284,10 @@ class AbsDatabase : Plugin() {
|
||||||
jsobj.put("server", true)
|
jsobj.put("server", true)
|
||||||
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
// call.resolve(JSObject("{\"local\":true,\"server\":true,\"localMediaProgress\":$lmpstring}"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
|
if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
|
||||||
// call.resolve(JSObject("{\"local\":true,\"localMediaProgress\":$lmpstring}}"))
|
|
||||||
var jsobj = JSObject()
|
var jsobj = JSObject()
|
||||||
jsobj.put("local", true)
|
jsobj.put("local", true)
|
||||||
jsobj.put("server", false)
|
jsobj.put("server", false)
|
||||||
|
@ -246,7 +295,6 @@ class AbsDatabase : Plugin() {
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun updateLocalTrackOrder(call:PluginCall) {
|
fun updateLocalTrackOrder(call:PluginCall) {
|
||||||
|
|
|
@ -180,15 +180,30 @@ class AbsDownloader : Plugin() {
|
||||||
|
|
||||||
// Item filenames could be the same if they are in sub-folders, this will make them unique
|
// Item filenames could be the same if they are in sub-folders, this will make them unique
|
||||||
private fun getFilenameFromRelPath(relPath: String): String {
|
private fun getFilenameFromRelPath(relPath: String): String {
|
||||||
val cleanedRelPath = relPath.replace("\\", "_").replace("/", "_")
|
var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_")
|
||||||
|
cleanedRelPath = cleanStringForFileSystem(cleanedRelPath)
|
||||||
return if (cleanedRelPath.startsWith("_")) cleanedRelPath.substring(1) else cleanedRelPath
|
return if (cleanedRelPath.startsWith("_")) cleanedRelPath.substring(1) else cleanedRelPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace characters that cant be used in the file system
|
||||||
|
// Reserved characters: ?:\"*|/\\<>
|
||||||
|
private fun cleanStringForFileSystem(str:String):String {
|
||||||
|
val reservedCharacters = listOf("?", "\"", "*", "|", "/", "\\", "<", ">")
|
||||||
|
var newTitle = str
|
||||||
|
newTitle = newTitle.replace(":", " -") // Special case replace : with -
|
||||||
|
|
||||||
|
reservedCharacters.forEach {
|
||||||
|
newTitle = newTitle.replace(it, "")
|
||||||
|
}
|
||||||
|
return newTitle
|
||||||
|
}
|
||||||
|
|
||||||
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
|
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
|
||||||
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
|
||||||
if (libraryItem.mediaType == "book") {
|
if (libraryItem.mediaType == "book") {
|
||||||
val bookTitle = libraryItem.media.metadata.title
|
val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
||||||
|
|
||||||
val tracks = libraryItem.media.getAudioTracks()
|
val tracks = libraryItem.media.getAudioTracks()
|
||||||
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
||||||
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
|
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
|
||||||
|
@ -243,8 +258,8 @@ class AbsDownloader : Plugin() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Podcast episode download
|
// Podcast episode download
|
||||||
|
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
||||||
|
|
||||||
val podcastTitle = libraryItem.media.metadata.title
|
|
||||||
val audioTrack = episode?.audioTrack
|
val audioTrack = episode?.audioTrack
|
||||||
Log.d(tag, "Starting podcast episode download")
|
Log.d(tag, "Starting podcast episode download")
|
||||||
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
|
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<template v-for="shelf in totalShelves">
|
<template v-for="shelf in totalShelves">
|
||||||
<div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
|
<div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
|
||||||
<div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
|
<div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
|
||||||
|
<div v-else class="flex border-t border-white border-opacity-10 my-3 py-1"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
|
<p v-if="duration" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
|
||||||
|
<p v-if="episodes" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
|
@ -99,9 +101,23 @@ export default {
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this._libraryItem.mediaType
|
return this._libraryItem.mediaType
|
||||||
},
|
},
|
||||||
|
duration() {
|
||||||
|
return this.media.duration || null
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
episodes() {
|
||||||
|
if (this.isPodcast) {
|
||||||
|
if (this.media.numEpisodes==1) {
|
||||||
|
return "1 episode"
|
||||||
|
} else {
|
||||||
|
return this.media.numEpisodes + ' episodes'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,11 +27,13 @@
|
||||||
|
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
|
|
||||||
|
<div v-if="!isIos">
|
||||||
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
||||||
<span v-else-if="!localEpisode" class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
<span v-else-if="!localEpisode" class="material-icons mx-1 mt-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
||||||
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
|
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!userIsFinished" class="absolute bottom-0 left-0 h-0.5 bg-warning" :style="{ width: itemProgressPercent * 100 + '%' }" />
|
<div v-if="!userIsFinished" class="absolute bottom-0 left-0 h-0.5 bg-warning" :style="{ width: itemProgressPercent * 100 + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,6 +63,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return 'podcast'
|
return 'podcast'
|
||||||
},
|
},
|
||||||
|
@ -204,9 +209,7 @@ export default {
|
||||||
var isFinished = !this.userIsFinished
|
var isFinished = !this.userIsFinished
|
||||||
var localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId
|
var localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId
|
||||||
var localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id
|
var localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id
|
||||||
var localMediaProgressId = `${localLibraryItemId}-${localEpisodeId}`
|
var payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId, localEpisodeId, isFinished })
|
||||||
console.log('toggleFinished local media progress id', localMediaProgressId, isFinished)
|
|
||||||
var payload = await this.$db.updateLocalMediaProgressFinished({ localMediaProgressId, isFinished })
|
|
||||||
console.log('toggleFinished payload', JSON.stringify(payload))
|
console.log('toggleFinished payload', JSON.stringify(payload))
|
||||||
if (!payload || payload.error) {
|
if (!payload || payload.error) {
|
||||||
var errorMsg = payload ? payload.error : 'Unknown error'
|
var errorMsg = payload ? payload.error : 'Unknown error'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
<button class="icon-btn rounded-md flex items-center justify-center px-2 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
||||||
<div class="w-5 h-5 text-white relative">
|
<div class="w-5 h-5 text-white relative">
|
||||||
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
|
|
|
@ -24,29 +24,25 @@ class AudioPlayer: NSObject {
|
||||||
private var playWhenReady: Bool
|
private var playWhenReady: Bool
|
||||||
private var initialPlaybackRate: Float
|
private var initialPlaybackRate: Float
|
||||||
|
|
||||||
private var audioPlayer: AVPlayer
|
private var audioPlayer: AVQueuePlayer
|
||||||
private var playbackSession: PlaybackSession
|
private var playbackSession: PlaybackSession
|
||||||
private var activeAudioTrack: AudioTrack
|
|
||||||
|
private var queueObserver:NSKeyValueObservation?
|
||||||
|
private var queueItemStatusObserver:NSKeyValueObservation?
|
||||||
|
|
||||||
|
private var currentTrackIndex = 0
|
||||||
|
private var allPlayerItems:[AVPlayerItem] = []
|
||||||
|
|
||||||
// MARK: - Constructor
|
// MARK: - Constructor
|
||||||
init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) {
|
init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) {
|
||||||
self.playWhenReady = playWhenReady
|
self.playWhenReady = playWhenReady
|
||||||
self.initialPlaybackRate = playbackRate
|
self.initialPlaybackRate = playbackRate
|
||||||
self.audioPlayer = AVPlayer()
|
self.audioPlayer = AVQueuePlayer()
|
||||||
self.playbackSession = playbackSession
|
self.playbackSession = playbackSession
|
||||||
self.status = -1
|
self.status = -1
|
||||||
self.rate = 0.0
|
self.rate = 0.0
|
||||||
self.tmpRate = playbackRate
|
self.tmpRate = playbackRate
|
||||||
|
|
||||||
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
|
|
||||||
NSLog("The player only support HLS streams right now")
|
|
||||||
self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: nil, mimeType: "", metadata: nil, serverIndex: 0)
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.activeAudioTrack = playbackSession.audioTracks[0]
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
initAudioSession()
|
initAudioSession()
|
||||||
|
@ -56,15 +52,29 @@ class AudioPlayer: NSObject {
|
||||||
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
|
||||||
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
|
||||||
|
|
||||||
let playerItem = AVPlayerItem(asset: createAsset())
|
for track in playbackSession.audioTracks {
|
||||||
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: &playerItemContext)
|
let playerItem = AVPlayerItem(asset: createAsset(itemId: playbackSession.libraryItemId!, track: track))
|
||||||
|
self.allPlayerItems.append(playerItem)
|
||||||
|
}
|
||||||
|
|
||||||
self.audioPlayer.replaceCurrentItem(with: playerItem)
|
self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
|
||||||
seek(playbackSession.currentTime)
|
NSLog("TEST: Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
|
||||||
|
|
||||||
|
let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count]
|
||||||
|
NSLog("TEST: Setting player items \(playerItems.count)")
|
||||||
|
|
||||||
|
for item in Array(playerItems) {
|
||||||
|
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupQueueObserver()
|
||||||
|
setupQueueItemStatusObserver()
|
||||||
|
|
||||||
NSLog("Audioplayer ready")
|
NSLog("Audioplayer ready")
|
||||||
}
|
}
|
||||||
deinit {
|
deinit {
|
||||||
|
self.queueObserver?.invalidate()
|
||||||
|
self.queueItemStatusObserver?.invalidate()
|
||||||
destroy()
|
destroy()
|
||||||
}
|
}
|
||||||
public func destroy() {
|
public func destroy() {
|
||||||
|
@ -87,6 +97,50 @@ class AudioPlayer: NSObject {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getItemIndexForTime(time:Double) -> Int {
|
||||||
|
for index in 0..<self.allPlayerItems.count {
|
||||||
|
let startOffset = playbackSession.audioTracks[index].startOffset ?? 0.0
|
||||||
|
let duration = playbackSession.audioTracks[index].duration
|
||||||
|
let trackEnd = startOffset + duration
|
||||||
|
if (time < trackEnd.rounded(.down)) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupQueueObserver() {
|
||||||
|
self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in
|
||||||
|
let prevTrackIndex = self.currentTrackIndex
|
||||||
|
self.audioPlayer.currentItem.map { item in
|
||||||
|
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
|
||||||
|
if (self.currentTrackIndex != prevTrackIndex) {
|
||||||
|
NSLog("TEST: New Current track index \(self.currentTrackIndex)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupQueueItemStatusObserver() {
|
||||||
|
self.queueItemStatusObserver?.invalidate()
|
||||||
|
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
|
||||||
|
if (playerItem.status == .readyToPlay) {
|
||||||
|
NSLog("TEST: queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
||||||
|
self.updateNowPlaying()
|
||||||
|
|
||||||
|
let firstReady = self.status < 0
|
||||||
|
self.status = 0
|
||||||
|
if self.playWhenReady {
|
||||||
|
self.seek(self.playbackSession.currentTime, from: "queueItemStatusObserver")
|
||||||
|
self.playWhenReady = false
|
||||||
|
self.play()
|
||||||
|
} else if (firstReady) { // Only seek on first readyToPlay
|
||||||
|
self.seek(self.playbackSession.currentTime, from: "queueItemStatusObserver")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
public func play(allowSeekBack: Bool = false) {
|
public func play(allowSeekBack: Bool = false) {
|
||||||
if allowSeekBack {
|
if allowSeekBack {
|
||||||
|
@ -110,7 +164,7 @@ class AudioPlayer: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if time != nil {
|
if time != nil {
|
||||||
seek(getCurrentTime() - Double(time!))
|
seek(getCurrentTime() - Double(time!), from: "play")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
||||||
|
@ -122,6 +176,7 @@ class AudioPlayer: NSObject {
|
||||||
|
|
||||||
updateNowPlaying()
|
updateNowPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pause() {
|
public func pause() {
|
||||||
self.audioPlayer.pause()
|
self.audioPlayer.pause()
|
||||||
self.status = 0
|
self.status = 0
|
||||||
|
@ -130,24 +185,60 @@ class AudioPlayer: NSObject {
|
||||||
updateNowPlaying()
|
updateNowPlaying()
|
||||||
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
||||||
}
|
}
|
||||||
public func seek(_ to: Double) {
|
|
||||||
let continuePlaing = rate > 0.0
|
public func seek(_ to: Double, from:String) {
|
||||||
|
let continuePlaying = rate > 0.0
|
||||||
|
|
||||||
pause()
|
pause()
|
||||||
self.audioPlayer.seek(to: CMTime(seconds: to, preferredTimescale: 1000)) { completed in
|
|
||||||
if !completed {
|
NSLog("TEST: Seek to \(to) from \(from)")
|
||||||
NSLog("WARNING: seeking not completed (to \(to)")
|
|
||||||
|
let currentTrack = self.playbackSession.audioTracks[self.currentTrackIndex]
|
||||||
|
let ctso = currentTrack.startOffset ?? 0.0
|
||||||
|
let trackEnd = ctso + currentTrack.duration
|
||||||
|
NSLog("TEST: Seek current track END = \(trackEnd)")
|
||||||
|
|
||||||
|
|
||||||
|
let indexOfSeek = getItemIndexForTime(time: to)
|
||||||
|
NSLog("TEST: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||||
|
|
||||||
|
// Reconstruct queue if seeking to a different track
|
||||||
|
if (self.currentTrackIndex != indexOfSeek) {
|
||||||
|
self.currentTrackIndex = indexOfSeek
|
||||||
|
|
||||||
|
self.playbackSession.currentTime = to
|
||||||
|
|
||||||
|
self.playWhenReady = continuePlaying // Only playWhenReady if already playing
|
||||||
|
self.status = -1
|
||||||
|
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
|
||||||
|
|
||||||
|
self.audioPlayer.removeAllItems()
|
||||||
|
for item in Array(playerItems) {
|
||||||
|
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||||
}
|
}
|
||||||
|
|
||||||
if continuePlaing {
|
setupQueueItemStatusObserver()
|
||||||
|
} else {
|
||||||
|
NSLog("TEST: Seeking in current item \(to)")
|
||||||
|
let currentTrackStartOffset = self.playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
|
||||||
|
let seekTime = to - currentTrackStartOffset
|
||||||
|
|
||||||
|
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { completed in
|
||||||
|
if !completed {
|
||||||
|
NSLog("WARNING: seeking not completed (to \(seekTime)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if continuePlaying {
|
||||||
self.play()
|
self.play()
|
||||||
}
|
}
|
||||||
self.updateNowPlaying()
|
self.updateNowPlaying()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
|
public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
|
||||||
if self.audioPlayer.rate != rate {
|
if self.audioPlayer.rate != rate {
|
||||||
|
NSLog("TEST: setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
|
||||||
self.audioPlayer.rate = rate
|
self.audioPlayer.rate = rate
|
||||||
}
|
}
|
||||||
if rate > 0.0 && !(observed && rate == 1) {
|
if rate > 0.0 && !(observed && rate == 1) {
|
||||||
|
@ -159,20 +250,31 @@ class AudioPlayer: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getCurrentTime() -> Double {
|
public func getCurrentTime() -> Double {
|
||||||
self.audioPlayer.currentTime().seconds
|
let currentTrackTime = self.audioPlayer.currentTime().seconds
|
||||||
|
let audioTrack = playbackSession.audioTracks[currentTrackIndex]
|
||||||
|
let startOffset = audioTrack.startOffset ?? 0.0
|
||||||
|
return startOffset + currentTrackTime
|
||||||
}
|
}
|
||||||
public func getDuration() -> Double {
|
public func getDuration() -> Double {
|
||||||
self.audioPlayer.currentItem?.duration.seconds ?? 0
|
return playbackSession.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
private func createAsset() -> AVAsset {
|
private func createAsset(itemId:String, track:AudioTrack) -> AVAsset {
|
||||||
let headers: [String: String] = [
|
let filename = track.metadata?.filename ?? ""
|
||||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||||
]
|
let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||||
|
let url = URL(string: urlstr)!
|
||||||
|
return AVURLAsset(url: url)
|
||||||
|
|
||||||
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
// Method for HLS
|
||||||
|
// let headers: [String: String] = [
|
||||||
|
// "Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
// ]
|
||||||
|
//
|
||||||
|
// return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initAudioSession() {
|
private func initAudioSession() {
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
|
||||||
|
@ -208,7 +310,7 @@ class AudioPlayer: NSObject {
|
||||||
return .noSuchContent
|
return .noSuchContent
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(getCurrentTime() + command.preferredIntervals[0].doubleValue)
|
seek(getCurrentTime() + command.preferredIntervals[0].doubleValue, from: "remote")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
commandCenter.skipBackwardCommand.isEnabled = true
|
commandCenter.skipBackwardCommand.isEnabled = true
|
||||||
|
@ -218,7 +320,7 @@ class AudioPlayer: NSObject {
|
||||||
return .noSuchContent
|
return .noSuchContent
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(getCurrentTime() - command.preferredIntervals[0].doubleValue)
|
seek(getCurrentTime() - command.preferredIntervals[0].doubleValue, from: "remote")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +330,7 @@ class AudioPlayer: NSObject {
|
||||||
return .noSuchContent
|
return .noSuchContent
|
||||||
}
|
}
|
||||||
|
|
||||||
self.seek(event.positionTime)
|
self.seek(event.positionTime, from: "remote")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,23 +352,9 @@ class AudioPlayer: NSObject {
|
||||||
|
|
||||||
// MARK: - Observer
|
// MARK: - Observer
|
||||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
if context == &playerItemContext {
|
if context == &playerContext {
|
||||||
if keyPath == #keyPath(AVPlayer.status) {
|
|
||||||
guard let playerStatus = AVPlayerItem.Status(rawValue: (change?[.newKey] as? Int ?? -1)) else { return }
|
|
||||||
|
|
||||||
if playerStatus == .readyToPlay {
|
|
||||||
self.updateNowPlaying()
|
|
||||||
|
|
||||||
self.status = 0
|
|
||||||
if self.playWhenReady {
|
|
||||||
seek(playbackSession.currentTime)
|
|
||||||
self.playWhenReady = false
|
|
||||||
self.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if context == &playerContext {
|
|
||||||
if keyPath == #keyPath(AVPlayer.rate) {
|
if keyPath == #keyPath(AVPlayer.rate) {
|
||||||
|
NSLog("TEST: playerContext observer player rate")
|
||||||
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
|
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
|
||||||
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||||
|
|
|
@ -32,6 +32,7 @@ class PlayerHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static var listeningTimePassedSinceLastSync: Double = 0.0
|
private static var listeningTimePassedSinceLastSync: Double = 0.0
|
||||||
|
private static var lastSyncReport: PlaybackReport?
|
||||||
|
|
||||||
public static var paused: Bool {
|
public static var paused: Bool {
|
||||||
get {
|
get {
|
||||||
|
@ -86,7 +87,7 @@ class PlayerHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
let destinationTime = player.getCurrentTime() + amount
|
let destinationTime = player.getCurrentTime() + amount
|
||||||
player.seek(destinationTime)
|
player.seek(destinationTime, from: "handler")
|
||||||
}
|
}
|
||||||
public static func seekBackward(amount: Double) {
|
public static func seekBackward(amount: Double) {
|
||||||
guard let player = player else {
|
guard let player = player else {
|
||||||
|
@ -94,10 +95,10 @@ class PlayerHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
let destinationTime = player.getCurrentTime() - amount
|
let destinationTime = player.getCurrentTime() - amount
|
||||||
player.seek(destinationTime)
|
player.seek(destinationTime, from: "handler")
|
||||||
}
|
}
|
||||||
public static func seek(amount: Double) {
|
public static func seek(amount: Double) {
|
||||||
player?.seek(amount)
|
player?.seek(amount, from: "handler")
|
||||||
}
|
}
|
||||||
public static func getMetdata() -> [String: Any] {
|
public static func getMetdata() -> [String: Any] {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@ -132,10 +133,17 @@ class PlayerHandler {
|
||||||
if session == nil { return }
|
if session == nil { return }
|
||||||
guard let player = player else { return }
|
guard let player = player else { return }
|
||||||
|
|
||||||
let report = PlaybackReport(currentTime: player.getCurrentTime(), duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
let playerCurrentTime = player.getCurrentTime()
|
||||||
|
if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) {
|
||||||
|
// No need to syncProgress
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
session!.currentTime = player.getCurrentTime()
|
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
||||||
|
|
||||||
|
session!.currentTime = playerCurrentTime
|
||||||
listeningTimePassedSinceLastSync = 0
|
listeningTimePassedSinceLastSync = 0
|
||||||
|
lastSyncReport = report
|
||||||
|
|
||||||
// TODO: check if online
|
// TODO: check if online
|
||||||
NSLog("sending playback report")
|
NSLog("sending playback report")
|
||||||
|
|
|
@ -68,7 +68,8 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||||
"forceTranscode": "true", // TODO: direct play
|
"forceDirectPlay": "true",
|
||||||
|
"forceTranscode": "false", // TODO: direct play
|
||||||
"mediaPlayer": "AVPlayer",
|
"mediaPlayer": "AVPlayer",
|
||||||
], decodable: PlaybackSession.self) { obj in
|
], decodable: PlaybackSession.self) { obj in
|
||||||
var session = obj
|
var session = obj
|
||||||
|
|
|
@ -20,33 +20,36 @@
|
||||||
<div v-if="!isPodcast && 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' : ''">
|
<div v-if="!isPodcast && 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>
|
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</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>
|
||||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||||
<span class="material-icons text-sm">close</span>
|
<span class="material-icons text-sm">close</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLocal" class="flex mt-4 -mr-2">
|
<div v-if="isLocal" class="flex mt-4">
|
||||||
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
||||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||||
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play Local' }}</span>
|
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
||||||
<span class="material-icons">auto_stories</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4 -mr-2">
|
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4">
|
||||||
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
||||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play Local' : 'Play Stream' }}</span>
|
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
||||||
<span class="material-icons">auto_stories</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="user && showPlay && !isIos && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
<ui-btn v-if="user && showPlay && !isIos && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mr-2" :padding-x="2" @click="downloadClick">
|
||||||
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,6 +106,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
resettingProgress: false,
|
resettingProgress: false,
|
||||||
|
isProcessingReadUpdate: false,
|
||||||
showSelectLocalFolder: false
|
showSelectLocalFolder: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -352,6 +356,50 @@ export default {
|
||||||
console.log('New local library item', item.id)
|
console.log('New local library item', item.id)
|
||||||
this.$set(this.libraryItem, 'localLibraryItem', item)
|
this.$set(this.libraryItem, 'localLibraryItem', item)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async toggleFinished() {
|
||||||
|
this.isProcessingReadUpdate = true
|
||||||
|
if (this.isLocal) {
|
||||||
|
var isFinished = !this.userIsFinished
|
||||||
|
var payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId: this.localLibraryItemId, isFinished })
|
||||||
|
console.log('toggleFinished payload', JSON.stringify(payload))
|
||||||
|
if (!payload || payload.error) {
|
||||||
|
var errorMsg = payload ? payload.error : 'Unknown error'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
} else {
|
||||||
|
var localMediaProgress = payload.localMediaProgress
|
||||||
|
console.log('toggleFinished localMediaProgress', JSON.stringify(localMediaProgress))
|
||||||
|
if (localMediaProgress) {
|
||||||
|
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) {
|
||||||
|
this.$toast.success(`Local & Server Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
|
} else {
|
||||||
|
this.$toast.success(`Local Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
} else {
|
||||||
|
var updatePayload = {
|
||||||
|
isFinished: !this.userIsFinished
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -199,7 +199,8 @@ class AbsDatabaseWeb extends WebPlugin {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLocalMediaProgressFinished({ localMediaProgressId, isFinished }) {
|
async updateLocalMediaProgressFinished(payload) {
|
||||||
|
// { localLibraryItemId, localEpisodeId, isFinished }
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue