Update Android Auto chapters and bookmarks
This commit is contained in:
parent
fc946702c1
commit
1cccb8efe9
8 changed files with 1926 additions and 72 deletions
|
|
@ -69,12 +69,20 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
|
if (playerNotificationService.isAndroidAutoPlayback()) {
|
||||||
|
playerNotificationService.skipToPreviousChapter()
|
||||||
|
} else {
|
||||||
playerNotificationService.skipToPrevious()
|
playerNotificationService.skipToPrevious()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
|
if (playerNotificationService.isAndroidAutoPlayback()) {
|
||||||
|
playerNotificationService.skipToNextChapter()
|
||||||
|
} else {
|
||||||
playerNotificationService.skipToNext()
|
playerNotificationService.skipToNext()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFastForward() {
|
override fun onFastForward() {
|
||||||
playerNotificationService.jumpForward()
|
playerNotificationService.jumpForward()
|
||||||
|
|
@ -85,8 +93,38 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekTo(pos: Long) {
|
override fun onSeekTo(pos: Long) {
|
||||||
val currentTrackStartOffset = playerNotificationService.getCurrentTrackStartOffsetMs()
|
if (playerNotificationService.isAndroidAutoPlayback()) {
|
||||||
playerNotificationService.seekPlayer(currentTrackStartOffset + pos)
|
val chapter = playerNotificationService.getCurrentBookChapter()
|
||||||
|
|
||||||
|
if (chapter != null) {
|
||||||
|
val chapterDuration =
|
||||||
|
(chapter.endMs - chapter.startMs).coerceAtLeast(0L)
|
||||||
|
|
||||||
|
val chapterPosition =
|
||||||
|
pos.coerceIn(0L, chapterDuration)
|
||||||
|
|
||||||
|
val absolutePosition =
|
||||||
|
chapter.startMs + chapterPosition
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Android Auto chapter seek: " +
|
||||||
|
"pos=$pos " +
|
||||||
|
"chapter=${chapter.startMs}-${chapter.endMs} " +
|
||||||
|
"absolute=$absolutePosition"
|
||||||
|
)
|
||||||
|
|
||||||
|
playerNotificationService.seekPlayer(absolutePosition)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentTrackStartOffset =
|
||||||
|
playerNotificationService.getCurrentTrackStartOffsetMs()
|
||||||
|
|
||||||
|
playerNotificationService.seekPlayer(
|
||||||
|
currentTrackStartOffset + pos
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onChangeSpeed() {
|
private fun onChangeSpeed() {
|
||||||
|
|
@ -110,6 +148,144 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
|
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
||||||
|
|
||||||
|
if (
|
||||||
|
mediaId != null &&
|
||||||
|
mediaId.startsWith(
|
||||||
|
ANDROID_AUTO_BOOKMARK_PLAY_PREFIX
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val bookmarkTime =
|
||||||
|
mediaId
|
||||||
|
.removePrefix(
|
||||||
|
ANDROID_AUTO_BOOKMARK_PLAY_PREFIX
|
||||||
|
)
|
||||||
|
.toLongOrNull()
|
||||||
|
|
||||||
|
if (bookmarkTime == null) {
|
||||||
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"Invalid Android Auto bookmark play ID: " +
|
||||||
|
mediaId
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
playerNotificationService.selectAndroidAutoBookmark(
|
||||||
|
bookmarkTime,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
mediaId != null &&
|
||||||
|
mediaId.startsWith(
|
||||||
|
ANDROID_AUTO_BOOKMARK_DELETE_PREFIX
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val bookmarkTime =
|
||||||
|
mediaId
|
||||||
|
.removePrefix(
|
||||||
|
ANDROID_AUTO_BOOKMARK_DELETE_PREFIX
|
||||||
|
)
|
||||||
|
.toLongOrNull()
|
||||||
|
|
||||||
|
val libraryItemId =
|
||||||
|
playerNotificationService
|
||||||
|
.currentPlaybackSession
|
||||||
|
?.libraryItemId
|
||||||
|
|
||||||
|
if (
|
||||||
|
bookmarkTime == null ||
|
||||||
|
libraryItemId.isNullOrBlank()
|
||||||
|
) {
|
||||||
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"Invalid Android Auto bookmark delete ID: " +
|
||||||
|
mediaId
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Android Auto bookmark delete selected: " +
|
||||||
|
"libraryItemId=$libraryItemId, " +
|
||||||
|
"time=$bookmarkTime"
|
||||||
|
)
|
||||||
|
|
||||||
|
playerNotificationService
|
||||||
|
.clientEventEmitter
|
||||||
|
?.onBookmarkDeleteRequested(
|
||||||
|
libraryItemId,
|
||||||
|
bookmarkTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android Auto bookmark items seek within the current audiobook.
|
||||||
|
if (
|
||||||
|
mediaId != null &&
|
||||||
|
mediaId.startsWith(ANDROID_AUTO_BOOKMARK_PREFIX)
|
||||||
|
) {
|
||||||
|
val bookmarkTime =
|
||||||
|
mediaId
|
||||||
|
.removePrefix(ANDROID_AUTO_BOOKMARK_PREFIX)
|
||||||
|
.toLongOrNull()
|
||||||
|
|
||||||
|
if (bookmarkTime == null) {
|
||||||
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"Invalid Android Auto bookmark media ID: $mediaId"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
playerNotificationService.selectAndroidAutoBookmark(
|
||||||
|
bookmarkTime,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android Auto chapter browser items use a dedicated media ID.
|
||||||
|
// Selecting one must seek within the current audiobook instead of
|
||||||
|
// trying to load it as a new library item.
|
||||||
|
if (
|
||||||
|
mediaId != null &&
|
||||||
|
mediaId.startsWith(ANDROID_AUTO_CHAPTER_PREFIX)
|
||||||
|
) {
|
||||||
|
val chapterId =
|
||||||
|
mediaId
|
||||||
|
.removePrefix(ANDROID_AUTO_CHAPTER_PREFIX)
|
||||||
|
.toIntOrNull()
|
||||||
|
|
||||||
|
if (chapterId == null) {
|
||||||
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"Invalid Android Auto chapter media ID: $mediaId"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
playerNotificationService.selectAndroidAutoChapter(
|
||||||
|
chapterId,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val libraryItemWrapper: LibraryItemWrapper?
|
val libraryItemWrapper: LibraryItemWrapper?
|
||||||
var podcastEpisode: PodcastEpisode? = null
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
|
|
@ -281,6 +457,8 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
||||||
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
||||||
CUSTOM_ACTION_CHANGE_SPEED -> onChangeSpeed()
|
CUSTOM_ACTION_CHANGE_SPEED -> onChangeSpeed()
|
||||||
|
CUSTOM_ACTION_ADD_BOOKMARK ->
|
||||||
|
playerNotificationService.requestBookmarkAtCurrentPosition()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,29 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
||||||
|
|
||||||
|
if (mediaId.startsWith(ANDROID_AUTO_CHAPTER_PREFIX)) {
|
||||||
|
val chapterId =
|
||||||
|
mediaId
|
||||||
|
.removePrefix(ANDROID_AUTO_CHAPTER_PREFIX)
|
||||||
|
.toIntOrNull()
|
||||||
|
|
||||||
|
if (chapterId == null) {
|
||||||
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"Invalid Android Auto chapter media ID: $mediaId"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
playerNotificationService.selectAndroidAutoChapter(
|
||||||
|
chapterId,
|
||||||
|
playWhenReady
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val libraryItemWrapper: LibraryItemWrapper?
|
val libraryItemWrapper: LibraryItemWrapper?
|
||||||
var podcastEpisode: PodcastEpisode? = null
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_ba
|
||||||
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
||||||
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
||||||
const val CUSTOM_ACTION_CHANGE_SPEED = "com.audiobookshelf.customAction.change_speed";
|
const val CUSTOM_ACTION_CHANGE_SPEED = "com.audiobookshelf.customAction.change_speed";
|
||||||
|
const val CUSTOM_ACTION_ADD_BOOKMARK = "com.audiobookshelf.customAction.add_bookmark";
|
||||||
|
|
||||||
const val PLAYMETHOD_DIRECTPLAY = 0
|
const val PLAYMETHOD_DIRECTPLAY = 0
|
||||||
const val PLAYMETHOD_DIRECTSTREAM = 1
|
const val PLAYMETHOD_DIRECTSTREAM = 1
|
||||||
|
|
@ -14,6 +15,17 @@ const val PLAYMETHOD_LOCAL = 3
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
|
const val ANDROID_AUTO_CHAPTERS_ROOT = "android-auto-chapters"
|
||||||
|
const val ANDROID_AUTO_BOOKMARKS_ROOT = "android-auto-bookmarks"
|
||||||
|
const val ANDROID_AUTO_BOOKMARK_PREFIX = "android-auto-bookmark:"
|
||||||
|
const val ANDROID_AUTO_BOOKMARK_MENU_PREFIX = "android-auto-bookmark-menu:"
|
||||||
|
const val ANDROID_AUTO_BOOKMARK_PLAY_PREFIX = "android-auto-bookmark-play:"
|
||||||
|
const val ANDROID_AUTO_BOOKMARK_DELETE_PREFIX = "android-auto-bookmark-delete:"
|
||||||
|
|
||||||
|
|
||||||
|
const val ANDROID_AUTO_ACTION_DELETE_BOOKMARK = "android-auto-action-delete-bookmark"
|
||||||
|
const val ANDROID_AUTO_CHAPTER_PREFIX = "android-auto-chapter:"
|
||||||
|
|
||||||
const val ANDROID_AUTO_PKG_NAME = "com.google.android.projection.gearhead"
|
const val ANDROID_AUTO_PKG_NAME = "com.google.android.projection.gearhead"
|
||||||
const val ANDROID_AUTO_SIMULATOR_PKG_NAME = "com.google.android.autosimulator"
|
const val ANDROID_AUTO_SIMULATOR_PKG_NAME = "com.google.android.autosimulator"
|
||||||
const val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
|
const val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,11 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||||
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
||||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playerNotificationService.isAndroidAutoPlayback()) {
|
||||||
|
playerNotificationService.updateAndroidAutoMediaSessionState()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcPauseSeekBackTime() : Long {
|
private fun calcPauseSeekBackTime() : Long {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@ import com.audiobookshelf.app.MainActivity
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaEventManager
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
|
import com.audiobookshelf.app.player.AndroidAutoBookmark
|
||||||
import com.audiobookshelf.app.player.CastManager
|
import com.audiobookshelf.app.player.CastManager
|
||||||
import com.audiobookshelf.app.player.PlayerListener
|
import com.audiobookshelf.app.player.PlayerListener
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
|
@ -114,6 +115,31 @@ class AbsAudioPlayer : Plugin() {
|
||||||
override fun onPlaybackSpeedChanged(playbackSpeed:Float) {
|
override fun onPlaybackSpeedChanged(playbackSpeed:Float) {
|
||||||
emit("onPlaybackSpeedChanged", playbackSpeed)
|
emit("onPlaybackSpeedChanged", playbackSpeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBookmarkRequested(
|
||||||
|
libraryItemId: String,
|
||||||
|
timeSeconds: Long
|
||||||
|
) {
|
||||||
|
val ret = JSObject()
|
||||||
|
ret.put("libraryItemId", libraryItemId)
|
||||||
|
ret.put("time", timeSeconds)
|
||||||
|
|
||||||
|
notifyListeners("onBookmarkRequested", ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBookmarkDeleteRequested(
|
||||||
|
libraryItemId: String,
|
||||||
|
timeSeconds: Long
|
||||||
|
) {
|
||||||
|
val ret = JSObject()
|
||||||
|
ret.put("libraryItemId", libraryItemId)
|
||||||
|
ret.put("time", timeSeconds)
|
||||||
|
|
||||||
|
notifyListeners(
|
||||||
|
"onBookmarkDeleteRequested",
|
||||||
|
ret
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||||
|
|
@ -292,6 +318,46 @@ class AbsAudioPlayer : Plugin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun setAndroidAutoBookmarks(call: PluginCall) {
|
||||||
|
val bookmarksJson = call.getString("value")
|
||||||
|
|
||||||
|
if (bookmarksJson == null) {
|
||||||
|
call.reject("Missing bookmarks JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val bookmarksType =
|
||||||
|
jacksonMapper.typeFactory.constructCollectionType(
|
||||||
|
ArrayList::class.java,
|
||||||
|
AndroidAutoBookmark::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
val bookmarks: List<AndroidAutoBookmark> =
|
||||||
|
jacksonMapper.readValue(
|
||||||
|
bookmarksJson,
|
||||||
|
bookmarksType
|
||||||
|
)
|
||||||
|
|
||||||
|
playerNotificationService.setAndroidAutoBookmarks(
|
||||||
|
bookmarks
|
||||||
|
)
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.e(
|
||||||
|
tag,
|
||||||
|
"setAndroidAutoBookmarks failed",
|
||||||
|
error
|
||||||
|
)
|
||||||
|
call.reject(
|
||||||
|
"Invalid bookmarks JSON",
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getCurrentTime(call: PluginCall) {
|
fun getCurrentTime(call: PluginCall) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,280 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div class="audio-player-root">
|
||||||
<!-- Main container - only shows when playback session exists, applies fullscreen/platform classes -->
|
<!-- Main container - only shows when playback session exists, applies fullscreen/platform classes -->
|
||||||
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
|
<div
|
||||||
|
v-if="playbackSession && !showAndroidAutoDebug"
|
||||||
|
id="streamContainer"
|
||||||
|
class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none"
|
||||||
|
:class="{
|
||||||
|
fullscreen: showFullscreen,
|
||||||
|
'ios-player': $platform === 'ios',
|
||||||
|
'web-player': $platform === 'web'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
|
||||||
<!-- Fullscreen overlays: colored background and menu overlays -->
|
<!-- Fullscreen overlays: colored background and menu overlays -->
|
||||||
<div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
|
<div
|
||||||
|
v-if="showFullscreen && !showAndroidAutoDebug"
|
||||||
|
class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto"
|
||||||
|
:style="{ backgroundColor: coverRgb }"
|
||||||
|
>
|
||||||
<!-- Background gradient for player -->
|
<!-- Background gradient for player -->
|
||||||
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
|
<div
|
||||||
|
class="w-full h-full absolute top-0 left-0 pointer-events-none"
|
||||||
|
style="background: var(--gradient-audio-player)"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Collapse button - minimizes player -->
|
<!-- Collapse button - minimizes player -->
|
||||||
<div class="top-4 left-4 absolute cursor-pointer">
|
<div class="top-4 left-4 absolute cursor-pointer">
|
||||||
<span class="material-symbols text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @click="collapseFullscreen">keyboard_arrow_down</span>
|
<span
|
||||||
|
class="material-symbols text-5xl"
|
||||||
|
:class="{
|
||||||
|
'text-black text-opacity-75':
|
||||||
|
coverBgIsLight && theme !== 'black'
|
||||||
|
}"
|
||||||
|
@click="collapseFullscreen"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cast button - Chromecast toggle -->
|
<!-- Top right controls -->
|
||||||
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
|
<div class="top-6 right-4 absolute flex items-center gap-5 z-50">
|
||||||
<span class="material-symbols text-3xl" :class="coverBgIsLight && theme !== 'black' ? 'text-black' : ''" @click="castClick">{{ isCasting ? 'cast_connected' : 'cast' }}</span>
|
|
||||||
|
<!-- Cast -->
|
||||||
|
<span
|
||||||
|
v-if="showCastBtn"
|
||||||
|
class="material-symbols text-3xl cursor-pointer"
|
||||||
|
:class="
|
||||||
|
coverBgIsLight && theme !== 'black'
|
||||||
|
? 'text-black'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click.stop="castClick"
|
||||||
|
>
|
||||||
|
{{ isCasting ? 'cast_connected' : 'cast' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Android Auto DEBUG -->
|
||||||
|
<span
|
||||||
|
class="material-symbols text-3xl cursor-pointer"
|
||||||
|
:class="{
|
||||||
|
'text-black text-opacity-75':
|
||||||
|
coverBgIsLight && theme !== 'black'
|
||||||
|
}"
|
||||||
|
@click.stop="toggleAndroidAutoDebug"
|
||||||
|
>
|
||||||
|
directions_car
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- More menu -->
|
||||||
|
<span
|
||||||
|
class="material-symbols text-3xl cursor-pointer"
|
||||||
|
:class="{
|
||||||
|
'text-black text-opacity-75':
|
||||||
|
coverBgIsLight && theme !== 'black'
|
||||||
|
}"
|
||||||
|
@click.stop="showMoreMenuDialog = true"
|
||||||
|
>
|
||||||
|
more_vert
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player options menu button -->
|
<!-- Playback method indicator -->
|
||||||
<div class="top-6 right-4 absolute cursor-pointer">
|
<p
|
||||||
<span class="material-symbols text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @click="showMoreMenuDialog = true">more_vert</span>
|
class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75 z-50"
|
||||||
</div>
|
:class="{
|
||||||
|
'text-black text-opacity-75':
|
||||||
<!-- Playback method indicator (Direct/Local/Transcode) -->
|
coverBgIsLight && theme !== 'black'
|
||||||
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75 z-50" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
|
}"
|
||||||
|
style="font-size: 10px"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
isDirectPlayMethod
|
||||||
|
? $strings.LabelPlaybackDirect
|
||||||
|
: isLocalPlayMethod
|
||||||
|
? $strings.LabelPlaybackLocal
|
||||||
|
: $strings.LabelPlaybackTranscode
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overall book progress bar -->
|
<!-- Overall book progress bar -->
|
||||||
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
|
<div
|
||||||
|
v-if="
|
||||||
|
playerSettings.useChapterTrack &&
|
||||||
|
playerSettings.useTotalTrack &&
|
||||||
|
showFullscreen &&
|
||||||
|
!showAndroidAutoDebug
|
||||||
|
"
|
||||||
|
class="absolute total-track w-full z-30 px-6"
|
||||||
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
<p class="font-mono text-fg" style="font-size: 0.8rem">
|
||||||
|
{{ currentTimePretty }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
|
||||||
|
<p class="font-mono text-fg" style="font-size: 0.8rem">
|
||||||
|
{{ totalTimeRemainingPretty }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="h-1 w-full bg-track/50 relative rounded-full overflow-hidden">
|
<div
|
||||||
<div ref="totalReadyTrack" class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full" />
|
class="h-1 w-full bg-track/50 relative rounded-full overflow-hidden"
|
||||||
<div ref="totalBufferedTrack" class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full" />
|
>
|
||||||
<div ref="totalPlayedTrack" class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full" />
|
<div
|
||||||
|
ref="totalReadyTrack"
|
||||||
|
class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="totalBufferedTrack"
|
||||||
|
class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="totalPlayedTrack"
|
||||||
|
class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Book cover wrapper -->
|
<!-- Book cover wrapper -->
|
||||||
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
|
<div
|
||||||
|
class="cover-wrapper absolute z-30 pointer-events-auto"
|
||||||
|
@click="clickContainer"
|
||||||
|
>
|
||||||
<div class="w-full h-full flex justify-center">
|
<div class="w-full h-full flex justify-center">
|
||||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" />
|
<covers-book-cover
|
||||||
|
v-if="libraryItem || localLibraryItemCoverSrc"
|
||||||
|
ref="cover"
|
||||||
|
:library-item="libraryItem"
|
||||||
|
:download-cover="localLibraryItemCoverSrc"
|
||||||
|
:width="bookCoverWidth"
|
||||||
|
:book-cover-aspect-ratio="bookCoverAspectRatio"
|
||||||
|
raw
|
||||||
|
@imageLoaded="coverImageLoaded"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sync failed indicator - shows error icon if progress sync failed -->
|
<!-- Sync failed indicator -->
|
||||||
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30" @click.stop="showSyncsFailedDialog">
|
<div
|
||||||
<span class="material-symbols text-error text-3xl">error</span>
|
v-if="syncStatus === $constants.SyncStatus.FAILED"
|
||||||
|
class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30"
|
||||||
|
@click.stop="showSyncsFailedDialog"
|
||||||
|
>
|
||||||
|
<span class="material-symbols text-error text-3xl">
|
||||||
|
error
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Title and author text -->
|
<!-- Title and author text -->
|
||||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
<div
|
||||||
<div ref="titlewrapper" class="overflow-hidden relative">
|
class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden"
|
||||||
|
@click="clickTitleAndAuthor"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="titlewrapper"
|
||||||
|
class="overflow-hidden relative"
|
||||||
|
>
|
||||||
<p class="title-text whitespace-nowrap"></p>
|
<p class="title-text whitespace-nowrap"></p>
|
||||||
</div>
|
</div>
|
||||||
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
|
|
||||||
|
<p class="author-text text-fg text-opacity-75 truncate">
|
||||||
|
{{ authorName }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
|
<!-- Main player content -->
|
||||||
<!-- Top controls bar - fullscreen only: bookmarks, speed, sleep timer, chapters -->
|
<div
|
||||||
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
|
v-if="!showAndroidAutoDebug"
|
||||||
|
id="playerContent"
|
||||||
|
class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all"
|
||||||
|
:style="{ backgroundColor: showFullscreen ? '' : coverRgb }"
|
||||||
|
@click="clickContainer"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Top controls bar - fullscreen only -->
|
||||||
|
<div
|
||||||
|
v-if="showFullscreen"
|
||||||
|
class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6"
|
||||||
|
style="max-width: 414px"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<span v-if="!isPodcast && serverLibraryItemId && socketConnected" class="material-symbols text-3xl text-fg-muted cursor-pointer" :class="{ fill: bookmarks.length }" @click="$emit('showBookmarks')">bookmark</span>
|
|
||||||
|
<span
|
||||||
|
v-if="!isPodcast && serverLibraryItemId && socketConnected"
|
||||||
|
class="material-symbols text-3xl text-fg-muted cursor-pointer"
|
||||||
|
:class="{ fill: bookmarks.length }"
|
||||||
|
@click="$emit('showBookmarks')"
|
||||||
|
>
|
||||||
|
bookmark
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- hidden for podcasts but still using this as a placeholder -->
|
<!-- hidden for podcasts but still using this as a placeholder -->
|
||||||
<span v-else class="material-symbols text-3xl text-white text-opacity-0">bookmark</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="material-symbols text-3xl text-white text-opacity-0"
|
||||||
|
>
|
||||||
|
bookmark
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="font-mono text-fg-muted cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
|
<span
|
||||||
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-fg-muted cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
|
class="font-mono text-fg-muted cursor-pointer"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
style="font-size: 1.35rem"
|
||||||
|
@click="$emit('selectPlaybackSpeed')"
|
||||||
|
>
|
||||||
|
{{ currentPlaybackRate }}x
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
v-if="!sleepTimerRunning"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-7 w-7 text-fg-muted cursor-pointer"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
@click.stop="$emit('showSleepTimer')"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div v-else class="h-7 w-7 flex items-center justify-around cursor-pointer" @click.stop="$emit('showSleepTimer')">
|
|
||||||
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-7 w-7 flex items-center justify-around cursor-pointer"
|
||||||
|
@click.stop="$emit('showSleepTimer')"
|
||||||
|
>
|
||||||
|
<p class="text-xl font-mono text-success">
|
||||||
|
{{ sleepTimeRemainingPretty }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="material-symbols text-3xl text-fg cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
|
<span
|
||||||
|
class="material-symbols text-3xl text-fg cursor-pointer"
|
||||||
|
:class="
|
||||||
|
chapters.length
|
||||||
|
? 'text-opacity-75'
|
||||||
|
: 'text-opacity-10'
|
||||||
|
"
|
||||||
|
@click="clickChaptersBtn"
|
||||||
|
>
|
||||||
|
format_list_bulleted
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
|
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
|
||||||
|
|
||||||
<!-- Playback controls - jump buttons, play/pause, chapter navigation -->
|
<!-- Playback controls - jump buttons, play/pause, chapter navigation -->
|
||||||
|
|
@ -126,6 +321,177 @@
|
||||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
||||||
<!-- More menu dialog - player settings and options -->
|
<!-- More menu dialog - player settings and options -->
|
||||||
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
|
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
|
||||||
|
</div>
|
||||||
|
<!-- Android Auto DEBUG panel -->
|
||||||
|
<div
|
||||||
|
v-if="showFullscreen && showAndroidAutoDebug"
|
||||||
|
class="fixed left-0 top-0 w-full h-full z-[2147483647] flex items-center justify-center bg-transparent pointer-events-auto"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md mx-4 rounded-xl bg-gray-900 text-white p-5 shadow-xl">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
Android Auto DEBUG
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-400">
|
||||||
|
Simulazione MediaSession
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="material-symbols text-2xl cursor-pointer"
|
||||||
|
@click="closeAndroidAutoDebug"
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentChapter">
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mt-2 space-y-1">
|
||||||
|
<div>Tempo assoluto: {{ currentTime.toFixed(2) }} s</div>
|
||||||
|
<div>Inizio capitolo: {{ Number(currentChapter.start).toFixed(2) }} s</div>
|
||||||
|
<div>Posizione capitolo: {{ Math.max(0, currentTime - currentChapter.start).toFixed(2) }} s</div>
|
||||||
|
<div>Durata capitolo: {{ Number(currentChapterDuration).toFixed(2) }} s</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-base font-semibold">
|
||||||
|
{{ currentChapterTitle || 'Senza titolo' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-400 mt-1">
|
||||||
|
Capitolo {{ chapters.findIndex(c => c.id === currentChapter.id) + 1 }}
|
||||||
|
/ {{ chapters.length }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 h-3 bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full bg-blue-500"
|
||||||
|
:style="{
|
||||||
|
width: `${Math.min(
|
||||||
|
100,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
((currentTime - currentChapter.start) / currentChapterDuration) * 100
|
||||||
|
)
|
||||||
|
)}%`
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between text-sm text-gray-400 mt-2">
|
||||||
|
<span>
|
||||||
|
{{ $secondsToTimestamp(Math.max(0, currentTime - currentChapter.start)) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ $secondsToTimestamp(currentChapterDuration) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-8 mt-6">
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="material-symbols text-4xl cursor-pointer"
|
||||||
|
@click="jumpChapterStart"
|
||||||
|
>
|
||||||
|
skip_previous
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="material-symbols text-5xl cursor-pointer"
|
||||||
|
@click="playPauseClick"
|
||||||
|
>
|
||||||
|
{{ isPlaying ? 'pause_circle' : 'play_circle' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="material-symbols text-4xl cursor-pointer"
|
||||||
|
:class="{ 'text-opacity-30': !nextChapter }"
|
||||||
|
@click="jumpNextChapter"
|
||||||
|
>
|
||||||
|
skip_next
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-4 mt-5">
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 rounded bg-gray-700"
|
||||||
|
@click="jumpBackwards"
|
||||||
|
>
|
||||||
|
−{{ jumpBackwardsLabel }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 rounded bg-gray-700"
|
||||||
|
@click="jumpForward"
|
||||||
|
>
|
||||||
|
+{{ jumpForwardLabel }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="w-full mt-5 rounded bg-gray-800">
|
||||||
|
<summary
|
||||||
|
class="w-full py-3 px-4 cursor-pointer select-none font-semibold"
|
||||||
|
>
|
||||||
|
Elenco capitoli
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="chapters.length"
|
||||||
|
class="max-h-64 overflow-y-auto border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(chapter, index) in chapters"
|
||||||
|
:key="chapter.id"
|
||||||
|
type="button"
|
||||||
|
class="w-full px-4 py-3 text-left border-b border-gray-700 last:border-b-0"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-700':
|
||||||
|
currentChapter && chapter.id === currentChapter.id,
|
||||||
|
'bg-gray-800':
|
||||||
|
!currentChapter || chapter.id !== currentChapter.id
|
||||||
|
}"
|
||||||
|
@click.stop="selectChapter(chapter)"
|
||||||
|
>
|
||||||
|
<div class="font-semibold">
|
||||||
|
Capitolo {{ index + 1 }}/{{ chapters.length }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-300 truncate">
|
||||||
|
{{ chapter.title || 'Senza titolo' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
|
{{ $secondsToTimestamp(Number(chapter.start)) }}
|
||||||
|
-
|
||||||
|
{{ $secondsToTimestamp(Number(chapter.end)) }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="px-4 py-4 text-sm text-gray-400 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
Nessun capitolo disponibile
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-gray-400 py-10">
|
||||||
|
Nessun capitolo corrente
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -155,6 +521,7 @@ export default {
|
||||||
playbackSession: null,
|
playbackSession: null,
|
||||||
showChapterModal: false,
|
showChapterModal: false,
|
||||||
showFullscreen: false,
|
showFullscreen: false,
|
||||||
|
showAndroidAutoDebug: false,
|
||||||
totalDuration: 0,
|
totalDuration: 0,
|
||||||
currentPlaybackRate: 1,
|
currentPlaybackRate: 1,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
|
|
@ -448,6 +815,104 @@ export default {
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleAndroidAutoDebug() {
|
||||||
|
this.showAndroidAutoDebug = true
|
||||||
|
this.showFullscreen = true
|
||||||
|
},
|
||||||
|
closeAndroidAutoDebug() {
|
||||||
|
this.showAndroidAutoDebug = false
|
||||||
|
},
|
||||||
|
async onBookmarkDeleteRequested(payload) {
|
||||||
|
const libraryItemId =
|
||||||
|
payload?.libraryItemId || this.serverLibraryItemId
|
||||||
|
|
||||||
|
const time =
|
||||||
|
Math.floor(Number(payload?.time))
|
||||||
|
|
||||||
|
if (!libraryItemId || !Number.isFinite(time)) {
|
||||||
|
console.error(
|
||||||
|
'Invalid Android Auto bookmark delete request',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$nativeHttp.delete(
|
||||||
|
`/api/me/item/${libraryItemId}/bookmark/${time}`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.$store.commit(
|
||||||
|
'user/deleteBookmark',
|
||||||
|
{
|
||||||
|
libraryItemId,
|
||||||
|
time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.$toast.success('Bookmark removed')
|
||||||
|
} catch (error) {
|
||||||
|
this.$toast.error(
|
||||||
|
this.$strings.ToastBookmarkRemoveFailed
|
||||||
|
)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onBookmarkRequested(payload) {
|
||||||
|
const libraryItemId =
|
||||||
|
payload?.libraryItemId || this.serverLibraryItemId
|
||||||
|
|
||||||
|
const time =
|
||||||
|
Math.floor(
|
||||||
|
Number(
|
||||||
|
payload?.time !== undefined
|
||||||
|
? payload.time
|
||||||
|
: this.currentTime
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!libraryItemId || !Number.isFinite(time)) {
|
||||||
|
console.error(
|
||||||
|
'Invalid Android Auto bookmark request',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBookmark =
|
||||||
|
this.bookmarks.find(
|
||||||
|
(bookmark) => Number(bookmark.time) === time
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingBookmark) {
|
||||||
|
this.$toast.success('Bookmark already exists')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmark = {
|
||||||
|
title: `Android Auto - ${this.$formatDate(
|
||||||
|
Date.now(),
|
||||||
|
'MMM dd, yyyy HH:mm'
|
||||||
|
)}`,
|
||||||
|
time
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$nativeHttp.post(
|
||||||
|
`/api/me/item/${libraryItemId}/bookmark`,
|
||||||
|
bookmark
|
||||||
|
)
|
||||||
|
|
||||||
|
this.$toast.success('Bookmark added')
|
||||||
|
} catch (error) {
|
||||||
|
this.$toast.error(
|
||||||
|
this.$strings.ToastBookmarkCreateFailed
|
||||||
|
)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async selectChapter(chapter) {
|
async selectChapter(chapter) {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
|
|
@ -580,20 +1045,26 @@ export default {
|
||||||
ts.innerText = this.$secondsToTimestamp(currentTime)
|
ts.innerText = this.$secondsToTimestamp(currentTime)
|
||||||
},
|
},
|
||||||
timeupdate() {
|
timeupdate() {
|
||||||
if (!this.$refs.playedTrack) {
|
|
||||||
console.error('Invalid no played track ref')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$emit('updateTime', this.currentTime)
|
this.$emit('updateTime', this.currentTime)
|
||||||
|
|
||||||
|
// A native time update confirms that the seek completed.
|
||||||
|
// Clear the loading state even when the normal player UI is hidden,
|
||||||
|
// as happens while the Android Auto DEBUG panel is displayed.
|
||||||
if (this.seekLoading) {
|
if (this.seekLoading) {
|
||||||
this.seekLoading = false
|
this.seekLoading = false
|
||||||
|
|
||||||
if (this.$refs.playedTrack) {
|
if (this.$refs.playedTrack) {
|
||||||
this.$refs.playedTrack.classList.remove('bg-yellow-300')
|
this.$refs.playedTrack.classList.remove('bg-yellow-300')
|
||||||
this.$refs.playedTrack.classList.add('bg-gray-200')
|
this.$refs.playedTrack.classList.add('bg-gray-200')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The normal progress-bar refs do not exist in Android Auto DEBUG.
|
||||||
|
// The debug panel uses reactive values and needs no direct DOM update.
|
||||||
|
if (!this.$refs.playedTrack) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
},
|
},
|
||||||
|
|
@ -921,6 +1392,14 @@ export default {
|
||||||
AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
||||||
AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
||||||
AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
|
AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
|
||||||
|
AbsAudioPlayer.addListener(
|
||||||
|
'onBookmarkRequested',
|
||||||
|
this.onBookmarkRequested
|
||||||
|
)
|
||||||
|
AbsAudioPlayer.addListener(
|
||||||
|
'onBookmarkDeleteRequested',
|
||||||
|
this.onBookmarkDeleteRequested
|
||||||
|
)
|
||||||
},
|
},
|
||||||
async screenOrientationChange() {
|
async screenOrientationChange() {
|
||||||
if (this.isRefreshingUI) return
|
if (this.isRefreshingUI) return
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mixins: [CellularPermissionHelpers],
|
mixins: [CellularPermissionHelpers],
|
||||||
|
watch: {
|
||||||
|
bookmarks: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.syncAndroidAutoBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.serverLibraryItemId) return []
|
if (!this.serverLibraryItemId) return []
|
||||||
|
|
@ -54,6 +63,32 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async syncAndroidAutoBookmarks() {
|
||||||
|
const payload =
|
||||||
|
this.bookmarks.map((bookmark) => ({
|
||||||
|
title:
|
||||||
|
bookmark.title ||
|
||||||
|
this.$formatDate(
|
||||||
|
Number(bookmark.time) * 1000,
|
||||||
|
'MMM dd, yyyy HH:mm'
|
||||||
|
),
|
||||||
|
timeSeconds: Math.floor(
|
||||||
|
Number(bookmark.time)
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AbsAudioPlayer.setAndroidAutoBookmarks({
|
||||||
|
value: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Failed to sync Android Auto bookmarks',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showBookmarks() {
|
showBookmarks() {
|
||||||
this.showBookmarksModal = true
|
this.showBookmarksModal = true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue