mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-29 06:37:16 +02:00
Merge branch 'master' into bug-ios-background-currenttime
This commit is contained in:
@@ -37,9 +37,9 @@ data class DeviceSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L
|
val jumpBackwardsTimeMs get() = (jumpBackwardsTime ?: default().jumpBackwardsTime) * 1000L
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val jumpForwardTimeMs get() = jumpForwardTime * 1000L
|
val jumpForwardTimeMs get() = (jumpForwardTime ?: default().jumpBackwardsTime) * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeviceData(
|
data class DeviceData(
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.R
|
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -21,7 +19,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
|
|
||||||
private var mediaButtonClickCount: Int = 0
|
private var mediaButtonClickCount: Int = 0
|
||||||
var mediaButtonClickTimeout: Long = 1000 //ms
|
var mediaButtonClickTimeout: Long = 1000 //ms
|
||||||
var seekAmount: Long = 20000 //ms
|
|
||||||
|
|
||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
@@ -75,19 +72,19 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
playerNotificationService.seekBackward(seekAmount)
|
playerNotificationService.skipToPrevious()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.skipToNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFastForward() {
|
override fun onFastForward() {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpForward()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRewind() {
|
override fun onRewind() {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpBackward()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekTo(pos: Long) {
|
override fun onSeekTo(pos: Long) {
|
||||||
@@ -179,10 +176,10 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpForward()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
||||||
playerNotificationService.seekBackward(seekAmount)
|
playerNotificationService.jumpBackward()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
||||||
playerNotificationService.closePlayback()
|
playerNotificationService.closePlayback()
|
||||||
@@ -226,22 +223,22 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun handleMessage(msg: Message) {
|
override fun handleMessage(msg: Message) {
|
||||||
super.handleMessage(msg)
|
super.handleMessage(msg)
|
||||||
if (2 == msg.what) {
|
if (2 == msg.what) {
|
||||||
playerNotificationService.seekBackward(seekAmount)
|
playerNotificationService.jumpBackward()
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
}
|
}
|
||||||
else if (msg.what >= 3) {
|
else if (msg.what >= 3) {
|
||||||
playerNotificationService.seekForward(seekAmount)
|
playerNotificationService.jumpForward()
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example Using a custom action in android auto
|
override fun onCustomAction(action: String?, extras: Bundle?) {
|
||||||
// override fun onCustomAction(action: String?, extras: Bundle?) {
|
super.onCustomAction(action, extras)
|
||||||
// super.onCustomAction(action, extras)
|
|
||||||
//
|
when (action) {
|
||||||
// if ("com.audiobookshelf.app.PLAYBACK_RATE" == action) {
|
CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
|
||||||
//
|
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
|
const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
|
||||||
|
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
||||||
+62
-17
@@ -20,7 +20,6 @@ import android.util.Log
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
import com.audiobookshelf.app.BuildConfig
|
import com.audiobookshelf.app.BuildConfig
|
||||||
@@ -30,9 +29,11 @@ import com.audiobookshelf.app.data.DeviceInfo
|
|||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.CustomActionProvider
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||||
import com.google.android.exoplayer2.source.MediaSource
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
@@ -42,6 +43,7 @@ import com.google.android.exoplayer2.upstream.*
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
|
||||||
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
||||||
|
|
||||||
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
@@ -294,17 +296,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
||||||
|
|
||||||
// Example adding custom action with icon in android auto
|
mediaSessionConnector.setCustomActionProviders(
|
||||||
// mediaSessionConnector.setCustomActionProviders(object : MediaSessionConnector.CustomActionProvider {
|
JumpForwardCustomActionProvider(),
|
||||||
// override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
JumpBackwardCustomActionProvider(),
|
||||||
// }
|
)
|
||||||
// override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
|
||||||
// var icon = R.drawable.exo_icon_rewind
|
|
||||||
// return PlaybackStateCompat.CustomAction.Builder(
|
|
||||||
// "com.audiobookshelf.app.PLAYBACK_RATE", "Playback Rate", icon)
|
|
||||||
// .build()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
mediaSession.setCallback(MediaSessionCallback(this))
|
mediaSession.setCallback(MediaSessionCallback(this))
|
||||||
|
|
||||||
@@ -320,13 +315,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
1000 * 20 // 20s playback rebuffer
|
1000 * 20 // 20s playback rebuffer
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
val seekBackTime = DeviceManager.deviceData.deviceSettings?.jumpBackwardsTimeMs ?: 10000
|
|
||||||
val seekForwardTime = DeviceManager.deviceData.deviceSettings?.jumpForwardTimeMs ?: 10000
|
|
||||||
|
|
||||||
mPlayer = ExoPlayer.Builder(this)
|
mPlayer = ExoPlayer.Builder(this)
|
||||||
.setLoadControl(customLoadControl)
|
.setLoadControl(customLoadControl)
|
||||||
.setSeekBackIncrementMs(seekBackTime)
|
.setSeekBackIncrementMs(deviceSettings.jumpBackwardsTimeMs)
|
||||||
.setSeekForwardIncrementMs(seekForwardTime)
|
.setSeekForwardIncrementMs(deviceSettings.jumpForwardTimeMs)
|
||||||
.build()
|
.build()
|
||||||
mPlayer.setHandleAudioBecomingNoisy(true)
|
mPlayer.setHandleAudioBecomingNoisy(true)
|
||||||
mPlayer.addListener(PlayerListener(this))
|
mPlayer.addListener(PlayerListener(this))
|
||||||
@@ -701,6 +693,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun skipToPrevious() {
|
||||||
|
currentPlayer.seekToPrevious()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skipToNext() {
|
||||||
|
currentPlayer.seekToNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jumpForward() {
|
||||||
|
seekForward(deviceSettings.jumpForwardTimeMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun jumpBackward() {
|
||||||
|
seekBackward(deviceSettings.jumpBackwardsTimeMs)
|
||||||
|
}
|
||||||
|
|
||||||
fun seekForward(amount: Long) {
|
fun seekForward(amount: Long) {
|
||||||
seekPlayer(getCurrentTime() + amount)
|
seekPlayer(getCurrentTime() + amount)
|
||||||
}
|
}
|
||||||
@@ -757,6 +765,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
||||||
|
|
||||||
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
||||||
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
||||||
}
|
}
|
||||||
@@ -966,5 +977,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
|
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class JumpBackwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_JUMP_BACKWARD,
|
||||||
|
getContext().getString(R.string.action_jump_backward),
|
||||||
|
R.drawable.exo_icon_rewind
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class JumpForwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_JUMP_FORWARD,
|
||||||
|
getContext().getString(R.string.action_jump_forward),
|
||||||
|
R.drawable.exo_icon_fastforward
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,6 @@
|
|||||||
<string name="custom_url_scheme">com.audiobookshelf.app</string>
|
<string name="custom_url_scheme">com.audiobookshelf.app</string>
|
||||||
<string name="add_widget">Add widget</string>
|
<string name="add_widget">Add widget</string>
|
||||||
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
||||||
|
<string name="action_jump_forward">Jump Forward</string>
|
||||||
|
<string name="action_jump_backward">Jump Backward</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.google.gms:google-services:4.3.5'
|
classpath 'com.google.gms:google-services:4.3.10'
|
||||||
classpath 'com.android.tools.build:gradle:7.2.0-beta04'
|
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -113,6 +113,13 @@ export default {
|
|||||||
text: 'Local Media',
|
text: 'Local Media',
|
||||||
to: '/localMedia/folders'
|
to: '/localMedia/folders'
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
icon: 'download',
|
||||||
|
iconOutlined: false,
|
||||||
|
text: 'Downloads',
|
||||||
|
to: '/downloads'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<!-- No progress shown for collapsed series in library -->
|
<!-- No progress shown for collapsed series in library -->
|
||||||
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></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="showHasLocalDownload" class="absolute right-0 top-0 z-20" :style="{ top: (isPodcast ? 1.75 : 0.375) * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
|
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,6 +164,7 @@ export default {
|
|||||||
return this.media.numTracks
|
return this.media.numTracks
|
||||||
},
|
},
|
||||||
numEpisodes() {
|
numEpisodes() {
|
||||||
|
if (this.isLocal && this.isPodcast && this.media.episodes) return this.media.episodes.length
|
||||||
return this.media.numEpisodes
|
return this.media.numEpisodes
|
||||||
},
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
@@ -351,6 +352,17 @@ export default {
|
|||||||
if (!this.isAltViewEnabled) return 0
|
if (!this.isAltViewEnabled) return 0
|
||||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||||
return 4.25 * this.sizeMultiplier
|
return 4.25 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
showHasLocalDownload() {
|
||||||
|
if (this.localLibraryItem || this.isLocal) {
|
||||||
|
if (this.recentEpisode && !this.isLocal) {
|
||||||
|
const localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === this.recentEpisode.id)
|
||||||
|
return !!localEpisode
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -399,8 +411,26 @@ export default {
|
|||||||
this.selectBtnClick()
|
this.selectBtnClick()
|
||||||
} else if (this.recentEpisode) {
|
} else if (this.recentEpisode) {
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
if (this.streamIsPlaying) eventBus.$emit('pause-item')
|
if (this.streamIsPlaying) {
|
||||||
else eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
|
eventBus.$emit('pause-item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.localLibraryItem) {
|
||||||
|
const localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === this.recentEpisode.id)
|
||||||
|
if (localEpisode) {
|
||||||
|
// Play episode locally
|
||||||
|
eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.localLibraryItemId,
|
||||||
|
episodeId: localEpisode.id,
|
||||||
|
serverLibraryItemId: this.libraryItemId,
|
||||||
|
serverEpisodeId: this.recentEpisode.id
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
|
||||||
} else {
|
} else {
|
||||||
var router = this.$router || this.$nuxt.$router
|
var router = this.$router || this.$nuxt.$router
|
||||||
if (router) {
|
if (router) {
|
||||||
|
|||||||
@@ -6,9 +6,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div
|
||||||
|
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
|
||||||
|
@click="
|
||||||
|
show = false
|
||||||
|
manualTimerModal = false
|
||||||
|
"
|
||||||
|
>
|
||||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||||
<ul v-if="!sleepTimerRunning" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<div v-if="manualTimerModal" class="p-4">
|
||||||
|
<div class="flex mb-4" @click="manualTimerModal = false">
|
||||||
|
<span class="material-icons text-3xl">arrow_back</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex my-2 justify-between">
|
||||||
|
<ui-btn @click="decreaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">remove</span></ui-btn>
|
||||||
|
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
|
||||||
|
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
|
||||||
|
</div>
|
||||||
|
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">Set Timer</ui-btn>
|
||||||
|
</div>
|
||||||
|
<ul v-else-if="!sleepTimerRunning" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="timeout in timeouts">
|
<template v-for="timeout in timeouts">
|
||||||
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
|
<li :key="timeout" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(timeout)">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -21,8 +38,13 @@
|
|||||||
<span class="font-normal block truncate text-lg text-center">End of Chapter</span>
|
<span class="font-normal block truncate text-lg text-center">End of Chapter</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="manualTimerModal = true">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="font-normal block truncate text-lg text-center">Manual sleep timer</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-else class="px-2 py-4">
|
<div v-else class="p-4">
|
||||||
<div class="flex my-2 justify-between">
|
<div class="flex my-2 justify-between">
|
||||||
<ui-btn @click="decreaseSleepTime" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">remove</span></ui-btn>
|
<ui-btn @click="decreaseSleepTime" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">remove</span></ui-btn>
|
||||||
<p class="text-2xl font-mono text-center">{{ timeRemainingPretty }}</p>
|
<p class="text-2xl font-mono text-center">{{ timeRemainingPretty }}</p>
|
||||||
@@ -45,7 +67,10 @@ export default {
|
|||||||
currentEndOfChapterTime: Number
|
currentEndOfChapterTime: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
manualTimerModal: null,
|
||||||
|
manualTimeoutMin: 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
@@ -57,7 +82,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeouts() {
|
timeouts() {
|
||||||
return [1, 5, 10, 15, 30, 45, 60, 90]
|
return [5, 10, 15, 30, 45, 60, 90]
|
||||||
},
|
},
|
||||||
timeRemainingPretty() {
|
timeRemainingPretty() {
|
||||||
return this.$secondsToTimestamp(this.currentTime)
|
return this.$secondsToTimestamp(this.currentTime)
|
||||||
@@ -71,6 +96,7 @@ export default {
|
|||||||
clickedOption(timeoutMin) {
|
clickedOption(timeoutMin) {
|
||||||
var timeout = timeoutMin * 1000 * 60
|
var timeout = timeoutMin * 1000 * 60
|
||||||
this.show = false
|
this.show = false
|
||||||
|
this.manualTimerModal = false
|
||||||
this.$nextTick(() => this.$emit('change', { time: timeout, isChapterTime: false }))
|
this.$nextTick(() => this.$emit('change', { time: timeout, isChapterTime: false }))
|
||||||
},
|
},
|
||||||
cancelSleepTimer() {
|
cancelSleepTimer() {
|
||||||
@@ -82,6 +108,12 @@ export default {
|
|||||||
},
|
},
|
||||||
decreaseSleepTime() {
|
decreaseSleepTime() {
|
||||||
this.$emit('decrease')
|
this.$emit('decrease')
|
||||||
|
},
|
||||||
|
increaseManualTimeout() {
|
||||||
|
this.manualTimeoutMin++
|
||||||
|
},
|
||||||
|
decreaseManualTimeout() {
|
||||||
|
if (this.manualTimeoutMin > 1) this.manualTimeoutMin--
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default {
|
|||||||
episodeId: this.episode.id
|
episodeId: this.episode.id
|
||||||
}
|
}
|
||||||
if (localFolder) {
|
if (localFolder) {
|
||||||
this.localFolderId = localFolder.id
|
payload.localFolderId = localFolder.id
|
||||||
}
|
}
|
||||||
var downloadRes = await AbsDownloader.downloadLibraryItem(payload)
|
var downloadRes = await AbsDownloader.downloadLibraryItem(payload)
|
||||||
if (downloadRes && downloadRes.error) {
|
if (downloadRes && downloadRes.error) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
// Fetch the most recent active session
|
// Fetch the most recent active session
|
||||||
let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last
|
let activeSession = try await Realm().objects(PlaybackSession.self).where({ $0.isActiveSession == true }).last?.freeze()
|
||||||
if let activeSession = activeSession {
|
if let activeSession = activeSession {
|
||||||
await PlayerProgress.shared.syncFromServer()
|
await PlayerProgress.shared.syncFromServer()
|
||||||
try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate)
|
try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate)
|
||||||
|
|||||||
+6
-6
@@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|||||||
@@ -155,8 +155,13 @@ extension LocalMediaProgress {
|
|||||||
|
|
||||||
static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? {
|
static func fetchOrCreateLocalMediaProgress(localMediaProgressId: String?, localLibraryItemId: String?, localEpisodeId: String?) -> LocalMediaProgress? {
|
||||||
if let localMediaProgressId = localMediaProgressId {
|
if let localMediaProgressId = localMediaProgressId {
|
||||||
return Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId)
|
// Check if it existing in the database, if not, we need to create it
|
||||||
} else if let localLibraryItemId = localLibraryItemId {
|
if let progress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) {
|
||||||
|
return progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let localLibraryItemId = localLibraryItemId {
|
||||||
guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil }
|
guard let localLibraryItem = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) else { return nil }
|
||||||
let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId)
|
let episode = localLibraryItem.getPodcastEpisode(episodeId: localEpisodeId)
|
||||||
return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode)
|
return LocalMediaProgress(localLibraryItem: localLibraryItem, episode: episode)
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ class AudioPlayer: NSObject {
|
|||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
public func play(allowSeekBack: Bool = false) {
|
public func play(allowSeekBack: Bool = false) {
|
||||||
|
guard self.isInitialized() else { return }
|
||||||
|
|
||||||
if allowSeekBack {
|
if allowSeekBack {
|
||||||
let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime
|
let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime
|
||||||
var time: Int?
|
var time: Int?
|
||||||
@@ -262,6 +264,8 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func pause() {
|
public func pause() {
|
||||||
|
guard self.isInitialized() else { return }
|
||||||
|
|
||||||
self.audioPlayer.pause()
|
self.audioPlayer.pause()
|
||||||
self.status = 0
|
self.status = 0
|
||||||
self.rate = 0.0
|
self.rate = 0.0
|
||||||
|
|||||||
@@ -123,17 +123,26 @@ class PlayerProgress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLocalSessionFromServerMediaProgress() async {
|
private static func updateLocalSessionFromServerMediaProgress() async {
|
||||||
NSLog("checkCurrentSessionProgress: Checking if local media progress was updated on server")
|
NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
|
||||||
guard let session = PlayerHandler.getPlaybackSession()?.freeze() else { return }
|
guard let session = try! await Realm().objects(PlaybackSession.self).last(where: { $0.isActiveSession == true })?.freeze() else {
|
||||||
|
NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the current progress
|
// Fetch the current progress
|
||||||
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
|
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
|
||||||
guard let progress = progress else { return }
|
guard let progress = progress else {
|
||||||
|
NSLog("updateLocalSessionFromServerMediaProgress: No progress object")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Determine which session is newer
|
// Determine which session is newer
|
||||||
let serverLastUpdate = progress.lastUpdate
|
let serverLastUpdate = progress.lastUpdate
|
||||||
guard let localLastUpdate = session.updatedAt else { return }
|
guard let localLastUpdate = session.updatedAt else {
|
||||||
|
NSLog("updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
let serverCurrentTime = progress.currentTime
|
let serverCurrentTime = progress.currentTime
|
||||||
let localCurrentTime = session.currentTime
|
let localCurrentTime = session.currentTime
|
||||||
|
|
||||||
@@ -142,12 +151,16 @@ class PlayerProgress {
|
|||||||
|
|
||||||
// Update the session, if needed
|
// Update the session, if needed
|
||||||
if serverIsNewerThanLocal && currentTimeIsDifferent {
|
if serverIsNewerThanLocal && currentTimeIsDifferent {
|
||||||
|
NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
|
||||||
guard let session = session.thaw() else { return }
|
guard let session = session.thaw() else { return }
|
||||||
session.update {
|
session.update {
|
||||||
session.currentTime = serverCurrentTime
|
session.currentTime = serverCurrentTime
|
||||||
session.updatedAt = serverLastUpdate
|
session.updatedAt = serverLastUpdate
|
||||||
}
|
}
|
||||||
|
NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
|
||||||
PlayerHandler.seek(amount: session.currentTime)
|
PlayerHandler.seek(amount: session.currentTime)
|
||||||
|
} else {
|
||||||
|
NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
})
|
})
|
||||||
categories = categories.map((cat) => {
|
categories = categories.map((cat) => {
|
||||||
console.log('[breadcrumb] Personalized category from server', cat.type)
|
console.log('[breadcrumb] Personalized category from server', cat.type)
|
||||||
if (cat.type == 'book' || cat.type == 'podcast') {
|
if (cat.type == 'book' || cat.type == 'podcast' || cat.type == 'episode') {
|
||||||
// Map localLibraryItem to entities
|
// Map localLibraryItem to entities
|
||||||
cat.entities = cat.entities.map((entity) => {
|
cat.entities = cat.entities.map((entity) => {
|
||||||
var localLibraryItem = this.localLibraryItems.find((lli) => {
|
var localLibraryItem = this.localLibraryItems.find((lli) => {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full py-6 px-4 overflow-y-auto">
|
||||||
|
<p class="mb-2 text-base text-white">Downloads ({{ localLibraryItems.length }})</p>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<template v-for="(mediaItem, num) in localLibraryItems">
|
||||||
|
<div :key="mediaItem.id" class="w-full">
|
||||||
|
<nuxt-link :to="`/localMedia/item/${mediaItem.id}`" class="flex items-center">
|
||||||
|
<div class="w-16 h-16 min-w-16 min-h-16 flex-none bg-primary relative">
|
||||||
|
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div class="px-2 flex-grow">
|
||||||
|
<p class="text-sm">{{ mediaItem.media.metadata.title }}</p>
|
||||||
|
<p v-if="mediaItem.mediaType == 'book'" class="text-xs text-gray-300">{{ mediaItem.media.tracks.length }} Track{{ mediaItem.media.tracks.length == 1 ? '' : 's' }}</p>
|
||||||
|
<p v-else-if="mediaItem.mediaType == 'podcast'" class="text-xs text-gray-300">{{ mediaItem.media.episodes.length }} Episode{{ mediaItem.media.episodes.length == 1 ? '' : 's' }}</p>
|
||||||
|
<p v-if="mediaItem.size" class="text-xs text-gray-300">{{ $bytesPretty(mediaItem.size) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 flex items-center justify-center">
|
||||||
|
<span class="material-icons text-2xl text-gray-400">chevron_right</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<div v-if="num+1 < localLibraryItems.length" class="flex border-t border-white border-opacity-10 my-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
localLibraryItems: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSize(item) {
|
||||||
|
if (!item || !item.localFiles) return 0
|
||||||
|
let size = 0
|
||||||
|
for (let i = 0; i < item.localFiles.length; i++) {
|
||||||
|
size += item.localFiles[i].size
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
},
|
||||||
|
newLocalLibraryItem(item) {
|
||||||
|
if (!item) return
|
||||||
|
const itemIndex = this.localLibraryItems.findIndex((li) => li.id === item.id)
|
||||||
|
const newItemObj = {
|
||||||
|
...item,
|
||||||
|
size: this.getSize(item),
|
||||||
|
coverPathSrc: item.coverContentUrl ? Capacitor.convertFileSrc(item.coverContentUrl) : null
|
||||||
|
}
|
||||||
|
if (itemIndex >= 0) {
|
||||||
|
this.localLibraryItems.splice(itemIndex, 1, newItemObj)
|
||||||
|
} else {
|
||||||
|
this.localLibraryItems.push(newItemObj)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
var items = (await this.$db.getLocalLibraryItems()) || []
|
||||||
|
this.localLibraryItems = items.map((lmi) => {
|
||||||
|
console.log('Local library item', JSON.stringify(lmi))
|
||||||
|
return {
|
||||||
|
...lmi,
|
||||||
|
size: this.getSize(lmi),
|
||||||
|
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
Reference in New Issue
Block a user