Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92f069d1e6 | ||
|
|
62c5042ecc | ||
|
|
604b086c0b | ||
|
|
0e0b356f6b | ||
|
|
573768e2b2 | ||
|
|
236fd09c94 | ||
|
|
cb6cb5f637 | ||
|
|
eb916a3ab0 | ||
|
|
61b8ca1510 | ||
|
|
5d2da97dc5 | ||
|
|
d626686614 | ||
|
|
60ee33cb72 | ||
|
|
8d2498e96d | ||
|
|
1794484bed | ||
|
|
f2b6331843 | ||
|
|
8d5f33245f | ||
|
|
1ee544b842 | ||
|
|
0fd9463c7c | ||
|
|
ad5edf3aee | ||
|
|
d5fafd8cab | ||
|
|
92256f2fed | ||
|
|
5fa8d4c989 | ||
|
|
6b75f79f00 | ||
|
|
b0c9f29d90 | ||
|
|
7721afc116 | ||
|
|
bc85e9bbfa | ||
|
|
e8b6602fe5 | ||
|
|
0c49bcfe3f | ||
|
|
aecb6a8dd2 | ||
|
|
718947522e | ||
|
|
2f8ca51447 | ||
|
|
4fa6dd2616 | ||
|
|
736e57fafd | ||
|
|
30d86279a5 | ||
|
|
8bbfcdeb82 | ||
|
|
d23cf62264 | ||
|
|
73d5b19d2b | ||
|
|
1959351125 | ||
|
|
0708133779 | ||
|
|
9a81fc3688 | ||
|
|
ac71d39265 | ||
|
|
4203654ec8 | ||
|
|
9701c767b2 | ||
|
|
0223df4f9e | ||
|
|
7549385404 | ||
|
|
3f2d0ed8b1 | ||
|
|
e4a5927e07 | ||
|
|
a3aac4da75 | ||
|
|
1e9453e501 | ||
|
|
bec3f5841e | ||
|
|
068762912f | ||
|
|
01e85d0e91 | ||
|
|
a67c19f30f | ||
|
|
394363c8cb | ||
|
|
7fd51ebcc1 | ||
|
|
ae4678cf24 | ||
|
|
68e565ebe2 | ||
|
|
d99f4406b7 | ||
|
|
2064cd8380 | ||
|
|
415ff65561 | ||
|
|
1fed00ca81 | ||
|
|
a63022a669 | ||
|
|
114dbd24bc |
@@ -29,8 +29,8 @@ android {
|
|||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 72
|
versionCode 75
|
||||||
versionName "0.9.43-beta"
|
versionName "0.9.46-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ dependencies {
|
|||||||
implementation project(':capacitor-network')
|
implementation project(':capacitor-network')
|
||||||
implementation project(':capacitor-status-bar')
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capacitor-storage')
|
implementation project(':capacitor-storage')
|
||||||
implementation project(':robingenz-capacitor-app-update')
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:dist="http://schemas.android.com/apk/distribution"
|
xmlns:dist="http://schemas.android.com/apk/distribution"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:installLocation="preferExternal"
|
||||||
package="com.audiobookshelf.app">
|
package="com.audiobookshelf.app">
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|||||||
@@ -22,9 +22,5 @@
|
|||||||
{
|
{
|
||||||
"pkg": "@capacitor/storage",
|
"pkg": "@capacitor/storage",
|
||||||
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
||||||
},
|
|
||||||
{
|
|
||||||
"pkg": "@robingenz/capacitor-app-update",
|
|
||||||
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ data class LibraryItem(
|
|||||||
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun checkHasTracks():Boolean {
|
||||||
|
return if (mediaType == "podcast") {
|
||||||
|
((media as Podcast).numEpisodes ?: 0) > 0
|
||||||
|
} else {
|
||||||
|
((media as Book).numTracks ?: 0) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
fun getMediaMetadata(): MediaMetadataCompat {
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
@@ -74,6 +83,7 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
|
|||||||
open fun removeAudioTrack(localFileId:String) { }
|
open fun removeAudioTrack(localFileId:String) { }
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -82,7 +92,8 @@ class Podcast(
|
|||||||
coverPath:String?,
|
coverPath:String?,
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var episodes:MutableList<PodcastEpisode>?,
|
var episodes:MutableList<PodcastEpisode>?,
|
||||||
var autoDownloadEpisodes:Boolean
|
var autoDownloadEpisodes:Boolean,
|
||||||
|
var numEpisodes:Int?
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
@@ -99,7 +110,7 @@ class Podcast(
|
|||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
||||||
val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +158,7 @@ class Podcast(
|
|||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Podcast {
|
override fun getLocalCopy(): Podcast {
|
||||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes)
|
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +171,8 @@ class Book(
|
|||||||
var chapters:List<BookChapter>?,
|
var chapters:List<BookChapter>?,
|
||||||
var tracks:MutableList<AudioTrack>?,
|
var tracks:MutableList<AudioTrack>?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?
|
var duration:Double?,
|
||||||
|
var numTracks:Int?
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
@@ -209,7 +221,7 @@ class Book(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
override fun getLocalCopy(): Book {
|
||||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null)
|
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +293,29 @@ data class PodcastEpisode(
|
|||||||
var duration:Double?,
|
var duration:Double?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
||||||
)
|
) {
|
||||||
|
@JsonIgnore
|
||||||
|
fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat {
|
||||||
|
var coverUri:Uri = Uri.EMPTY
|
||||||
|
val podcast = if(libraryItem is LocalLibraryItem) {
|
||||||
|
coverUri = libraryItem.getCoverUri()
|
||||||
|
libraryItem.media as Podcast
|
||||||
|
} else {
|
||||||
|
coverUri = (libraryItem as LibraryItem).getCoverUri()
|
||||||
|
(libraryItem as LibraryItem).media as Podcast
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName())
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
||||||
|
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LibraryFile(
|
data class LibraryFile(
|
||||||
@@ -312,7 +346,16 @@ data class Library(
|
|||||||
var folders:MutableList<Folder>,
|
var folders:MutableList<Folder>,
|
||||||
var icon:String,
|
var icon:String,
|
||||||
var mediaType:String
|
var mediaType:String
|
||||||
)
|
) {
|
||||||
|
@JsonIgnore
|
||||||
|
fun getMediaMetadata(): MediaMetadataCompat {
|
||||||
|
return MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Folder(
|
data class Folder(
|
||||||
@@ -371,3 +414,9 @@ data class MediaProgress(
|
|||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?
|
var finishedAt:Long?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper class
|
||||||
|
data class LibraryItemWithEpisode(
|
||||||
|
var libraryItemWrapper:LibraryItemWrapper,
|
||||||
|
var episode:PodcastEpisode
|
||||||
|
)
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ class DbManager {
|
|||||||
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String):LibraryItemWithEpisode? {
|
||||||
|
var podcastEpisode:PodcastEpisode? = null
|
||||||
|
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem ->
|
||||||
|
val podcast = localLibraryItem.media as Podcast
|
||||||
|
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
|
||||||
|
podcastEpisode != null
|
||||||
|
}
|
||||||
|
return if (localLibraryItem != null) {
|
||||||
|
LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeLocalLibraryItem(localLibraryItemId:String) {
|
fun removeLocalLibraryItem(localLibraryItemId:String) {
|
||||||
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ data class LocalFile(
|
|||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun isAudioFile():Boolean {
|
fun isAudioFile():Boolean {
|
||||||
|
if (mimeType == "application/octet-stream") return true
|
||||||
|
if (mimeType == "video/mp4") return true
|
||||||
return mimeType?.startsWith("audio") == true
|
return mimeType?.startsWith("audio") == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.player.NOTIFICATION_LARGE_ICON_SIZE
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -44,7 +52,7 @@ data class LocalLibraryItem(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getDuration():Double {
|
fun getDuration():Double {
|
||||||
var total = 0.0
|
var total = 0.0
|
||||||
var audioTracks = media.getAudioTracks()
|
val audioTracks = media.getAudioTracks()
|
||||||
audioTracks.forEach{ total += it.duration }
|
audioTracks.forEach{ total += it.duration }
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
@@ -94,15 +102,17 @@ data class LocalLibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
fun getMediaMetadata(ctx: Context): MediaMetadataCompat {
|
||||||
|
val coverUri = getCoverUri()
|
||||||
|
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
|
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ data class LocalMediaItem(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getLocalLibraryItem():LocalLibraryItem {
|
fun getLocalLibraryItem():LocalLibraryItem {
|
||||||
var mediaMetadata = getMediaMetadata()
|
val mediaMetadata = getMediaMetadata()
|
||||||
if (mediaType == "book") {
|
if (mediaType == "book") {
|
||||||
var chapters = getAudiobookChapters()
|
val chapters = getAudiobookChapters()
|
||||||
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size)
|
||||||
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
||||||
} else {
|
} else {
|
||||||
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
|
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
|
||||||
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
|
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
|
||||||
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ package com.audiobookshelf.app.data
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.google.android.exoplayer2.C
|
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.MediaMetadata
|
import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.gms.cast.MediaInfo
|
import com.google.android.gms.cast.MediaInfo
|
||||||
@@ -68,7 +66,7 @@ class PlaybackSession(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackIndex():Int {
|
fun getCurrentTrackIndex():Int {
|
||||||
for (i in 0..(audioTracks.size - 1)) {
|
for (i in 0..(audioTracks.size - 1)) {
|
||||||
var track = audioTracks[i]
|
val track = audioTracks[i]
|
||||||
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
|
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -78,14 +76,14 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackTimeMs():Long {
|
fun getCurrentTrackTimeMs():Long {
|
||||||
var currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
||||||
var time = currentTime - currentTrack.startOffset
|
val time = currentTime - currentTrack.startOffset
|
||||||
return (time * 1000L).toLong()
|
return (time * 1000L).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getTrackStartOffsetMs(index:Int):Long {
|
fun getTrackStartOffsetMs(index:Int):Long {
|
||||||
var currentTrack = audioTracks[index]
|
val currentTrack = audioTracks[index]
|
||||||
return (currentTrack.startOffset * 1000L).toLong()
|
return (currentTrack.startOffset * 1000L).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +110,7 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
val metadataBuilder = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
||||||
@@ -125,14 +123,14 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
|
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
|
||||||
var metadataBuilder = MediaMetadata.Builder()
|
val metadataBuilder = MediaMetadata.Builder()
|
||||||
.setTitle(displayTitle)
|
.setTitle(displayTitle)
|
||||||
.setDisplayTitle(displayTitle)
|
.setDisplayTitle(displayTitle)
|
||||||
.setArtist(displayAuthor)
|
.setArtist(displayAuthor)
|
||||||
.setAlbumArtist(displayAuthor)
|
.setAlbumArtist(displayAuthor)
|
||||||
.setSubtitle(displayAuthor)
|
.setSubtitle(displayAuthor)
|
||||||
|
|
||||||
var contentUri = this.getContentUri(audioTrack)
|
val contentUri = this.getContentUri(audioTrack)
|
||||||
metadataBuilder.setMediaUri(contentUri)
|
metadataBuilder.setMediaUri(contentUri)
|
||||||
|
|
||||||
return metadataBuilder.build()
|
return metadataBuilder.build()
|
||||||
@@ -140,15 +138,15 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaItems():List<MediaItem> {
|
fun getMediaItems():List<MediaItem> {
|
||||||
var mediaItems:MutableList<MediaItem> = mutableListOf()
|
val mediaItems:MutableList<MediaItem> = mutableListOf()
|
||||||
|
|
||||||
for (audioTrack in audioTracks) {
|
for (audioTrack in audioTracks) {
|
||||||
var mediaMetadata = this.getExoMediaMetadata(audioTrack)
|
val mediaMetadata = this.getExoMediaMetadata(audioTrack)
|
||||||
var mediaUri = this.getContentUri(audioTrack)
|
val mediaUri = this.getContentUri(audioTrack)
|
||||||
var mimeType = audioTrack.mimeType
|
val mimeType = audioTrack.mimeType
|
||||||
|
|
||||||
var queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
|
val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
|
||||||
var mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
|
val mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
|
||||||
mediaItems.add(mediaItem)
|
mediaItems.add(mediaItem)
|
||||||
}
|
}
|
||||||
return mediaItems
|
return mediaItems
|
||||||
@@ -156,7 +154,7 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
|
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
|
||||||
var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
|
val castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
|
||||||
|
|
||||||
coverPath?.let {
|
coverPath?.let {
|
||||||
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
|
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
|
||||||
@@ -171,11 +169,11 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
|
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
|
||||||
var castMetadata = getCastMediaMetadata(audioTrack)
|
val castMetadata = getCastMediaMetadata(audioTrack)
|
||||||
|
|
||||||
var mediaUri = getContentUri(audioTrack)
|
val mediaUri = getContentUri(audioTrack)
|
||||||
|
|
||||||
var mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
|
val mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
|
||||||
setContentUrl(mediaUri.toString())
|
setContentUrl(mediaUri.toString())
|
||||||
setContentType(audioTrack.mimeType)
|
setContentType(audioTrack.mimeType)
|
||||||
setMetadata(castMetadata)
|
setMetadata(castMetadata)
|
||||||
|
|||||||
@@ -248,9 +248,10 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
||||||
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
||||||
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||||
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
||||||
|
|
||||||
var localEpisodeId:String? = null
|
var localEpisodeId:String? = null
|
||||||
@@ -288,7 +289,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
// Create new audio track
|
// Create new audio track
|
||||||
val track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
|
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
|
||||||
audioTracks.add(track)
|
audioTracks.add(track)
|
||||||
|
|
||||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
||||||
@@ -296,7 +297,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
// Add podcast episodes to library
|
// Add podcast episodes to library
|
||||||
itemPart.episode?.let { podcastEpisode ->
|
itemPart.episode?.let { podcastEpisode ->
|
||||||
val podcast = localLibraryItem.media as Podcast
|
val podcast = localLibraryItem.media as Podcast
|
||||||
var newEpisode = podcast.addEpisode(track, podcastEpisode)
|
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
||||||
localEpisodeId = newEpisode.id
|
localEpisodeId = newEpisode.id
|
||||||
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
||||||
}
|
}
|
||||||
@@ -366,7 +367,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
||||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
|
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
|
||||||
|
|
||||||
if (df == null) {
|
if (df == null) {
|
||||||
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
|
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
|
||||||
@@ -377,7 +378,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var wasUpdated = false
|
var wasUpdated = false
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
||||||
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
||||||
|
|
||||||
filesFound.forEach {
|
filesFound.forEach {
|
||||||
@@ -388,10 +389,10 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingAudioTracks = localLibraryItem.media.getAudioTracks()
|
val existingAudioTracks = localLibraryItem.media.getAudioTracks()
|
||||||
|
|
||||||
// Remove any files no longer found in library item folder
|
// Remove any files no longer found in library item folder
|
||||||
var existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
|
val existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
|
||||||
existingLocalFileIds.forEach { localFileId ->
|
existingLocalFileIds.forEach { localFileId ->
|
||||||
Log.d(tag, "Checking local file id is there $localFileId")
|
Log.d(tag, "Checking local file id is there $localFileId")
|
||||||
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
|
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
|
||||||
@@ -407,12 +408,12 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filesFound.forEach { docFile ->
|
filesFound.forEach { docFile ->
|
||||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
|
val existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
|
||||||
|
|
||||||
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
|
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
|
||||||
|
|
||||||
var localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
val localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
||||||
if (existingLocalFile == null) {
|
if (existingLocalFile == null) {
|
||||||
localLibraryItem.localFiles.add(localFile)
|
localLibraryItem.localFiles.add(localFile)
|
||||||
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
|
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
|
||||||
@@ -420,22 +421,26 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
if (localFile.isAudioFile()) {
|
if (localFile.isAudioFile()) {
|
||||||
// TODO: Make asynchronous
|
// TODO: Make asynchronous
|
||||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
var existingTrack = existingAudioTracks.find { audioTrack ->
|
val existingTrack = existingAudioTracks.find { audioTrack ->
|
||||||
audioTrack.localFileId == localFile.id
|
audioTrack.localFileId == localFile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingTrack == null) {
|
if (existingTrack == null) {
|
||||||
// Create new audio track
|
// Create new audio track
|
||||||
var lastTrack = existingAudioTracks.lastOrNull()
|
val lastTrack = existingAudioTracks.lastOrNull()
|
||||||
var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
|
val startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
|
||||||
var track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
|
val track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
|
||||||
localLibraryItem.media.addAudioTrack(track)
|
localLibraryItem.media.addAudioTrack(track)
|
||||||
|
Log.d(tag, "Added New Audio Track ${track.title}")
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
} else {
|
} else {
|
||||||
existingTrack.audioProbeResult = audioProbeResult
|
existingTrack.audioProbeResult = audioProbeResult
|
||||||
// TODO: Update data found from probe
|
// TODO: Update data found from probe
|
||||||
|
|
||||||
|
Log.d(tag, "Updated Audio Track Probe Data ${existingTrack.title}")
|
||||||
|
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
}
|
}
|
||||||
} else { // Check if cover is empty
|
} else { // Check if cover is empty
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.audiobookshelf.app.media
|
|||||||
|
|
||||||
import android.bluetooth.BluetoothClass
|
import android.bluetooth.BluetoothClass
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -14,6 +16,12 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
var serverLibraryItems = listOf<LibraryItem>()
|
var serverLibraryItems = listOf<LibraryItem>()
|
||||||
|
var selectedLibraryId = ""
|
||||||
|
|
||||||
|
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
||||||
|
var selectedPodcast:Podcast? = null
|
||||||
|
var selectedLibraryItemId:String? = null
|
||||||
|
var serverPodcastEpisodes = listOf<PodcastEpisode>()
|
||||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
var serverLibraryCategories = listOf<LibraryCategory>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
|
|
||||||
@@ -22,6 +30,10 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
Paper.init(ctx)
|
Paper.init(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
|
return serverLibraries.find { it.id == id } != null
|
||||||
|
}
|
||||||
|
|
||||||
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
||||||
if (serverLibraryCategories.isNotEmpty()) {
|
if (serverLibraryCategories.isNotEmpty()) {
|
||||||
cb(serverLibraryCategories)
|
cb(serverLibraryCategories)
|
||||||
@@ -33,17 +45,75 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||||
if (serverLibraryItems.isNotEmpty()) {
|
if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
||||||
cb(serverLibraryItems)
|
cb(serverLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||||
serverLibraryItems = libraryItems
|
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||||
cb(libraryItems)
|
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
|
||||||
|
|
||||||
|
serverLibraryItems = libraryItemsWithAudio
|
||||||
|
cb(libraryItemsWithAudio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
||||||
|
if (libraryItemId.startsWith("local")) {
|
||||||
|
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "loadLibraryItem: $libraryItemId")
|
||||||
|
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
|
||||||
|
Log.d(tag, "loadLibraryItem: Got library item $libraryItem")
|
||||||
|
cb(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
|
||||||
|
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
|
||||||
|
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
|
||||||
|
|
||||||
|
selectedLibraryItemWrapper = libraryItemWrapper
|
||||||
|
|
||||||
|
libraryItemWrapper?.let {
|
||||||
|
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
|
||||||
|
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
||||||
|
serverPodcastEpisodes = listOf()
|
||||||
|
cb(mutableListOf())
|
||||||
|
} else {
|
||||||
|
val podcast = libraryItemWrapper.media as Podcast
|
||||||
|
serverPodcastEpisodes = podcast.episodes ?: listOf()
|
||||||
|
selectedLibraryItemId = libraryItemWrapper.id
|
||||||
|
selectedPodcast = podcast
|
||||||
|
|
||||||
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
|
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
|
||||||
|
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
|
}
|
||||||
|
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
|
||||||
|
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
||||||
|
serverPodcastEpisodes = listOf()
|
||||||
|
cb(mutableListOf())
|
||||||
|
} else {
|
||||||
|
val podcast = libraryItemWrapper.media as Podcast
|
||||||
|
serverPodcastEpisodes = podcast.episodes ?: listOf()
|
||||||
|
selectedLibraryItemId = libraryItemWrapper.id
|
||||||
|
selectedPodcast = podcast
|
||||||
|
|
||||||
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
|
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadLibraries(cb: (List<Library>) -> Unit) {
|
fun loadLibraries(cb: (List<Library>) -> Unit) {
|
||||||
if (serverLibraries.isNotEmpty()) {
|
if (serverLibraries.isNotEmpty()) {
|
||||||
cb(serverLibraries)
|
cb(serverLibraries)
|
||||||
@@ -57,9 +127,9 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
|
|
||||||
// TODO: Load currently listening category for local items
|
// TODO: Load currently listening category for local items
|
||||||
fun loadLocalCategory():List<LibraryCategory> {
|
fun loadLocalCategory():List<LibraryCategory> {
|
||||||
var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
var localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||||
var cats = mutableListOf<LibraryCategory>()
|
val cats = mutableListOf<LibraryCategory>()
|
||||||
if (localBooks.isNotEmpty()) {
|
if (localBooks.isNotEmpty()) {
|
||||||
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
|
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
|
||||||
}
|
}
|
||||||
@@ -69,11 +139,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return cats
|
return cats
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAndroidAutoItems(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
|
||||||
Log.d(tag, "Load android auto items for library id $libraryId")
|
Log.d(tag, "Load android auto items")
|
||||||
var cats = mutableListOf<LibraryCategory>()
|
val cats = mutableListOf<LibraryCategory>()
|
||||||
|
|
||||||
var localCategories = loadLocalCategory()
|
val localCategories = loadLocalCategory()
|
||||||
cats.addAll(localCategories)
|
cats.addAll(localCategories)
|
||||||
|
|
||||||
// Connected to server and has internet - load other cats
|
// Connected to server and has internet - load other cats
|
||||||
@@ -84,26 +154,21 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadLibraries { libraries ->
|
loadLibraries { libraries ->
|
||||||
var library = libraries.find { it.id == libraryId } ?: libraries[0]
|
val library = libraries[0]
|
||||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||||
|
|
||||||
loadLibraryCategories(libraryId) { libraryCategories ->
|
loadLibraryCategories(library.id) { libraryCategories ->
|
||||||
|
|
||||||
// Only using book or podcast library categories for now
|
// Only using book or podcast library categories for now
|
||||||
libraryCategories.forEach {
|
libraryCategories.forEach {
|
||||||
Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
||||||
if (it.type == library.mediaType) {
|
if (it.type == library.mediaType) {
|
||||||
Log.d(tag, "Using library category ${it.id}")
|
// Log.d(tag, "Using library category ${it.id}")
|
||||||
cats.add(it)
|
cats.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLibraryItems(libraryId) { libraryItems ->
|
cb(cats)
|
||||||
var mainCat = LibraryCategory("library", "Library", library.mediaType, libraryItems, false)
|
|
||||||
cats.add(mainCat)
|
|
||||||
|
|
||||||
cb(cats)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Not connected/no internet sent downloaded cats only
|
} else { // Not connected/no internet sent downloaded cats only
|
||||||
@@ -115,11 +180,24 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
if (serverLibraryItems.isNotEmpty()) {
|
if (serverLibraryItems.isNotEmpty()) {
|
||||||
return serverLibraryItems[0]
|
return serverLibraryItems[0]
|
||||||
} else {
|
} else {
|
||||||
var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
return if (localBooks.isNotEmpty()) return localBooks[0] else null
|
return if (localBooks.isNotEmpty()) return localBooks[0] else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPodcastWithEpisodeByEpisodeId(id:String) : LibraryItemWithEpisode? {
|
||||||
|
if (id.startsWith("local")) {
|
||||||
|
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
|
||||||
|
} else {
|
||||||
|
val podcastEpisode = serverPodcastEpisodes.find { it.id == id }
|
||||||
|
return if (podcastEpisode != null && selectedLibraryItemWrapper != null) {
|
||||||
|
LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getById(id:String) : LibraryItemWrapper? {
|
fun getById(id:String) : LibraryItemWrapper? {
|
||||||
if (id.startsWith("local")) {
|
if (id.startsWith("local")) {
|
||||||
return DeviceManager.dbManager.getLocalLibraryItem(id)
|
return DeviceManager.dbManager.getLocalLibraryItem(id)
|
||||||
@@ -135,13 +213,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(libraryItemWrapper:LibraryItemWrapper, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||||
if (libraryItemWrapper is LocalLibraryItem) {
|
if (libraryItemWrapper is LocalLibraryItem) {
|
||||||
var localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
||||||
cb(localLibraryItem.getPlaybackSession(null))
|
cb(localLibraryItem.getPlaybackSession(episode))
|
||||||
} else {
|
} else {
|
||||||
var libraryItem = libraryItemWrapper as LibraryItem
|
val libraryItem = libraryItemWrapper as LibraryItem
|
||||||
apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
|
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",false, mediaPlayer) {
|
||||||
cb(it)
|
cb(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,8 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
|
|
||||||
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
|
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
// Block on downloading artwork.
|
|
||||||
try {
|
try {
|
||||||
Glide.with(playerNotificationService).applyDefaultRequestOptions(glideOptions)
|
Glide.with(playerNotificationService)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.placeholder(R.drawable.icon)
|
.placeholder(R.drawable.icon)
|
||||||
@@ -71,7 +70,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
Glide.with(playerNotificationService).applyDefaultRequestOptions(glideOptions)
|
Glide.with(playerNotificationService)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
|
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
|
||||||
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import android.support.v4.media.MediaMetadataCompat
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
|
import com.audiobookshelf.app.data.Library
|
||||||
import com.audiobookshelf.app.data.LibraryCategory
|
import com.audiobookshelf.app.data.LibraryCategory
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.audiobookshelf.app.data.LocalLibraryItem
|
import com.audiobookshelf.app.data.LocalLibraryItem
|
||||||
|
|
||||||
|
|
||||||
class BrowseTree(
|
class BrowseTree(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
libraryCategories: List<LibraryCategory>
|
libraryCategories: List<LibraryCategory>,
|
||||||
|
libraries: List<Library>
|
||||||
) {
|
) {
|
||||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
|
|
||||||
@@ -41,22 +42,22 @@ class BrowseTree(
|
|||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val allMetadata = MediaMetadataCompat.Builder().apply {
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items")
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_books).toString())
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
val librariesMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
// Server continue Listening cat
|
// Server continue Listening cat
|
||||||
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
||||||
var continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
||||||
var libraryItem = liw as LibraryItem
|
val libraryItem = liw as LibraryItem
|
||||||
libraryItem.getMediaMetadata()
|
libraryItem.getMediaMetadata()
|
||||||
}
|
}
|
||||||
if (continueListeningMediaMetadata.isNotEmpty()) {
|
if (continueListeningMediaMetadata.isNotEmpty()) {
|
||||||
@@ -69,30 +70,32 @@ class BrowseTree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootList += allMetadata
|
if (libraries.isNotEmpty()) {
|
||||||
rootList += downloadsMetadata
|
rootList += librariesMetadata
|
||||||
|
|
||||||
// Server library cat
|
libraries.forEach { library ->
|
||||||
libraryCategories.find { it.id == "library" }?.let { libraryCategory ->
|
val libraryMediaMetadata = library.getMediaMetadata()
|
||||||
var libraryMediaMetadata = libraryCategory.entities.map { libc ->
|
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||||
var libraryItem = libc as LibraryItem
|
children += libraryMediaMetadata
|
||||||
libraryItem.getMediaMetadata()
|
mediaIdToChildren[LIBRARIES_ROOT] = children
|
||||||
}
|
|
||||||
libraryMediaMetadata.forEach {
|
|
||||||
val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
|
||||||
children += it
|
|
||||||
mediaIdToChildren[ALL_ROOT] = children
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootList += downloadsMetadata
|
||||||
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
||||||
var localMediaMetadata = localBooksCat.entities.map { libc ->
|
localBooksCat.entities.forEach { libc ->
|
||||||
var libraryItem = libc as LocalLibraryItem
|
val libraryItem = libc as LocalLibraryItem
|
||||||
libraryItem.getMediaMetadata()
|
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
||||||
|
children += libraryItem.getMediaMetadata(context)
|
||||||
|
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
||||||
}
|
}
|
||||||
localMediaMetadata.forEach {
|
}
|
||||||
|
|
||||||
|
libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat ->
|
||||||
|
localPodcastsCat.entities.forEach { libc ->
|
||||||
|
val libraryItem = libc as LocalLibraryItem
|
||||||
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
||||||
children += it
|
children += libraryItem.getMediaMetadata(context)
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +107,6 @@ class BrowseTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val AUTO_BROWSE_ROOT = "/"
|
const val AUTO_BROWSE_ROOT = "/"
|
||||||
const val ALL_ROOT = "__ALL__"
|
|
||||||
const val CONTINUE_ROOT = "__CONTINUE__"
|
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
|
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ data class MediaProgressSyncData(
|
|||||||
var currentTime:Double // seconds
|
var currentTime:Double // seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) {
|
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
||||||
private val tag = "MediaProgressSync"
|
private val tag = "MediaProgressSync"
|
||||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
|
||||||
private val apiHandler = apiHandler
|
|
||||||
|
|
||||||
private var listeningTimerTask: TimerTask? = null
|
private var listeningTimerTask: TimerTask? = null
|
||||||
var listeningTimerRunning:Boolean = false
|
var listeningTimerRunning:Boolean = false
|
||||||
@@ -54,7 +52,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||||
var currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
sync(currentTime)
|
sync(currentTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,20 +63,20 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
var currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
sync(currentTime)
|
sync(currentTime)
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sync(currentTime:Double) {
|
fun sync(currentTime:Double) {
|
||||||
var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||||
if (diffSinceLastSync < 1000L) {
|
if (diffSinceLastSync < 1000L) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var listeningTimeToAdd = diffSinceLastSync / 1000L
|
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
||||||
|
|
||||||
currentPlaybackSession?.syncData(syncData)
|
currentPlaybackSession?.syncData(syncData)
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
@@ -87,10 +85,16 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
|
|
||||||
// Send sync to server also if connected to this server and local item belongs to this server
|
// Local library item is linked to a server library item
|
||||||
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
if (!it.libraryItemId.isNullOrEmpty()) {
|
||||||
apiHandler.sendLocalProgressSync(it) {
|
// Send sync to server also if connected to this server and local item belongs to this server
|
||||||
Log.d(tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
|
apiHandler.sendLocalProgressSync(it) {
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +107,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
|
|
||||||
private fun saveLocalProgress(playbackSession:PlaybackSession) {
|
private fun saveLocalProgress(playbackSession:PlaybackSession) {
|
||||||
if (currentLocalMediaProgress == null) {
|
if (currentLocalMediaProgress == null) {
|
||||||
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
|
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
|
||||||
if (mediaProgress == null) {
|
if (mediaProgress == null) {
|
||||||
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
|
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.util.Log
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -27,7 +28,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
@@ -49,7 +50,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
@@ -90,14 +91,20 @@ 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")
|
||||||
var libraryItemWrapper: LibraryItemWrapper? = null
|
var libraryItemWrapper: LibraryItemWrapper? = null
|
||||||
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
if (mediaId.isNullOrEmpty()) {
|
if (mediaId.isNullOrEmpty()) {
|
||||||
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
|
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
|
||||||
|
} else if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
|
||||||
|
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
|
||||||
|
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
|
||||||
|
podcastEpisode = libraryItemWithEpisode?.episode
|
||||||
} else {
|
} else {
|
||||||
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItemWrapper?.let { li ->
|
libraryItemWrapper?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import android.os.Looper
|
|||||||
import android.os.ResultReceiver
|
import android.os.ResultReceiver
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
override fun onPrepare(playWhenReady: Boolean) {
|
override fun onPrepare(playWhenReady: Boolean) {
|
||||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,19 @@ 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")
|
||||||
|
|
||||||
var libraryItemWrapper: LibraryItemWrapper? = playerNotificationService.mediaManager.getById(mediaId)
|
var libraryItemWrapper: LibraryItemWrapper? = null
|
||||||
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
|
if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
|
||||||
|
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
|
||||||
|
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
|
||||||
|
podcastEpisode = libraryItemWithEpisode?.episode
|
||||||
|
} else {
|
||||||
|
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
libraryItemWrapper?.let { li ->
|
libraryItemWrapper?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
@@ -55,7 +65,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
private var onSeekBack: Boolean = false
|
private var onSeekBack: Boolean = false
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
var errorMessage = error.message ?: "Unknown Error"
|
val errorMessage = error.message ?: "Unknown Error"
|
||||||
Log.e(tag, "onPlayerError $errorMessage")
|
Log.e(tag, "onPlayerError $errorMessage")
|
||||||
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
||||||
}
|
}
|
||||||
@@ -90,6 +90,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
// Start/stop progress sync interval
|
// Start/stop progress sync interval
|
||||||
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
|
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
|
||||||
if (player.isPlaying) {
|
if (player.isPlaying) {
|
||||||
|
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
playerNotificationService.mediaProgressSyncer.start()
|
playerNotificationService.mediaProgressSyncer.start()
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.mediaProgressSyncer.stop()
|
playerNotificationService.mediaProgressSyncer.stop()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||||
|
|
||||||
lateinit var mediaManager: MediaManager
|
lateinit var mediaManager: MediaManager
|
||||||
lateinit var apiHandler: ApiHandler
|
private lateinit var apiHandler: ApiHandler
|
||||||
|
|
||||||
lateinit var mPlayer: ExoPlayer
|
lateinit var mPlayer: ExoPlayer
|
||||||
lateinit var currentPlayer:Player
|
lateinit var currentPlayer:Player
|
||||||
@@ -75,7 +75,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
lateinit var sleepTimerManager:SleepTimerManager
|
lateinit var sleepTimerManager:SleepTimerManager
|
||||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
||||||
|
|
||||||
private var notificationId = 10;
|
private var notificationId = 10
|
||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
private var channelName = "Audiobookshelf Channel"
|
private var channelName = "Audiobookshelf Channel"
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// Android Auto Media Browser Service
|
// Android Auto Media Browser Service
|
||||||
if (SERVICE_INTERFACE == intent.action) {
|
if (SERVICE_INTERFACE == intent.action) {
|
||||||
Log.d(tag, "Is Media Browser Service")
|
Log.d(tag, "Is Media Browser Service")
|
||||||
return super.onBind(intent);
|
return super.onBind(intent)
|
||||||
}
|
}
|
||||||
return binder
|
return binder
|
||||||
}
|
}
|
||||||
@@ -245,11 +245,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
|
val coverUri = currentPlaybackSession!!.getCoverUri()
|
||||||
|
|
||||||
|
// Fix for local images crashing on Android 11 for specific devices
|
||||||
|
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
|
||||||
|
ctx.grantUriPermission(
|
||||||
|
"com.android.systemui",
|
||||||
|
coverUri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
return MediaDescriptionCompat.Builder()
|
return MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(currentPlaybackSession!!.id)
|
.setMediaId(currentPlaybackSession!!.id)
|
||||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||||
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
||||||
.setIconUri(currentPlaybackSession!!.getCoverUri()).build()
|
.setIconUri(coverUri).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +356,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
||||||
currentPlayer.playWhenReady = playWhenReady
|
currentPlayer.playWhenReady = playWhenReady
|
||||||
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
||||||
|
|
||||||
currentPlayer.prepare()
|
currentPlayer.prepare()
|
||||||
|
|
||||||
} else if (castPlayer != null) {
|
} else if (castPlayer != null) {
|
||||||
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
||||||
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
||||||
@@ -367,7 +379,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
val episodeId = playbackSession.episodeId
|
val episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
preparePlayer(it, true, null)
|
preparePlayer(it, true, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,12 +522,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
fun seekForward(amount: Long) {
|
fun seekForward(amount: Long) {
|
||||||
seekPlayer(getCurrentTime() + amount)
|
seekPlayer(getCurrentTime() + amount)
|
||||||
// currentPlayer.seekTo(currentPlayer.currentPosition + amount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekBackward(amount: Long) {
|
fun seekBackward(amount: Long) {
|
||||||
seekPlayer(getCurrentTime() - amount)
|
seekPlayer(getCurrentTime() - amount)
|
||||||
// currentPlayer.seekTo(currentPlayer.currentPosition - amount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPlaybackSpeed(speed: Float) {
|
fun setPlaybackSpeed(speed: Float) {
|
||||||
@@ -539,6 +549,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getContext():Context {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
@@ -551,6 +565,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
private val ALL_ROOT = "__ALL__"
|
private val ALL_ROOT = "__ALL__"
|
||||||
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
|
|
||||||
|
|
||||||
@@ -596,32 +611,66 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
||||||
|
|
||||||
val flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
|
||||||
result.detach()
|
result.detach()
|
||||||
|
|
||||||
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
|
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
||||||
browseTree = BrowseTree(this, libraryCategories)
|
Log.d(tag, "Loading podcast episodes")
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) {
|
||||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
result.sendResult(it)
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: For using sub menus. Check if this is the root menu:
|
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||||
// if (AUTO_MEDIA_ROOT == parentMediaId) {
|
val children = libraryItems.map { libraryItem ->
|
||||||
// build the MediaItem objects for the top level,
|
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
|
||||||
// and put them in the mediaItems list
|
|
||||||
// } else {
|
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||||
// examine the passed parentMediaId to see which submenu we're at,
|
flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
// and put the children of that menu in the mediaItems list
|
}
|
||||||
// }
|
|
||||||
|
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
} else if (parentMediaId == "__DOWNLOADS__") { // Load downloads
|
||||||
|
|
||||||
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
|
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||||
|
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
|
localBooks.forEach { localLibraryItem ->
|
||||||
|
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
|
||||||
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
localPodcasts.forEach { localLibraryItem ->
|
||||||
|
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
|
||||||
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sendResult(localBrowseItems)
|
||||||
|
|
||||||
|
} else { // Load categories
|
||||||
|
|
||||||
|
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
||||||
|
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
||||||
|
|
||||||
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
|
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag")
|
||||||
|
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
result.detach()
|
result.detach()
|
||||||
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
|
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
||||||
browseTree = BrowseTree(this, libraryCategories)
|
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
||||||
val children = browseTree[ALL_ROOT]?.map { item ->
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.os.Handler
|
import android.content.Context
|
||||||
import android.os.Looper
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
@@ -9,9 +9,8 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
const val SLEEP_EXTENSION_TIME = 900000L // 15m
|
const val SLEEP_EXTENSION_TIME = 900000L // 15m
|
||||||
|
|
||||||
class SleepTimerManager constructor(playerNotificationService:PlayerNotificationService) {
|
class SleepTimerManager constructor(val playerNotificationService:PlayerNotificationService) {
|
||||||
private val tag = "SleepTimerManager"
|
private val tag = "SleepTimerManager"
|
||||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
|
||||||
|
|
||||||
private var sleepTimerTask:TimerTask? = null
|
private var sleepTimerTask:TimerTask? = null
|
||||||
private var sleepTimerRunning:Boolean = false
|
private var sleepTimerRunning:Boolean = false
|
||||||
@@ -64,7 +63,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
// Register shake sensor
|
// Register shake sensor
|
||||||
playerNotificationService.registerSensor()
|
playerNotificationService.registerSensor()
|
||||||
|
|
||||||
var currentTime = getCurrentTime()
|
val currentTime = getCurrentTime()
|
||||||
if (isChapterTime) {
|
if (isChapterTime) {
|
||||||
if (currentTime > time) {
|
if (currentTime > time) {
|
||||||
Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
|
Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
|
||||||
@@ -95,7 +94,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
if (getIsPlaying()) {
|
if (getIsPlaying()) {
|
||||||
sleepTimerElapsed += 1000L
|
sleepTimerElapsed += 1000L
|
||||||
|
|
||||||
var sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds()
|
val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds()
|
||||||
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining > 0) {
|
if (sleepTimeSecondsRemaining > 0) {
|
||||||
@@ -111,7 +110,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||||
} else if (sleepTimeSecondsRemaining <= 30) {
|
} else if (sleepTimeSecondsRemaining <= 30) {
|
||||||
// Start fading out audio
|
// Start fading out audio
|
||||||
var volume = sleepTimeSecondsRemaining / 30F
|
val volume = sleepTimeSecondsRemaining / 30F
|
||||||
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
||||||
setVolume(volume)
|
setVolume(volume)
|
||||||
}
|
}
|
||||||
@@ -129,7 +128,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
playerNotificationService.unregisterSensor()
|
playerNotificationService.unregisterSensor()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSleepTimerTime():Long? {
|
fun getSleepTimerTime():Long {
|
||||||
return sleepTimerEndTime
|
return sleepTimerEndTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +138,30 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vibrate when extending sleep timer by shaking
|
||||||
|
private fun vibrate() {
|
||||||
|
val context = playerNotificationService.getContext()
|
||||||
|
val vibrator:Vibrator
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val vibratorManager =
|
||||||
|
context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
|
vibrator = vibratorManager.defaultVibrator
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
}
|
||||||
|
|
||||||
|
vibrator.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150),-1)
|
||||||
|
it.vibrate(vibrationEffect)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
it.vibrate(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun extendSleepTime() {
|
private fun extendSleepTime() {
|
||||||
if (!sleepTimerRunning) return
|
if (!sleepTimerRunning) return
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
@@ -157,7 +180,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
if (!sleepTimerRunning) {
|
if (!sleepTimerRunning) {
|
||||||
if (sleepTimerFinishedAt <= 0L) return
|
if (sleepTimerFinishedAt <= 0L) return
|
||||||
|
|
||||||
var finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt
|
val finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt
|
||||||
if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes
|
if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes
|
||||||
{
|
{
|
||||||
Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it")
|
Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it")
|
||||||
@@ -165,14 +188,18 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME
|
val newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME
|
||||||
|
vibrate()
|
||||||
setSleepTimer(newSleepTime, false)
|
setSleepTimer(newSleepTime, false)
|
||||||
play()
|
play()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Only extend if within 30 seconds of finishing
|
// Only extend if within 30 seconds of finishing
|
||||||
var sleepTimeRemaining = getSleepTimerTimeRemainingSeconds()
|
val sleepTimeRemaining = getSleepTimerTimeRemainingSeconds()
|
||||||
if (sleepTimeRemaining <= 30) extendSleepTime()
|
if (sleepTimeRemaining <= 30) {
|
||||||
|
vibrate()
|
||||||
|
extendSleepTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleShake() {
|
fun handleShake() {
|
||||||
@@ -188,7 +215,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
sleepTimerLength += time
|
sleepTimerLength += time
|
||||||
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
|
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
|
||||||
} else {
|
} else {
|
||||||
var newSleepEndTime = sleepTimerEndTime + time
|
val newSleepEndTime = sleepTimerEndTime + time
|
||||||
sleepTimerEndTime = if (newSleepEndTime >= getDuration()) {
|
sleepTimerEndTime = if (newSleepEndTime >= getDuration()) {
|
||||||
getDuration()
|
getDuration()
|
||||||
} else {
|
} else {
|
||||||
@@ -209,7 +236,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
|
|||||||
sleepTimerLength -= time
|
sleepTimerLength -= time
|
||||||
if (sleepTimerLength <= 0) sleepTimerLength = 1000L
|
if (sleepTimerLength <= 0) sleepTimerLength = 1000L
|
||||||
} else {
|
} else {
|
||||||
var newSleepEndTime = sleepTimerEndTime - time
|
val newSleepEndTime = sleepTimerEndTime - time
|
||||||
sleepTimerEndTime = if (newSleepEndTime <= 1000) {
|
sleepTimerEndTime = if (newSleepEndTime <= 1000) {
|
||||||
// End sleep timer in 1 second
|
// End sleep timer in 1 second
|
||||||
getCurrentTime() + 1000
|
getCurrentTime() + 1000
|
||||||
|
|||||||
@@ -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,45 +206,93 @@ 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
|
||||||
Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring")
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
|
||||||
// Send update to server media progress is linked to a server and user is logged into that server
|
val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
|
||||||
localMediaProgress.serverConnectionConfigId?.let { configId ->
|
Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring")
|
||||||
if (DeviceManager.serverConnectionConfigId == configId) {
|
|
||||||
var libraryItemId = localMediaProgress.libraryItemId ?: ""
|
// Send update to server media progress is linked to a server and user is logged into that server
|
||||||
var episodeId = localMediaProgress.episodeId ?: ""
|
localMediaProgress.serverConnectionConfigId?.let { configId ->
|
||||||
var updatePayload = JSObject()
|
if (DeviceManager.serverConnectionConfigId == configId) {
|
||||||
updatePayload.put("isFinished", isFinished)
|
var libraryItemId = localMediaProgress.libraryItemId ?: ""
|
||||||
apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) {
|
var episodeId = localMediaProgress.episodeId ?: ""
|
||||||
Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server")
|
var updatePayload = JSObject()
|
||||||
var jsobj = JSObject()
|
updatePayload.put("isFinished", isFinished)
|
||||||
jsobj.put("local", true)
|
apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) {
|
||||||
jsobj.put("server", true)
|
Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server")
|
||||||
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
var jsobj = JSObject()
|
||||||
call.resolve(jsobj)
|
jsobj.put("local", true)
|
||||||
// call.resolve(JSObject("{\"local\":true,\"server\":true,\"localMediaProgress\":$lmpstring}"))
|
jsobj.put("server", true)
|
||||||
}
|
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
||||||
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
|
}
|
||||||
// call.resolve(JSObject("{\"local\":true,\"localMediaProgress\":$lmpstring}}"))
|
if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
|
||||||
var jsobj = JSObject()
|
var jsobj = JSObject()
|
||||||
jsobj.put("local", true)
|
jsobj.put("local", true)
|
||||||
jsobj.put("server", false)
|
jsobj.put("server", false)
|
||||||
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -262,7 +277,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
|
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
var dlRequest = downloadItemPart.getDownloadRequest()
|
var dlRequest = downloadItemPart.getDownloadRequest()
|
||||||
@@ -279,7 +294,7 @@ class AbsDownloader : Plugin() {
|
|||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
||||||
} else {
|
} else {
|
||||||
downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
|
downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
dlRequest = downloadItemPart.getDownloadRequest()
|
dlRequest = downloadItemPart.getDownloadRequest()
|
||||||
@@ -310,6 +325,13 @@ class AbsDownloader : Plugin() {
|
|||||||
delay(500)
|
delay(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove download notifications
|
||||||
|
downloadItem.downloadItemParts.forEach { downloadItemPart ->
|
||||||
|
downloadItemPart.downloadId?.let {
|
||||||
|
downloadManager.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
|
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
|
||||||
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
||||||
downloadQueue.remove(downloadItem)
|
downloadQueue.remove(downloadItem)
|
||||||
@@ -352,6 +374,7 @@ class AbsDownloader : Plugin() {
|
|||||||
if (!downloadItemPart.completed) {
|
if (!downloadItemPart.completed) {
|
||||||
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
|
||||||
downloadItemPart.completed = true
|
downloadItemPart.completed = true
|
||||||
|
|
||||||
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
||||||
Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}")
|
Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}")
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,6 @@
|
|||||||
android:translateY="-1.5294118">
|
android:translateY="-1.5294118">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 466 B |
@@ -19,6 +19,3 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
|
|||||||
|
|
||||||
include ':capacitor-storage'
|
include ':capacitor-storage'
|
||||||
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
||||||
|
|
||||||
include ':robingenz-capacitor-app-update'
|
|
||||||
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ body {
|
|||||||
min-height: calc(100% - 64px);
|
min-height: calc(100% - 64px);
|
||||||
max-height: calc(100% - 64px);
|
max-height: calc(100% - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#content.playerOpen {
|
#content.playerOpen {
|
||||||
height: calc(100% - 164px);
|
height: calc(100% - 164px);
|
||||||
min-height: calc(100% - 164px);
|
min-height: calc(100% - 164px);
|
||||||
@@ -46,6 +47,7 @@ body {
|
|||||||
.box-shadow-book {
|
.box-shadow-book {
|
||||||
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-height {
|
.shadow-height {
|
||||||
height: calc(100% - 4px);
|
height: calc(100% - 4px);
|
||||||
}
|
}
|
||||||
@@ -53,6 +55,7 @@ body {
|
|||||||
.bookshelfRow {
|
.bookshelfRow {
|
||||||
background-image: url(/wood_panels.jpg);
|
background-image: url(/wood_panels.jpg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookshelfDivider {
|
.bookshelfDivider {
|
||||||
background: rgb(149, 119, 90);
|
background: rgb(149, 119, 90);
|
||||||
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||||
@@ -63,9 +66,9 @@ body {
|
|||||||
Bookshelf Label
|
Bookshelf Label
|
||||||
*/
|
*/
|
||||||
.categoryPlacard {
|
.categoryPlacard {
|
||||||
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
|
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shinyBlack {
|
.shinyBlack {
|
||||||
background-color: #2d3436;
|
background-color: #2d3436;
|
||||||
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons Outlined';
|
font-family: 'Material Icons Outlined';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -12,43 +12,6 @@
|
|||||||
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .material-icons {
|
|
||||||
font-family: 'Material Icons';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
.material-icons.text-icon {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
}
|
|
||||||
.material-icons.text-lg {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
.material-icons.text-2xl {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
.material-icons.text-3xl {
|
|
||||||
font-size: 1.875rem;
|
|
||||||
}
|
|
||||||
.material-icons.text-4xl {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
}
|
|
||||||
.material-icons.text-5xl {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
.material-icons.text-base {
|
|
||||||
font-size: 1rem;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.material-icons {
|
.material-icons {
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -60,9 +23,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -78,9 +41,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -93,6 +56,7 @@
|
|||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Gentium Book Basic';
|
font-family: 'Gentium Book Basic';
|
||||||
@@ -101,4 +65,275 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
||||||
<span class="material-icons text-3xl text-white">arrow_back</span>
|
<span class="material-icons text-3xl text-white">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="user">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
||||||
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
||||||
@@ -51,14 +51,11 @@ export default {
|
|||||||
this.$store.commit('setCastAvailable', val)
|
this.$store.commit('setCastAvailable', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
currentLibrary() {
|
currentLibrary() {
|
||||||
return this.$store.getters['libraries/getCurrentLibrary']
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
},
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.currentLibrary ? this.currentLibrary.name : 'Main'
|
return this.currentLibrary ? this.currentLibrary.name : ''
|
||||||
},
|
},
|
||||||
currentLibraryIcon() {
|
currentLibraryIcon() {
|
||||||
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
||||||
@@ -80,11 +77,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
castClick() {
|
castClick() {
|
||||||
if (this.$store.state.playerIsLocal) {
|
if (this.$store.state.playerIsLocal) {
|
||||||
this.$toast.warn('Cannot cast downloaded media item')
|
this.$eventBus.$emit('cast-local-item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Cast Btn Click')
|
|
||||||
AbsAudioPlayer.requestSession()
|
AbsAudioPlayer.requestSession()
|
||||||
},
|
},
|
||||||
clickShowSideDrawer() {
|
clickShowSideDrawer() {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
|
<div v-if="playbackSession" id="streamContainer" class="playerContainer 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="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
||||||
<div class="top-2 left-4 absolute cursor-pointer">
|
<div class="top-2 left-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer">
|
<div v-show="showCastBtn" class="top-4 right-16 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-4 right-4 absolute cursor-pointer">
|
<div class="top-4 right-4 absolute cursor-pointer">
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<span class="material-icons text-3xl">more_vert</span>
|
<span class="material-icons text-3xl">more_vert</span>
|
||||||
</ui-dropdown-menu>
|
</ui-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="top-2 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
|
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
<div id="playerContent" class="playerContainer w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
||||||
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<span v-if="!isPodcast && !isLocalPlayMethod" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
<span v-if="!isPodcast && !isLocalPlayMethod" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
|
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4" :class="{ 'animate-spin': seekLoading, 'bg-accent': !isLocalPlayMethod, 'bg-success': isLocalPlayMethod }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
||||||
<widgets-spinner-icon v-else class="h-8 w-8" />
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +79,7 @@
|
|||||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pt-0.5">
|
<div id="timestamp-row" class="flex pt-0.5">
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
|
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
|
||||||
@@ -159,7 +160,7 @@ export default {
|
|||||||
return this.showFullscreen ? 200 : 60
|
return this.showFullscreen ? 200 : 60
|
||||||
},
|
},
|
||||||
showCastBtn() {
|
showCastBtn() {
|
||||||
return this.$store.state.isCastAvailable && !this.isLocalPlayMethod
|
return this.$store.state.isCastAvailable
|
||||||
},
|
},
|
||||||
isCasting() {
|
isCasting() {
|
||||||
return this.mediaPlayer === 'cast-player'
|
return this.mediaPlayer === 'cast-player'
|
||||||
@@ -193,6 +194,9 @@ export default {
|
|||||||
isLocalPlayMethod() {
|
isLocalPlayMethod() {
|
||||||
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
||||||
},
|
},
|
||||||
|
isDirectPlayMethod() {
|
||||||
|
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.playbackSession) return this.playbackSession.displayTitle
|
if (this.playbackSession) return this.playbackSession.displayTitle
|
||||||
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
||||||
@@ -269,12 +273,10 @@ export default {
|
|||||||
this.showChapterModal = false
|
this.showChapterModal = false
|
||||||
},
|
},
|
||||||
castClick() {
|
castClick() {
|
||||||
console.log('Cast Btn Click')
|
|
||||||
if (this.isLocalPlayMethod) {
|
if (this.isLocalPlayMethod) {
|
||||||
this.$toast.warn('Cannot cast downloaded media items')
|
this.$eventBus.$emit('cast-local-item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
AbsAudioPlayer.requestSession()
|
AbsAudioPlayer.requestSession()
|
||||||
},
|
},
|
||||||
clickContainer() {
|
clickContainer() {
|
||||||
@@ -657,13 +659,15 @@ export default {
|
|||||||
.bookCoverWrapper {
|
.bookCoverWrapper {
|
||||||
box-shadow: 3px -2px 5px #00000066;
|
box-shadow: 3px -2px 5px #00000066;
|
||||||
}
|
}
|
||||||
#streamContainer {
|
.playerContainer {
|
||||||
box-shadow: 0px -8px 8px #11111155;
|
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
.fullscreen #streamContainer {
|
.fullscreen .playerContainer {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
#playerContent {
|
||||||
|
box-shadow: 0px -8px 8px #11111155;
|
||||||
|
}
|
||||||
|
|
||||||
#playerTrack {
|
#playerTrack {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
@@ -674,6 +678,11 @@ export default {
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ios-player #timestamp-row {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.cover-wrapper {
|
.cover-wrapper {
|
||||||
bottom: 44px;
|
bottom: 44px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
|
|||||||
@@ -168,10 +168,36 @@ export default {
|
|||||||
this.$refs.audioPlayer.closePlayback()
|
this.$refs.audioPlayer.closePlayback()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
castLocalItem() {
|
||||||
|
if (!this.serverLibraryItemId) {
|
||||||
|
this.$toast.error(`Cannot cast locally downloaded media`)
|
||||||
|
} else {
|
||||||
|
// Change to server library item
|
||||||
|
this.playServerLibraryItemAndCast(this.serverLibraryItemId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playServerLibraryItemAndCast(libraryItemId) {
|
||||||
|
var playbackRate = 1
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
||||||
|
}
|
||||||
|
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId: null, playWhenReady: false, playbackRate })
|
||||||
|
.then((data) => {
|
||||||
|
console.log('Library item play response', JSON.stringify(data))
|
||||||
|
AbsAudioPlayer.requestSession()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
var libraryItemId = payload.libraryItemId
|
var libraryItemId = payload.libraryItemId
|
||||||
var episodeId = payload.episodeId
|
var episodeId = payload.episodeId
|
||||||
|
|
||||||
|
// When playing local library item and can also play this item from the server
|
||||||
|
// then store the server library item id so it can be used if a cast is made
|
||||||
|
var serverLibraryItemId = payload.serverLibraryItemId || null
|
||||||
|
|
||||||
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Warning',
|
title: 'Warning',
|
||||||
@@ -195,6 +221,8 @@ export default {
|
|||||||
console.log('Library item play response', JSON.stringify(data))
|
console.log('Library item play response', JSON.stringify(data))
|
||||||
if (!libraryItemId.startsWith('local')) {
|
if (!libraryItemId.startsWith('local')) {
|
||||||
this.serverLibraryItemId = libraryItemId
|
this.serverLibraryItemId = libraryItemId
|
||||||
|
} else {
|
||||||
|
this.serverLibraryItemId = serverLibraryItemId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -228,6 +256,7 @@ export default {
|
|||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||||
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
||||||
|
this.$eventBus.$on('cast-local-item', this.castLocalItem)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
@@ -246,6 +275,7 @@ export default {
|
|||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||||
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
||||||
|
this.$eventBus.$off('cast-local-item', this.castLocalItem)
|
||||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ export default {
|
|||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : ''
|
return this.user ? this.user.username : ''
|
||||||
},
|
},
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
navItems() {
|
navItems() {
|
||||||
var items = [
|
var items = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -24,11 +25,12 @@ export default {
|
|||||||
mixins: [bookshelfCardsHelpers],
|
mixins: [bookshelfCardsHelpers],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
routeFullPath: null,
|
||||||
|
entitiesPerShelf: 2,
|
||||||
bookshelfHeight: 0,
|
bookshelfHeight: 0,
|
||||||
bookshelfWidth: 0,
|
bookshelfWidth: 0,
|
||||||
bookshelfMarginLeft: 0,
|
bookshelfMarginLeft: 0,
|
||||||
shelvesPerPage: 0,
|
shelvesPerPage: 0,
|
||||||
entitiesPerShelf: 2,
|
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
booksPerFetch: 20,
|
booksPerFetch: 20,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -71,6 +73,7 @@ export default {
|
|||||||
return this.page
|
return this.page
|
||||||
},
|
},
|
||||||
hasFilter() {
|
hasFilter() {
|
||||||
|
if (this.page === 'series' || this.page === 'collections') return false
|
||||||
return this.filterBy !== 'all'
|
return this.filterBy !== 'all'
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
@@ -82,14 +85,11 @@ export default {
|
|||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
isCoverSquareAspectRatio() {
|
isCoverSquareAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = 100
|
var coverSize = 100
|
||||||
@@ -118,7 +118,7 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
if (this.showBookshelfListView) return this.entityHeight
|
if (this.showBookshelfListView) return this.entityHeight + 16
|
||||||
return this.entityHeight + 40
|
return this.entityHeight + 40
|
||||||
},
|
},
|
||||||
totalEntityCardWidth() {
|
totalEntityCardWidth() {
|
||||||
@@ -299,7 +299,6 @@ export default {
|
|||||||
this.bookshelfHeight = clientHeight
|
this.bookshelfHeight = clientHeight
|
||||||
this.bookshelfWidth = clientWidth
|
this.bookshelfWidth = clientWidth
|
||||||
this.entitiesPerShelf = this.showBookshelfListView ? 1 : Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
|
this.entitiesPerShelf = this.showBookshelfListView ? 1 : Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
|
||||||
|
|
||||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
||||||
|
|
||||||
@@ -319,6 +318,15 @@ export default {
|
|||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntites(0, lastBookIndex)
|
this.mountEntites(0, lastBookIndex)
|
||||||
|
|
||||||
|
// Set last scroll position for this bookshelf page
|
||||||
|
if (this.$store.state.lastBookshelfScrollData[this.page] && window['bookshelf-wrapper']) {
|
||||||
|
const { path, scrollTop } = this.$store.state.lastBookshelfScrollData[this.page]
|
||||||
|
if (path === this.routeFullPath) {
|
||||||
|
// Exact path match with query so use scroll position
|
||||||
|
window['bookshelf-wrapper'].scrollTop = scrollTop
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scroll(e) {
|
scroll(e) {
|
||||||
if (!e || !e.target) return
|
if (!e || !e.target) return
|
||||||
@@ -361,6 +369,8 @@ export default {
|
|||||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||||
window.history.replaceState({ path: newurl }, '', newurl)
|
window.history.replaceState({ path: newurl }, '', newurl)
|
||||||
|
|
||||||
|
this.routeFullPath = window.location.pathname + (window.location.search || '') // Update for saving scroll position
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,12 +465,22 @@ export default {
|
|||||||
this.$socket.$off('items_added', this.libraryItemsAdded)
|
this.$socket.$off('items_added', this.libraryItemsAdded)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updated() {
|
||||||
|
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.removeListeners()
|
this.removeListeners()
|
||||||
|
|
||||||
|
// Set bookshelf scroll position for specific bookshelf page and query
|
||||||
|
if (window['bookshelf-wrapper']) {
|
||||||
|
this.$store.commit('setLastBookshelfScrollData', { scrollTop: window['bookshelf-wrapper'].scrollTop || 0, path: this.routeFullPath, name: this.page })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -353,7 +353,34 @@ export default {
|
|||||||
this.isSelectionMode = val
|
this.isSelectionMode = val
|
||||||
if (!val) this.selected = false
|
if (!val) this.selected = false
|
||||||
},
|
},
|
||||||
setEntity(libraryItem) {
|
setEntity(_libraryItem) {
|
||||||
|
var libraryItem = _libraryItem
|
||||||
|
|
||||||
|
// this code block is only necessary when showing a selected series with sequence #
|
||||||
|
// it will update the selected series so we get realtime updates for series sequence changes
|
||||||
|
if (this.series) {
|
||||||
|
// i know.. but the libraryItem passed to this func cannot be modified so we need to create a copy
|
||||||
|
libraryItem = {
|
||||||
|
..._libraryItem,
|
||||||
|
media: {
|
||||||
|
..._libraryItem.media,
|
||||||
|
metadata: {
|
||||||
|
..._libraryItem.media.metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mediaMetadata = libraryItem.media.metadata
|
||||||
|
if (mediaMetadata.series) {
|
||||||
|
var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id)
|
||||||
|
if (newSeries) {
|
||||||
|
// update selected series
|
||||||
|
libraryItem.media.metadata.series = newSeries
|
||||||
|
this.libraryItem = libraryItem
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
},
|
},
|
||||||
setLocalLibraryItem(localLibraryItem) {
|
setLocalLibraryItem(localLibraryItem) {
|
||||||
@@ -446,10 +473,6 @@ export default {
|
|||||||
this.selected = !this.selected
|
this.selected = !this.selected
|
||||||
this.$emit('select', this.libraryItem)
|
this.$emit('select', this.libraryItem)
|
||||||
},
|
},
|
||||||
play() {
|
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
|
||||||
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
|
|
||||||
},
|
|
||||||
destroy() {
|
destroy() {
|
||||||
// destroy the vue listeners, etc
|
// destroy the vue listeners, etc
|
||||||
this.$destroy()
|
this.$destroy()
|
||||||
|
|||||||
@@ -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'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,16 +54,16 @@ export default {
|
|||||||
updatedAt() {
|
updatedAt() {
|
||||||
return this._author.updatedAt
|
return this._author.updatedAt
|
||||||
},
|
},
|
||||||
serverAddres() {
|
serverAddress() {
|
||||||
return this.$store.getters['user/getServerAddress']
|
return this.$store.getters['user/getServerAddress']
|
||||||
},
|
},
|
||||||
imgSrc() {
|
imgSrc() {
|
||||||
if (!this.imagePath || !this.serverAddres) return null
|
if (!this.imagePath || !this.serverAddress) return null
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production' && this.serverAddress.startsWith('http://192.168')) {
|
||||||
// Testing
|
// Testing
|
||||||
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
}
|
}
|
||||||
return `${this.serverAddres}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
return `${this.serverAddress}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && !hasCover ? 'object-contain' : 'object-fill'" />
|
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && hasCover ? 'object-contain' : 'object-fill'" />
|
||||||
|
|
||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
@@ -121,7 +121,7 @@ export default {
|
|||||||
return this.media.coverPath || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.media.coverPath || this.localCover || this.downloadCover
|
return (!!this.media.coverPath && !this.isLocal) || this.localCover || this.downloadCover
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 128 : 96
|
var baseSize = this.squareAspectRatio ? 128 : 96
|
||||||
|
|||||||
@@ -83,11 +83,7 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
canCreateBookmark() {
|
canCreateBookmark() {
|
||||||
if (!this.isConnected) return false
|
|
||||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export default {
|
|||||||
text: 'Size',
|
text: 'Size',
|
||||||
value: 'size'
|
value: 'size'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Duration',
|
||||||
|
value: 'media.duration'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'File Birthtime',
|
text: 'File Birthtime',
|
||||||
value: 'birthtimeMs'
|
value: 'birthtimeMs'
|
||||||
|
|||||||
@@ -27,9 +27,11 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
|
|
||||||
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
<div v-if="!isIos && userCanDownload">
|
||||||
<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-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
||||||
<span v-else class="material-icons px-2 text-success text-xl">download_done</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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
import { AbsDownloader } from '@/plugins/capacitor'
|
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -61,9 +63,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return 'podcast'
|
return 'podcast'
|
||||||
},
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
audioFile() {
|
audioFile() {
|
||||||
return this.episode.audioFile
|
return this.episode.audioFile
|
||||||
},
|
},
|
||||||
@@ -127,8 +135,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectFolder() {
|
async selectFolder() {
|
||||||
this.$toast.error('Folder selector not implemented for podcasts yet')
|
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.mediaType })
|
||||||
|
if (folderObj.error) {
|
||||||
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
|
}
|
||||||
|
return folderObj
|
||||||
},
|
},
|
||||||
downloadClick() {
|
downloadClick() {
|
||||||
if (this.downloadItem) return
|
if (this.downloadItem) return
|
||||||
@@ -204,9 +216,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" />
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
|
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
||||||
|
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B951282EE822008272D4 /* AbsDownloader.m */; };
|
||||||
|
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; };
|
||||||
|
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; };
|
||||||
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||||
@@ -50,6 +54,10 @@
|
|||||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
||||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
|
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
|
||||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
||||||
|
4D66B951282EE822008272D4 /* AbsDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDownloader.m; sourceTree = "<group>"; };
|
||||||
|
4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = "<group>"; };
|
||||||
|
4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = "<group>"; };
|
||||||
|
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = "<group>"; };
|
||||||
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -111,6 +119,10 @@
|
|||||||
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
|
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
|
||||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
||||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
|
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
|
||||||
|
4D66B951282EE822008272D4 /* AbsDownloader.m */,
|
||||||
|
4D66B953282EE87C008272D4 /* AbsDownloader.swift */,
|
||||||
|
4D66B955282EE951008272D4 /* AbsFileSystem.m */,
|
||||||
|
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */,
|
||||||
);
|
);
|
||||||
path = plugins;
|
path = plugins;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -303,16 +315,20 @@
|
|||||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
||||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
||||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
||||||
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
||||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
||||||
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
||||||
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
||||||
|
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
||||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
||||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||||
|
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
||||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
||||||
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
||||||
|
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */,
|
||||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -459,14 +475,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 8;
|
||||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.9.43;
|
MARKETING_VERSION = 0.9.46;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.development;
|
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
|
||||||
@@ -483,12 +499,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 8;
|
||||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.9.43;
|
MARKETING_VERSION = 0.9.46;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
|
|||||||
|
|
||||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
|
|
||||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
||||||
|
|
||||||
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
||||||
|
|
||||||
|
CAP_PLUGIN_METHOD(cancelSleepTimer, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(decreaseSleepTime, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(increaseSleepTime, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(getSleepTimerTime, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(setSleepTimer, CAPPluginReturnPromise);
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,12 +10,19 @@ import Capacitor
|
|||||||
|
|
||||||
@objc(AbsAudioPlayer)
|
@objc(AbsAudioPlayer)
|
||||||
public class AbsAudioPlayer: CAPPlugin {
|
public class AbsAudioPlayer: CAPPlugin {
|
||||||
|
private var initialPlayWhenReady = false
|
||||||
|
private var initialPlaybackRate:Float = 1
|
||||||
|
|
||||||
override public func load() {
|
override public func load() {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
||||||
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.didBecomeActiveNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.willEnterForegroundNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerSet), name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerEnded), name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
|
||||||
|
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func prepareLibraryItem(_ call: CAPPluginCall) {
|
@objc func prepareLibraryItem(_ call: CAPPluginCall) {
|
||||||
@@ -33,8 +40,11 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
return call.resolve()
|
return call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialPlayWhenReady = playWhenReady
|
||||||
|
initialPlaybackRate = playbackRate
|
||||||
|
|
||||||
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
|
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
|
||||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId) { session in
|
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in
|
||||||
PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -43,7 +53,6 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
} catch(let exception) {
|
} catch(let exception) {
|
||||||
NSLog("failed to convert session to json")
|
NSLog("failed to convert session to json")
|
||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
|
|
||||||
call.resolve([:])
|
call.resolve([:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,18 +77,19 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func playPause(_ call: CAPPluginCall) {
|
|
||||||
PlayerHandler.playPause()
|
|
||||||
call.resolve([ "playing": !PlayerHandler.paused() ])
|
|
||||||
}
|
|
||||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||||
PlayerHandler.play()
|
PlayerHandler.paused = false
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func pausePlayer(_ call: CAPPluginCall) {
|
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||||
PlayerHandler.pause()
|
PlayerHandler.paused = true
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
// I have no clue why but after i moved this block of code from above "playPlayer" to here the app stopped crashing. Move it back up if you want to
|
||||||
|
@objc func playPause(_ call: CAPPluginCall) {
|
||||||
|
PlayerHandler.paused = !PlayerHandler.paused
|
||||||
|
call.resolve([ "playing": !PlayerHandler.paused ])
|
||||||
|
}
|
||||||
|
|
||||||
@objc func seek(_ call: CAPPluginCall) {
|
@objc func seek(_ call: CAPPluginCall) {
|
||||||
PlayerHandler.seek(amount: call.getDouble("value", 0.0))
|
PlayerHandler.seek(amount: call.getDouble("value", 0.0))
|
||||||
@@ -95,13 +105,97 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func sendMetadata() {
|
@objc func sendMetadata() {
|
||||||
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused() ])
|
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ])
|
||||||
self.notifyListeners("onMetadata", data: PlayerHandler.getMetdata())
|
self.notifyListeners("onMetadata", data: PlayerHandler.getMetdata())
|
||||||
}
|
}
|
||||||
@objc func sendPlaybackClosedEvent() {
|
@objc func sendPlaybackClosedEvent() {
|
||||||
self.notifyListeners("onPlaybackClosed", data: [ "value": true ])
|
self.notifyListeners("onPlaybackClosed", data: [ "value": true ])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func decreaseSleepTime(_ call: CAPPluginCall) {
|
||||||
|
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
|
||||||
|
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
|
||||||
|
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
|
||||||
|
|
||||||
|
PlayerHandler.remainingSleepTime = currentSleepTime - (time / 1000)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
@objc func increaseSleepTime(_ call: CAPPluginCall) {
|
||||||
|
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
|
||||||
|
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
|
||||||
|
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
|
||||||
|
|
||||||
|
PlayerHandler.remainingSleepTime = currentSleepTime + (time / 1000)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
@objc func setSleepTimer(_ call: CAPPluginCall) {
|
||||||
|
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
|
||||||
|
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
|
||||||
|
|
||||||
|
NSLog("chapter time: \(call.getBool("isChapterTime", false))")
|
||||||
|
|
||||||
|
if call.getBool("isChapterTime", false) {
|
||||||
|
let timeToPause = time / 1000 - Int(PlayerHandler.getCurrentTime() ?? 0)
|
||||||
|
if timeToPause < 0 { return call.resolve([ "success": false ]) }
|
||||||
|
|
||||||
|
NSLog("oof \(timeToPause)")
|
||||||
|
|
||||||
|
PlayerHandler.remainingSleepTime = timeToPause
|
||||||
|
return call.resolve([ "success": true ])
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerHandler.remainingSleepTime = time / 1000
|
||||||
|
call.resolve([ "success": true ])
|
||||||
|
}
|
||||||
|
@objc func cancelSleepTimer(_ call: CAPPluginCall) {
|
||||||
|
PlayerHandler.remainingSleepTime = nil
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
@objc func getSleepTimerTime(_ call: CAPPluginCall) {
|
||||||
|
call.resolve([
|
||||||
|
"value": PlayerHandler.remainingSleepTime
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func sendSleepTimerEnded() {
|
||||||
|
self.notifyListeners("onSleepTimerEnded", data: [
|
||||||
|
"value": PlayerHandler.getCurrentTime()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
@objc func sendSleepTimerSet() {
|
||||||
|
self.notifyListeners("onSleepTimerSet", data: [
|
||||||
|
"value": PlayerHandler.remainingSleepTime
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func onPlaybackFailed() {
|
||||||
|
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
|
||||||
|
let playbackSession = PlayerHandler.getPlaybackSession()
|
||||||
|
let libraryItemId = playbackSession?.libraryItemId ?? ""
|
||||||
|
let episodeId = playbackSession?.episodeId ?? nil
|
||||||
|
NSLog("TEST: Forcing Transcode")
|
||||||
|
|
||||||
|
// If direct playing then fallback to transcode
|
||||||
|
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { session in
|
||||||
|
PlayerHandler.startPlayback(session: session, playWhenReady: self.initialPlayWhenReady, playbackRate: self.initialPlaybackRate)
|
||||||
|
|
||||||
|
do {
|
||||||
|
self.sendPlaybackSession(session: try session.asDictionary())
|
||||||
|
} catch(let exception) {
|
||||||
|
NSLog("failed to convert session to json")
|
||||||
|
debugPrint(exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sendMetadata()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.notifyListeners("onPlaybackFailed", data: [
|
||||||
|
"value": "Playback Error"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) {
|
@objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) {
|
||||||
self.notifyListeners("onPrepareMedia", data: [
|
self.notifyListeners("onPrepareMedia", data: [
|
||||||
"audiobookId": itemId,
|
"audiobookId": itemId,
|
||||||
@@ -111,32 +205,4 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
@objc func sendPlaybackSession(session: [String: Any]) {
|
@objc func sendPlaybackSession(session: [String: Any]) {
|
||||||
self.notifyListeners("onPlaybackSession", data: session)
|
self.notifyListeners("onPlaybackSession", data: session)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
IMPLEMENTED:
|
|
||||||
|
|
||||||
cancelSleepTimer
|
|
||||||
decreaseSleepTime
|
|
||||||
increaseSleepTime
|
|
||||||
getSleepTimerTime
|
|
||||||
setSleepTimer
|
|
||||||
* closePlayback
|
|
||||||
* setPlaybackSpeed
|
|
||||||
* seekBackward
|
|
||||||
* seekForward
|
|
||||||
* seek
|
|
||||||
* playPause
|
|
||||||
* playPlayer
|
|
||||||
* pausePlayer
|
|
||||||
* getCurrentTime
|
|
||||||
|
|
||||||
* onPlaybackSession
|
|
||||||
* onPrepareMedia
|
|
||||||
|
|
||||||
* onPlaybackClosed
|
|
||||||
* onPlayingUpdate
|
|
||||||
* onMetadata
|
|
||||||
onSleepTimerEnded
|
|
||||||
onSleepTimerSet
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
}
|
}
|
||||||
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
|
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
|
||||||
let id = call.getString("serverConnectionConfigId", "")
|
let id = call.getString("serverConnectionConfigId", "")
|
||||||
Database.deleteServerConnectionConfig(id: id)
|
Database.shared.deleteServerConnectionConfig(id: id)
|
||||||
|
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@@ -63,13 +63,12 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func getDeviceData(_ call: CAPPluginCall) {
|
@objc func getDeviceData(_ call: CAPPluginCall) {
|
||||||
let configs = Database.getServerConnectionConfigs()
|
let configs = Database.shared.getServerConnectionConfigs()
|
||||||
let index = Database.getLastActiveConfigIndex()
|
let index = Database.shared.getLastActiveConfigIndex()
|
||||||
|
|
||||||
call.resolve([
|
call.resolve([
|
||||||
"serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) },
|
"serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) },
|
||||||
"lastServerConnectionConfigId": configs.first { config in config.index == index }?.id,
|
"lastServerConnectionConfigId": configs.first { config in config.index == index }?.id as Any,
|
||||||
// Luckily this isn't implemented yet
|
|
||||||
// "currentLocalPlaybackSession": nil,
|
// "currentLocalPlaybackSession": nil,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// AbsDownloader.m
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Capacitor/Capacitor.h>
|
||||||
|
|
||||||
|
CAP_PLUGIN(AbsDownloader, "AbsDownloader",
|
||||||
|
CAP_PLUGIN_METHOD(downloadLibraryItem, CAPPluginReturnPromise);
|
||||||
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// AbsDownloader.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@objc(AbsDownloader)
|
||||||
|
public class AbsDownloader: CAPPlugin {
|
||||||
|
@objc func downloadLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let libraryItemId = call.getString("libraryItemId")
|
||||||
|
let episodeId = call.getString("episodeId")
|
||||||
|
|
||||||
|
NSLog("Download library item \(libraryItemId ?? "N/A") episode \(episodeId ?? "")")
|
||||||
|
|
||||||
|
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId!, episodeId: episodeId) { libraryItem in
|
||||||
|
if (libraryItem == nil) {
|
||||||
|
NSLog("Library item not found")
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
NSLog("Got library item \(libraryItem!)")
|
||||||
|
|
||||||
|
// TODO: break out in seperate functions
|
||||||
|
libraryItem!.media.tracks?.forEach { track in
|
||||||
|
NSLog("TRACK \(track.contentUrl!)")
|
||||||
|
// filename needs to be encoded otherwise would just use contentUrl
|
||||||
|
let filename = track.metadata?.filename ?? ""
|
||||||
|
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||||
|
let urlstr = "\(Store.serverConfig!.address)/s/item/\(libraryItemId!)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||||
|
let url = URL(string: urlstr)!
|
||||||
|
|
||||||
|
|
||||||
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
let itemDirectory = documentsDirectory.appendingPathComponent("\(libraryItemId!)")
|
||||||
|
NSLog("ITEM DIR \(itemDirectory)")
|
||||||
|
|
||||||
|
// Create library item directory
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: false)
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output filename
|
||||||
|
let trackFilename = itemDirectory.appendingPathComponent("\(filename)")
|
||||||
|
|
||||||
|
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
|
||||||
|
|
||||||
|
guard let fileURL = urlOrNil else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
NSLog("Download TMP file URL \(fileURL)")
|
||||||
|
let imageData = try Data(contentsOf:fileURL)
|
||||||
|
try imageData.write(to: trackFilename)
|
||||||
|
NSLog("Download written to \(trackFilename)")
|
||||||
|
} catch {
|
||||||
|
NSLog("FILE ERROR: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// AbsFileSystem.m
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Capacitor/Capacitor.h>
|
||||||
|
|
||||||
|
CAP_PLUGIN(AbsFileSystem, "AbsFileSystem",
|
||||||
|
CAP_PLUGIN_METHOD(selectFolder, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(checkFolderPermission, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(scanFolder, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(removeFolder, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(removeLocalLibraryItem, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(scanLocalLibraryItem, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(deleteItem, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(deleteTrackFromItem, CAPPluginReturnPromise);
|
||||||
|
)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// AbsFileSystem.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@objc(AbsFileSystem)
|
||||||
|
public class AbsFileSystem: CAPPlugin {
|
||||||
|
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||||
|
let mediaType = call.getString("mediaType")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("Select Folder for media type \(mediaType ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
||||||
|
let folderUrl = call.getString("folderUrl")
|
||||||
|
|
||||||
|
// TODO: Is this even necessary on iOS?
|
||||||
|
NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve([
|
||||||
|
"value": true
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func scanFolder(_ call: CAPPluginCall) {
|
||||||
|
let folderId = call.getString("folderId")
|
||||||
|
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeFolder(_ call: CAPPluginCall) {
|
||||||
|
let folderId = call.getString("folderId")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("removeFolder \(folderId ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func scanLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func deleteItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let contentUrl = call.getString("contentUrl")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func deleteTrackFromItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let trackLocalFileId = call.getString("trackLocalFileId")
|
||||||
|
let contentUrl = call.getString("contentUrl")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ def capacitor_pods
|
|||||||
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'
|
||||||
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct LibraryItem: Codable {
|
|||||||
var folderId: String
|
var folderId: String
|
||||||
var path: String
|
var path: String
|
||||||
var relPath: String
|
var relPath: String
|
||||||
|
var isFile: Bool
|
||||||
var mtimeMs: Int64
|
var mtimeMs: Int64
|
||||||
var ctimeMs: Int64
|
var ctimeMs: Int64
|
||||||
var birthtimeMs: Int64
|
var birthtimeMs: Int64
|
||||||
@@ -27,6 +28,7 @@ struct LibraryItem: Codable {
|
|||||||
var mediaType: String
|
var mediaType: String
|
||||||
var media: MediaType
|
var media: MediaType
|
||||||
var libraryFiles: [LibraryFile]
|
var libraryFiles: [LibraryFile]
|
||||||
|
var userMediaProgress:MediaProgress?
|
||||||
}
|
}
|
||||||
struct MediaType: Codable {
|
struct MediaType: Codable {
|
||||||
var libraryItemId: String?
|
var libraryItemId: String?
|
||||||
@@ -125,3 +127,15 @@ struct LibraryFile: Codable {
|
|||||||
var ino: String
|
var ino: String
|
||||||
var metadata: FileMetadata
|
var metadata: FileMetadata
|
||||||
}
|
}
|
||||||
|
struct MediaProgress:Codable {
|
||||||
|
var id:String
|
||||||
|
var libraryItemId:String
|
||||||
|
var episodeId:String?
|
||||||
|
var duration:Double
|
||||||
|
var progress:Double
|
||||||
|
var currentTime:Double
|
||||||
|
var isFinished:Bool
|
||||||
|
var lastUpdate:Int64
|
||||||
|
var startedAt:Int64
|
||||||
|
var finishedAt:Int64?
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import AVFoundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
|
|
||||||
|
enum PlayMethod:Int {
|
||||||
|
case directplay = 0
|
||||||
|
case directstream = 1
|
||||||
|
case transcode = 2
|
||||||
|
case local = 3
|
||||||
|
}
|
||||||
|
|
||||||
class AudioPlayer: NSObject {
|
class AudioPlayer: NSObject {
|
||||||
// enums and @objc are not compatible
|
// enums and @objc are not compatible
|
||||||
@objc dynamic var status: Int
|
@objc dynamic var status: Int
|
||||||
@@ -24,29 +31,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,19 +59,35 @@ 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)
|
||||||
|
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() {
|
||||||
// Pause is not synchronous causing this error on below lines:
|
// Pause is not synchronous causing this error on below lines:
|
||||||
// AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
|
// AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
|
||||||
|
// It is related to L79 `AVAudioSession.sharedInstance().setActive(false)`
|
||||||
pause()
|
pause()
|
||||||
audioPlayer.replaceCurrentItem(with: nil)
|
audioPlayer.replaceCurrentItem(with: nil)
|
||||||
|
|
||||||
@@ -79,14 +98,60 @@ class AudioPlayer: NSObject {
|
|||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throws error Possibly related to the error above
|
DispatchQueue.runOnMainQueue {
|
||||||
// DispatchQueue.main.sync {
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||||
// UIApplication.shared.endReceivingRemoteControlEvents()
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
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")
|
||||||
|
}
|
||||||
|
} else if (playerItem.status == .failed) {
|
||||||
|
NSLog("TEST: queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
public func play(allowSeekBack: Bool = false) {
|
public func play(allowSeekBack: Bool = false) {
|
||||||
if allowSeekBack {
|
if allowSeekBack {
|
||||||
@@ -110,11 +175,11 @@ 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
|
||||||
|
|
||||||
self.audioPlayer.play()
|
self.audioPlayer.play()
|
||||||
self.status = 1
|
self.status = 1
|
||||||
self.rate = self.tmpRate
|
self.rate = self.tmpRate
|
||||||
@@ -122,6 +187,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 +196,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()
|
||||||
self.play()
|
} 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.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) {
|
||||||
@@ -155,25 +257,42 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.rate = rate
|
self.rate = rate
|
||||||
|
|
||||||
self.updateNowPlaying()
|
self.updateNowPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
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 getPlayMethod() -> Int {
|
||||||
|
return self.playbackSession.playMethod
|
||||||
|
}
|
||||||
|
public func getPlaybackSession() -> PlaybackSession {
|
||||||
|
return self.playbackSession
|
||||||
}
|
}
|
||||||
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] = [
|
if (playbackSession.playMethod == PlayMethod.directplay.rawValue) {
|
||||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
// The only reason this is separate is because the filename needs to be encoded
|
||||||
]
|
let filename = track.metadata?.filename ?? ""
|
||||||
|
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||||
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||||
|
let url = URL(string: urlstr)!
|
||||||
|
return AVURLAsset(url: url)
|
||||||
|
} else { // HLS Transcode
|
||||||
|
let headers: [String: String] = [
|
||||||
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
]
|
||||||
|
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(track.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])
|
||||||
@@ -186,9 +305,9 @@ class AudioPlayer: NSObject {
|
|||||||
|
|
||||||
// MARK: - Now playing
|
// MARK: - Now playing
|
||||||
private func setupRemoteTransportControls() {
|
private func setupRemoteTransportControls() {
|
||||||
// DispatchQueue.main.sync {
|
DispatchQueue.runOnMainQueue {
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
// }
|
}
|
||||||
let commandCenter = MPRemoteCommandCenter.shared()
|
let commandCenter = MPRemoteCommandCenter.shared()
|
||||||
|
|
||||||
commandCenter.playCommand.isEnabled = true
|
commandCenter.playCommand.isEnabled = true
|
||||||
@@ -209,7 +328,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
|
||||||
@@ -219,7 +338,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +348,7 @@ class AudioPlayer: NSObject {
|
|||||||
return .noSuchContent
|
return .noSuchContent
|
||||||
}
|
}
|
||||||
|
|
||||||
self.seek(event.positionTime)
|
self.seek(event.positionTime, from: "remote")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,33 +365,17 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
private func updateNowPlaying() {
|
private func updateNowPlaying() {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||||
NowPlayingInfo.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate)
|
NowPlayingInfo.shared.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
let firstReady = self.status < 0
|
|
||||||
self.status = 0
|
|
||||||
if self.playWhenReady {
|
|
||||||
seek(playbackSession.currentTime)
|
|
||||||
self.playWhenReady = false
|
|
||||||
self.play()
|
|
||||||
} else if (firstReady) { // Only seek on first readyToPlay
|
|
||||||
seek(playbackSession.currentTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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)
|
||||||
NSLog("WARNING: Item ended")
|
NSLog("WARNING: Item ended")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,7 +12,44 @@ class PlayerHandler {
|
|||||||
private static var session: PlaybackSession?
|
private static var session: PlaybackSession?
|
||||||
private static var timer: Timer?
|
private static var timer: Timer?
|
||||||
|
|
||||||
private static var listeningTimePassedSinceLastSync = 0.0
|
private static var _remainingSleepTime: Int? = nil
|
||||||
|
public static var remainingSleepTime: Int? {
|
||||||
|
get {
|
||||||
|
return _remainingSleepTime
|
||||||
|
}
|
||||||
|
set(time) {
|
||||||
|
if time != nil && time! < 0 {
|
||||||
|
_remainingSleepTime = nil
|
||||||
|
} else {
|
||||||
|
_remainingSleepTime = time
|
||||||
|
}
|
||||||
|
|
||||||
|
if _remainingSleepTime == nil {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: _remainingSleepTime)
|
||||||
|
} else {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: _remainingSleepTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static var listeningTimePassedSinceLastSync: Double = 0.0
|
||||||
|
private static var lastSyncReport: PlaybackReport?
|
||||||
|
|
||||||
|
public static var paused: Bool {
|
||||||
|
get {
|
||||||
|
guard let player = player else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return player.rate == 0.0
|
||||||
|
}
|
||||||
|
set(paused) {
|
||||||
|
if paused {
|
||||||
|
self.player?.pause()
|
||||||
|
} else {
|
||||||
|
self.player?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) {
|
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) {
|
||||||
if player != nil {
|
if player != nil {
|
||||||
@@ -20,16 +57,16 @@ class PlayerHandler {
|
|||||||
player = nil
|
player = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
NowPlayingInfo.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
||||||
|
|
||||||
self.session = session
|
self.session = session
|
||||||
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||||
|
|
||||||
// DispatchQueue.main.sync {
|
DispatchQueue.runOnMainQueue {
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||||
self.tick()
|
self.tick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
public static func stopPlayback() {
|
public static func stopPlayback() {
|
||||||
player?.destroy()
|
player?.destroy()
|
||||||
@@ -38,7 +75,7 @@ class PlayerHandler {
|
|||||||
timer?.invalidate()
|
timer?.invalidate()
|
||||||
timer = nil
|
timer = nil
|
||||||
|
|
||||||
NowPlayingInfo.reset()
|
NowPlayingInfo.shared.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getCurrentTime() -> Double? {
|
public static func getCurrentTime() -> Double? {
|
||||||
@@ -47,45 +84,32 @@ class PlayerHandler {
|
|||||||
public static func setPlaybackSpeed(speed: Float) {
|
public static func setPlaybackSpeed(speed: Float) {
|
||||||
self.player?.setPlaybackRate(speed)
|
self.player?.setPlaybackRate(speed)
|
||||||
}
|
}
|
||||||
|
public static func getPlayMethod() -> Int? {
|
||||||
public static func play() {
|
self.player?.getPlayMethod()
|
||||||
self.player?.play()
|
|
||||||
}
|
}
|
||||||
public static func pause() {
|
public static func getPlaybackSession() -> PlaybackSession? {
|
||||||
self.player?.play()
|
self.player?.getPlaybackSession()
|
||||||
}
|
|
||||||
public static func playPause() {
|
|
||||||
if paused() {
|
|
||||||
self.player?.play()
|
|
||||||
} else {
|
|
||||||
self.player?.pause()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func seekForward(amount: Double) {
|
public static func seekForward(amount: Double) {
|
||||||
if player == nil {
|
guard let player = player else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if player == nil {
|
guard let player = player else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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 paused() -> Bool {
|
|
||||||
player?.rate == 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func getMetdata() -> [String: Any] {
|
public static func getMetdata() -> [String: Any] {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
syncProgress()
|
syncProgress()
|
||||||
@@ -94,29 +118,42 @@ class PlayerHandler {
|
|||||||
return [
|
return [
|
||||||
"duration": player?.getDuration() ?? 0,
|
"duration": player?.getDuration() ?? 0,
|
||||||
"currentTime": player?.getCurrentTime() ?? 0,
|
"currentTime": player?.getCurrentTime() ?? 0,
|
||||||
"playerState": !paused(),
|
"playerState": !paused,
|
||||||
"currentRate": player?.rate ?? 0,
|
"currentRate": player?.rate ?? 0,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func tick() {
|
private static func tick() {
|
||||||
if !paused() {
|
if !paused {
|
||||||
listeningTimePassedSinceLastSync += 1
|
listeningTimePassedSinceLastSync += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if listeningTimePassedSinceLastSync > 3 {
|
if listeningTimePassedSinceLastSync > 3 {
|
||||||
syncProgress()
|
syncProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remainingSleepTime != nil {
|
||||||
|
if remainingSleepTime! == 0 {
|
||||||
|
paused = true
|
||||||
|
}
|
||||||
|
remainingSleepTime! -= 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public static func syncProgress() {
|
public static func syncProgress() {
|
||||||
if player == nil || session == nil {
|
if session == nil { return }
|
||||||
|
guard let player = player else { return }
|
||||||
|
|
||||||
|
let playerCurrentTime = player.getCurrentTime()
|
||||||
|
if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) {
|
||||||
|
// No need to syncProgress
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let report = PlaybackReport(currentTime: player!.getCurrentTime(), duration: player!.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
|
||||||
|
|
||||||
session!.currentTime = player!.getCurrentTime()
|
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")
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import Foundation
|
|||||||
import Alamofire
|
import Alamofire
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
|
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
|
||||||
|
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
|
||||||
|
if let data = data {
|
||||||
|
completion(UIImage(data:data))
|
||||||
|
}
|
||||||
|
}).resume()
|
||||||
|
}
|
||||||
|
|
||||||
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: String], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: String], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
||||||
if (Store.serverConfig == nil) {
|
if (Store.serverConfig == nil) {
|
||||||
NSLog("Server config not set")
|
NSLog("Server config not set")
|
||||||
@@ -52,16 +60,37 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||||
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, callback: @escaping (_ param: PlaybackSession) -> Void) {
|
if (Store.serverConfig == nil) {
|
||||||
|
NSLog("Server config not set")
|
||||||
|
callback?(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: HTTPHeaders = [
|
||||||
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
]
|
||||||
|
|
||||||
|
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .get, encoding: JSONEncoding.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||||
|
switch response.result {
|
||||||
|
case .success(let obj):
|
||||||
|
callback?(obj)
|
||||||
|
case .failure(let error):
|
||||||
|
NSLog("api request to \(endpoint) failed")
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, forceTranscode:Bool, callback: @escaping (_ param: PlaybackSession) -> Void) {
|
||||||
var endpoint = "api/items/\(libraryItemId)/play"
|
var endpoint = "api/items/\(libraryItemId)/play"
|
||||||
if episodeId != nil {
|
if episodeId != nil {
|
||||||
endpoint += "/\(episodeId!)"
|
endpoint += "/\(episodeId!)"
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||||
"forceTranscode": "true", // TODO: direct play
|
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||||
|
"forceTranscode": forceTranscode ? "1" : "",
|
||||||
"mediaPlayer": "AVPlayer",
|
"mediaPlayer": "AVPlayer",
|
||||||
], decodable: PlaybackSession.self) { obj in
|
], decodable: PlaybackSession.self) { obj in
|
||||||
var session = obj
|
var session = obj
|
||||||
@@ -75,4 +104,14 @@ class ApiClient {
|
|||||||
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
|
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
|
||||||
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
||||||
}
|
}
|
||||||
|
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
|
||||||
|
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
|
||||||
|
if episodeId != nil {
|
||||||
|
endpoint += "&episodeId=\(episodeId!)"
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiClient.getResource(endpoint: endpoint, decodable: LibraryItem.self) { obj in
|
||||||
|
callback(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,25 @@ import RealmSwift
|
|||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
// All DB releated actions must be executed on "realm-queue"
|
// All DB releated actions must be executed on "realm-queue"
|
||||||
public static let realmQueue = DispatchQueue(label: "realm-queue")
|
public static let realmQueue: DispatchQueue = DispatchQueue(label: "realm-queue")
|
||||||
private static var instance: Realm = try! Realm(queue: realmQueue)
|
public static var shared = {
|
||||||
|
realmQueue.sync {
|
||||||
|
return Database()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
private var instance: Realm
|
||||||
|
private init() {
|
||||||
|
self.instance = try! Realm(queue: Database.realmQueue)
|
||||||
|
}
|
||||||
|
|
||||||
public static func setServerConnectionConfig(config: ServerConnectionConfig) {
|
public func setServerConnectionConfig(config: ServerConnectionConfig) {
|
||||||
var refrence: ThreadSafeReference<ServerConnectionConfig>?
|
var refrence: ThreadSafeReference<ServerConnectionConfig>?
|
||||||
if config.realm != nil {
|
if config.realm != nil {
|
||||||
refrence = ThreadSafeReference(to: config)
|
refrence = ThreadSafeReference(to: config)
|
||||||
}
|
}
|
||||||
|
|
||||||
realmQueue.sync {
|
Database.realmQueue.sync {
|
||||||
let existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id)
|
let existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id)
|
||||||
|
|
||||||
if config.index == 0 {
|
if config.index == 0 {
|
||||||
@@ -55,8 +64,8 @@ class Database {
|
|||||||
setLastActiveConfigIndex(index: config.index)
|
setLastActiveConfigIndex(index: config.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func deleteServerConnectionConfig(id: String) {
|
public func deleteServerConnectionConfig(id: String) {
|
||||||
realmQueue.sync {
|
Database.realmQueue.sync {
|
||||||
let config = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: id)
|
let config = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: id)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -71,10 +80,10 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func getServerConnectionConfigs() -> [ServerConnectionConfig] {
|
public func getServerConnectionConfigs() -> [ServerConnectionConfig] {
|
||||||
var refrences: [ThreadSafeReference<ServerConnectionConfig>] = []
|
var refrences: [ThreadSafeReference<ServerConnectionConfig>] = []
|
||||||
|
|
||||||
realmQueue.sync {
|
Database.realmQueue.sync {
|
||||||
let configs = instance.objects(ServerConnectionConfig.self)
|
let configs = instance.objects(ServerConnectionConfig.self)
|
||||||
refrences = configs.map { config in
|
refrences = configs.map { config in
|
||||||
return ThreadSafeReference(to: config)
|
return ThreadSafeReference(to: config)
|
||||||
@@ -94,12 +103,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func setLastActiveConfigIndexToNil() {
|
public func setLastActiveConfigIndexToNil() {
|
||||||
realmQueue.sync {
|
Database.realmQueue.sync {
|
||||||
setLastActiveConfigIndex(index: nil)
|
setLastActiveConfigIndex(index: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func setLastActiveConfigIndex(index: Int?) {
|
public func setLastActiveConfigIndex(index: Int?) {
|
||||||
let existing = instance.objects(ServerConnectionConfigActiveIndex.self)
|
let existing = instance.objects(ServerConnectionConfigActiveIndex.self)
|
||||||
let obj = ServerConnectionConfigActiveIndex()
|
let obj = ServerConnectionConfigActiveIndex()
|
||||||
obj.index = index
|
obj.index = index
|
||||||
@@ -114,8 +123,8 @@ class Database {
|
|||||||
debugPrint(exception)
|
debugPrint(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func getLastActiveConfigIndex() -> Int? {
|
public func getLastActiveConfigIndex() -> Int? {
|
||||||
return realmQueue.sync {
|
return Database.realmQueue.sync {
|
||||||
return instance.objects(ServerConnectionConfigActiveIndex.self).first?.index ?? nil
|
return instance.objects(ServerConnectionConfigActiveIndex.self).first?.index ?? nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,3 +18,14 @@ extension Encodable {
|
|||||||
return dictionary
|
return dictionary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
extension DispatchQueue {
|
||||||
|
static func runOnMainQueue(callback: @escaping (() -> Void)) {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
callback()
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,14 +8,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
|
|
||||||
func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
|
|
||||||
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
|
|
||||||
if let data = data {
|
|
||||||
completion(UIImage(data:data))
|
|
||||||
}
|
|
||||||
}).resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NowPlayingMetadata {
|
struct NowPlayingMetadata {
|
||||||
var id: String
|
var id: String
|
||||||
var itemId: String
|
var itemId: String
|
||||||
@@ -26,22 +18,22 @@ struct NowPlayingMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NowPlayingInfo {
|
class NowPlayingInfo {
|
||||||
private static var nowPlayingInfo: [String: Any] = [:]
|
static var shared = {
|
||||||
|
return NowPlayingInfo()
|
||||||
|
}()
|
||||||
|
|
||||||
public static func setSessionMetadata(metadata: NowPlayingMetadata) {
|
private var nowPlayingInfo: [String: Any]
|
||||||
|
private init() {
|
||||||
|
self.nowPlayingInfo = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setSessionMetadata(metadata: NowPlayingMetadata) {
|
||||||
setMetadata(artwork: nil, metadata: metadata)
|
setMetadata(artwork: nil, metadata: metadata)
|
||||||
|
|
||||||
/*
|
|
||||||
if !shouldFetchCover(id: metadata.id) || metadata.artworkUrl == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
|
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ApiClient.getData(from: url) { [self] image in
|
||||||
getData(from: url) { [self] image in
|
|
||||||
guard let downloadedImage = image else {
|
guard let downloadedImage = image else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -52,7 +44,7 @@ class NowPlayingInfo {
|
|||||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static func update(duration: Double, currentTime: Double, rate: Float) {
|
public func update(duration: Double, currentTime: Double, rate: Float) {
|
||||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||||
@@ -60,12 +52,12 @@ class NowPlayingInfo {
|
|||||||
|
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
}
|
}
|
||||||
public static func reset() {
|
public func reset() {
|
||||||
nowPlayingInfo = [:]
|
nowPlayingInfo = [:]
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func setMetadata(artwork: MPMediaItemArtwork?, metadata: NowPlayingMetadata?) {
|
private func setMetadata(artwork: MPMediaItemArtwork?, metadata: NowPlayingMetadata?) {
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,7 +76,7 @@ class NowPlayingInfo {
|
|||||||
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
|
||||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
|
||||||
}
|
}
|
||||||
private static func shouldFetchCover(id: String) -> Bool {
|
private func shouldFetchCover(id: String) -> Bool {
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,7 @@ import Foundation
|
|||||||
enum PlayerEvents: String {
|
enum PlayerEvents: String {
|
||||||
case update = "com.audiobookshelf.app.player.update"
|
case update = "com.audiobookshelf.app.player.update"
|
||||||
case closed = "com.audiobookshelf.app.player.closed"
|
case closed = "com.audiobookshelf.app.player.closed"
|
||||||
|
case sleepSet = "com.audiobookshelf.app.player.sleep.set"
|
||||||
|
case sleepEnded = "com.audiobookshelf.app.player.sleep.ended"
|
||||||
|
case failed = "com.audiobookshelf.app.player.failed"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ class Store {
|
|||||||
}
|
}
|
||||||
set(updated) {
|
set(updated) {
|
||||||
if updated != nil {
|
if updated != nil {
|
||||||
Database.setServerConnectionConfig(config: updated!)
|
Database.shared.setServerConnectionConfig(config: updated!)
|
||||||
} else {
|
} else {
|
||||||
Database.setLastActiveConfigIndexToNil()
|
Database.shared.setLastActiveConfigIndexToNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.realmQueue.sync {
|
Database.realmQueue.sync {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -79,36 +77,6 @@ export default {
|
|||||||
this.$refs.streamContainer.streamOpen(stream)
|
this.$refs.streamContainer.streamOpen(stream)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async clickUpdateToast() {
|
|
||||||
var immediateUpdateAllowed = this.$store.state.appUpdateInfo.immediateUpdateAllowed
|
|
||||||
if (immediateUpdateAllowed) {
|
|
||||||
await AppUpdate.performImmediateUpdate()
|
|
||||||
} else {
|
|
||||||
await AppUpdate.openAppStore()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async checkForUpdate() {
|
|
||||||
if (this.$platform == 'web') return
|
|
||||||
console.log('Checking for app update')
|
|
||||||
const result = await AppUpdate.getAppUpdateInfo()
|
|
||||||
if (!result) {
|
|
||||||
console.error('Invalid version check')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('App Update Info', JSON.stringify(result))
|
|
||||||
this.$store.commit('setAppUpdateInfo', result)
|
|
||||||
if (result.updateAvailability === 2) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$toast.info(`Update is available! Click to update.`, {
|
|
||||||
draggable: false,
|
|
||||||
hideProgressBar: false,
|
|
||||||
timeout: 20000,
|
|
||||||
closeButton: true,
|
|
||||||
onClick: this.clickUpdateToast
|
|
||||||
})
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async loadSavedSettings() {
|
async loadSavedSettings() {
|
||||||
var userSavedServerSettings = await this.$localStore.getServerSettings()
|
var userSavedServerSettings = await this.$localStore.getServerSettings()
|
||||||
if (userSavedServerSettings) {
|
if (userSavedServerSettings) {
|
||||||
@@ -255,7 +223,6 @@ export default {
|
|||||||
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
||||||
await this.syncLocalMediaProgress()
|
await this.syncLocalMediaProgress()
|
||||||
this.$store.dispatch('globals/loadLocalMediaProgress')
|
this.$store.dispatch('globals/loadLocalMediaProgress')
|
||||||
this.checkForUpdate()
|
|
||||||
this.loadSavedSettings()
|
this.loadSavedSettings()
|
||||||
this.hasMounted = true
|
this.hasMounted = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ export default {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.43-beta",
|
"version": "0.9.46-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"start": "nuxt start",
|
"start": "nuxt start",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
"@capacitor/status-bar": "^1.0.8",
|
"@capacitor/status-bar": "^1.0.8",
|
||||||
"@capacitor/storage": "^1.2.5",
|
"@capacitor/storage": "^1.2.5",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@robingenz/capacitor-app-update": "^1.3.1",
|
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
|
|||||||
@@ -19,19 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
||||||
|
|
||||||
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate">Update is available</ui-btn>
|
|
||||||
|
|
||||||
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
|
|
||||||
|
|
||||||
<p class="text-xs text-gray-400">UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
asyncData({ redirect, store }) {
|
asyncData({ redirect, store }) {
|
||||||
if (!store.state.socketConnected) {
|
if (!store.state.socketConnected) {
|
||||||
@@ -58,24 +49,6 @@ export default {
|
|||||||
},
|
},
|
||||||
serverAddress() {
|
serverAddress() {
|
||||||
return this.serverConnectionConfig.address
|
return this.serverConnectionConfig.address
|
||||||
},
|
|
||||||
appUpdateInfo() {
|
|
||||||
return this.$store.state.appUpdateInfo
|
|
||||||
},
|
|
||||||
availableVersion() {
|
|
||||||
return this.appUpdateInfo ? this.appUpdateInfo.availableVersion : null
|
|
||||||
},
|
|
||||||
currentVersion() {
|
|
||||||
return this.appUpdateInfo ? this.appUpdateInfo.currentVersion : null
|
|
||||||
},
|
|
||||||
immediateUpdateAllowed() {
|
|
||||||
return this.appUpdateInfo ? !!this.appUpdateInfo.immediateUpdateAllowed : false
|
|
||||||
},
|
|
||||||
updateAvailability() {
|
|
||||||
return this.appUpdateInfo ? this.appUpdateInfo.updateAvailability : null
|
|
||||||
},
|
|
||||||
isUpdateAvailable() {
|
|
||||||
return this.updateAvailability === 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -85,16 +58,6 @@ export default {
|
|||||||
})
|
})
|
||||||
this.$server.logout()
|
this.$server.logout()
|
||||||
this.$router.push('/connect')
|
this.$router.push('/connect')
|
||||||
},
|
|
||||||
openAppStore() {
|
|
||||||
AppUpdate.openAppStore()
|
|
||||||
},
|
|
||||||
async clickUpdate() {
|
|
||||||
if (this.immediateUpdateAllowed) {
|
|
||||||
AppUpdate.performImmediateUpdate()
|
|
||||||
} else {
|
|
||||||
AppUpdate.openAppStore()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Show an indicator for local library items whether they are linked to a server item and if that server item is connected -->
|
||||||
|
<p v-if="isLocal && serverLibraryItemId" style="font-size: 10px" class="text-success py-1 uppercase tracking-widest">connected</p>
|
||||||
|
<p v-else-if="isLocal && libraryItem.serverAddress" style="font-size: 10px" class="text-gray-400 py-1">{{ libraryItem.serverAddress }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-3">
|
<div class="flex-grow px-3">
|
||||||
<h1 class="text-lg">{{ title }}</h1>
|
<h1 class="text-lg">{{ title }}</h1>
|
||||||
<!-- <h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3> -->
|
<h3 v-if="seriesName" class="text-gray-300 text-sm leading-6">{{ seriesName }}</h3>
|
||||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
<p class="text-sm text-gray-400">by {{ author }}</p>
|
||||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||||
{{ $elapsedPretty(duration) }}
|
{{ $elapsedPretty(duration) }}
|
||||||
@@ -20,33 +23,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" 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="showDownload" :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 +109,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
resettingProgress: false,
|
resettingProgress: false,
|
||||||
|
isProcessingReadUpdate: false,
|
||||||
showSelectLocalFolder: false
|
showSelectLocalFolder: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -110,6 +117,9 @@ export default {
|
|||||||
isIos() {
|
isIos() {
|
||||||
return this.$platform === 'ios'
|
return this.$platform === 'ios'
|
||||||
},
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
isLocal() {
|
isLocal() {
|
||||||
return this.libraryItem.isLocal
|
return this.libraryItem.isLocal
|
||||||
},
|
},
|
||||||
@@ -129,8 +139,14 @@ export default {
|
|||||||
var podcastMedia = this.localLibraryItem.media
|
var podcastMedia = this.localLibraryItem.media
|
||||||
return podcastMedia ? podcastMedia.episodes || [] : []
|
return podcastMedia ? podcastMedia.episodes || [] : []
|
||||||
},
|
},
|
||||||
isConnected() {
|
serverLibraryItemId() {
|
||||||
return this.$store.state.socketConnected
|
if (!this.isLocal) return this.libraryItem.id
|
||||||
|
// Check if local library item is connected to the current server
|
||||||
|
if (!this.libraryItem.serverAddress || !this.libraryItem.libraryItemId) return null
|
||||||
|
if (this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress) {
|
||||||
|
return this.libraryItem.libraryItemId
|
||||||
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
@@ -163,6 +179,10 @@ export default {
|
|||||||
series() {
|
series() {
|
||||||
return this.mediaMetadata.series || []
|
return this.mediaMetadata.series || []
|
||||||
},
|
},
|
||||||
|
seriesName() {
|
||||||
|
// For books only on toJSONExpanded
|
||||||
|
return this.mediaMetadata.seriesName || ''
|
||||||
|
},
|
||||||
duration() {
|
duration() {
|
||||||
return this.media.duration
|
return this.media.duration
|
||||||
},
|
},
|
||||||
@@ -218,6 +238,10 @@ export default {
|
|||||||
showRead() {
|
showRead() {
|
||||||
return this.ebookFile && this.ebookFormat !== 'pdf'
|
return this.ebookFile && this.ebookFormat !== 'pdf'
|
||||||
},
|
},
|
||||||
|
showDownload() {
|
||||||
|
if (this.isIos) return false
|
||||||
|
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
|
||||||
|
},
|
||||||
ebookFile() {
|
ebookFile() {
|
||||||
return this.media.ebookFile
|
return this.media.ebookFile
|
||||||
},
|
},
|
||||||
@@ -225,14 +249,14 @@ export default {
|
|||||||
if (!this.ebookFile) return null
|
if (!this.ebookFile) return null
|
||||||
return this.ebookFile.ebookFormat
|
return this.ebookFile.ebookFormat
|
||||||
},
|
},
|
||||||
hasStoragePermission() {
|
|
||||||
return this.$store.state.hasStoragePermission
|
|
||||||
},
|
|
||||||
downloadItem() {
|
downloadItem() {
|
||||||
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
|
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
episodes() {
|
episodes() {
|
||||||
return this.media.episodes || []
|
return this.media.episodes || []
|
||||||
|
},
|
||||||
|
isCasting() {
|
||||||
|
return this.$store.state.isCasting
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -241,7 +265,12 @@ export default {
|
|||||||
},
|
},
|
||||||
playClick() {
|
playClick() {
|
||||||
// Todo: Allow playing local or streaming
|
// Todo: Allow playing local or streaming
|
||||||
if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id })
|
if (this.hasLocal && this.serverLibraryItemId && this.isCasting) {
|
||||||
|
// If casting and connected to server for local library item then send server library item id
|
||||||
|
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, serverLibraryItemId: this.serverLibraryItemId })
|
||||||
this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
|
this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
|
||||||
},
|
},
|
||||||
async clearProgressClick() {
|
async clearProgressClick() {
|
||||||
@@ -294,13 +323,17 @@ export default {
|
|||||||
if (this.downloadItem) {
|
if (this.downloadItem) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.download()
|
|
||||||
},
|
|
||||||
async download(selectedLocalFolder = null) {
|
|
||||||
if (!this.numTracks) {
|
if (!this.numTracks) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.isIos) {
|
||||||
|
// no local folders on iOS
|
||||||
|
this.startDownload()
|
||||||
|
} else {
|
||||||
|
this.download()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async download(selectedLocalFolder = null) {
|
||||||
// Get the local folder to download to
|
// Get the local folder to download to
|
||||||
var localFolder = selectedLocalFolder
|
var localFolder = selectedLocalFolder
|
||||||
if (!localFolder) {
|
if (!localFolder) {
|
||||||
@@ -338,9 +371,15 @@ export default {
|
|||||||
this.startDownload(localFolder)
|
this.startDownload(localFolder)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async startDownload(localFolder) {
|
async startDownload(localFolder = null) {
|
||||||
console.log('Starting download to local folder', localFolder.name)
|
const payload = {
|
||||||
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
|
libraryItemId: this.libraryItemId
|
||||||
|
}
|
||||||
|
if (localFolder) {
|
||||||
|
console.log('Starting download to local folder', localFolder.name)
|
||||||
|
payload.localFolderId = localFolder.id
|
||||||
|
}
|
||||||
|
var downloadRes = await AbsDownloader.downloadLibraryItem(payload)
|
||||||
if (downloadRes && downloadRes.error) {
|
if (downloadRes && downloadRes.error) {
|
||||||
var errorMsg = downloadRes.error || 'Unknown error'
|
var errorMsg = downloadRes.error || 'Unknown error'
|
||||||
console.error('Download error', errorMsg)
|
console.error('Download error', errorMsg)
|
||||||
@@ -352,15 +391,59 @@ 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() {
|
||||||
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
||||||
// this.$server.socket.on('item_updated', this.itemUpdated)
|
this.$socket.on('item_updated', this.itemUpdated)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
|
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
|
||||||
// this.$server.socket.off('item_updated', this.itemUpdated)
|
this.$socket.off('item_updated', this.itemUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -199,7 +199,8 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLocalMediaProgressFinished({ localMediaProgressId, isFinished }) {
|
async updateLocalMediaProgressFinished(payload) {
|
||||||
|
// { localLibraryItemId, localEpisodeId, isFinished }
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { Capacitor } from '@capacitor/core';
|
|
||||||
import { AbsDatabase } from './capacitor/AbsDatabase'
|
import { AbsDatabase } from './capacitor/AbsDatabase'
|
||||||
|
|
||||||
const isWeb = Capacitor.getPlatform() == 'web'
|
|
||||||
|
|
||||||
class DbService {
|
class DbService {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name ‘Source’.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
-------------------------------
|
||||||
|
UBUNTU FONT LICENCE Version 1.0
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
This licence allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely. The fonts, including any derivative works, can be
|
||||||
|
bundled, embedded, and redistributed provided the terms of this licence
|
||||||
|
are met. The fonts and derivatives, however, cannot be released under
|
||||||
|
any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not require any document created using the fonts or their
|
||||||
|
derivatives to be published under this licence, as long as the primary
|
||||||
|
purpose of the document is not to be a vehicle for the distribution of
|
||||||
|
the fonts.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this licence and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components
|
||||||
|
as received under this licence.
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to
|
||||||
|
a new environment.
|
||||||
|
|
||||||
|
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||||
|
copyright ownership of the Font Software.
|
||||||
|
|
||||||
|
"Substantially Changed" refers to Modified Versions which can be easily
|
||||||
|
identified as dissimilar to the Font Software by users of the Font
|
||||||
|
Software comparing the Original Version with the Modified Version.
|
||||||
|
|
||||||
|
To "Propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification and with or without charging
|
||||||
|
a redistribution fee), making available to the public, and in some
|
||||||
|
countries other activities as well.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
This licence does not grant any rights under trademark law and all such
|
||||||
|
rights are reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of the Font Software, to propagate the Font Software, subject to
|
||||||
|
the below conditions:
|
||||||
|
|
||||||
|
1) Each copy of the Font Software must contain the above copyright
|
||||||
|
notice and this licence. These can be included either as stand-alone
|
||||||
|
text files, human-readable headers or in the appropriate machine-
|
||||||
|
readable metadata fields within text or binary files as long as those
|
||||||
|
fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
2) The font name complies with the following:
|
||||||
|
(a) The Original Version must retain its name, unmodified.
|
||||||
|
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||||
|
avoid use of the name of the Original Version or similar names entirely.
|
||||||
|
(c) Modified Versions which are not Substantially Changed must be
|
||||||
|
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||||
|
additional naming elements to distinguish the Modified Version from the
|
||||||
|
Original Version. The name of such Modified Versions must be the name of
|
||||||
|
the Original Version, with "derivative X" where X represents the name of
|
||||||
|
the new work, appended to that name.
|
||||||
|
|
||||||
|
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||||
|
Font Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except (i) as required by this licence, (ii) to
|
||||||
|
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||||
|
their explicit written permission.
|
||||||
|
|
||||||
|
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||||
|
be distributed entirely under this licence, and must not be distributed
|
||||||
|
under any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not affect any document created using the Font Software,
|
||||||
|
except any version of the Font Software extracted from a document
|
||||||
|
created using the Font Software may only be distributed under this
|
||||||
|
licence.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This licence becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||||
|
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||||
|
DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -7,7 +7,6 @@ export const state = () => ({
|
|||||||
playerIsPlaying: false,
|
playerIsPlaying: false,
|
||||||
isCasting: false,
|
isCasting: false,
|
||||||
isCastAvailable: false,
|
isCastAvailable: false,
|
||||||
appUpdateInfo: null,
|
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
networkConnected: false,
|
networkConnected: false,
|
||||||
networkConnectionType: null,
|
networkConnectionType: null,
|
||||||
@@ -17,7 +16,8 @@ export const state = () => ({
|
|||||||
showReader: false,
|
showReader: false,
|
||||||
showSideDrawer: false,
|
showSideDrawer: false,
|
||||||
isNetworkListenerInit: false,
|
isNetworkListenerInit: false,
|
||||||
serverSettings: null
|
serverSettings: null,
|
||||||
|
lastBookshelfScrollData: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -55,6 +55,9 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setLastBookshelfScrollData(state, { scrollTop, path, name }) {
|
||||||
|
state.lastBookshelfScrollData[name] = { scrollTop, path }
|
||||||
|
},
|
||||||
setPlayerItem(state, playbackSession) {
|
setPlayerItem(state, playbackSession) {
|
||||||
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
|
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
|
||||||
|
|
||||||
@@ -84,9 +87,6 @@ export const mutations = {
|
|||||||
setIsFirstLoad(state, val) {
|
setIsFirstLoad(state, val) {
|
||||||
state.isFirstLoad = val
|
state.isFirstLoad = val
|
||||||
},
|
},
|
||||||
setAppUpdateInfo(state, info) {
|
|
||||||
state.appUpdateInfo = info
|
|
||||||
},
|
|
||||||
setSocketConnected(state, val) {
|
setSocketConnected(state, val) {
|
||||||
state.socketConnected = val
|
state.socketConnected = val
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export const getters = {
|
|||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings ? state.settings[key] || null : null
|
return state.settings ? state.settings[key] || null : null
|
||||||
|
},
|
||||||
|
getUserCanDownload: (state) => {
|
||||||
|
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||