mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-15 08:24:50 +02:00
Fix: android auto requirements, Change: New UI #33
This commit is contained in:
parent
bf8e48fd27
commit
0abefbd9bc
43 changed files with 2336 additions and 308 deletions
|
@ -13,8 +13,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 38
|
versionCode 39
|
||||||
versionName "0.9.19-beta"
|
versionName "0.9.20-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.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<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:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.audiobookshelf.app">
|
package="com.audiobookshelf.app">
|
||||||
|
|
||||||
|
@ -20,6 +21,14 @@
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
|
|
||||||
|
<!--Used by Android Auto-->
|
||||||
|
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||||
|
android:resource="@drawable/icon" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.car.application"
|
||||||
|
android:resource="@xml/automotive_app_desc"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:name="com.audiobookshelf.app.MainActivity"
|
android:name="com.audiobookshelf.app.MainActivity"
|
||||||
|
@ -31,14 +40,13 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!--Used by Android Auto-->
|
|
||||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
|
||||||
android:resource="@drawable/icon" />
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.gms.car.application"
|
|
||||||
android:resource="@xml/automotive_app_desc"/>
|
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
@ -66,7 +66,7 @@ class Audiobook {
|
||||||
// return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
// return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||||
return Uri.parse(localCoverUrl)
|
return Uri.parse(localCoverUrl)
|
||||||
}
|
}
|
||||||
if (book.cover == "") return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
if (book.cover == "" || serverUrl == "" || token == "") return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||||
return Uri.parse("$serverUrl${book.cover}?token=$token&ts=${book.lastUpdate}")
|
return Uri.parse("$serverUrl${book.cover}?token=$token&ts=${book.lastUpdate}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,29 +74,6 @@ class Audiobook {
|
||||||
return duration.toLong() * 1000L
|
return duration.toLong() * 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toMediaItem():MediaBrowserCompat.MediaItem {
|
|
||||||
var builder = MediaDescriptionCompat.Builder()
|
|
||||||
.setMediaId(id)
|
|
||||||
.setTitle(book.title)
|
|
||||||
.setSubtitle(book.authorFL)
|
|
||||||
.setMediaUri(null)
|
|
||||||
.setIconUri(getCover())
|
|
||||||
|
|
||||||
val extras = Bundle()
|
|
||||||
if (isDownloaded) {
|
|
||||||
extras.putLong(
|
|
||||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
|
||||||
MediaDescriptionCompat.STATUS_DOWNLOADED)
|
|
||||||
}
|
|
||||||
// extras.putInt(
|
|
||||||
// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
|
||||||
// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
|
|
||||||
builder.setExtras(extras)
|
|
||||||
|
|
||||||
var mediaDescription = builder.build()
|
|
||||||
return MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toMediaMetadata():MediaMetadataCompat {
|
fun toMediaMetadata():MediaMetadataCompat {
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
|
|
@ -2,15 +2,10 @@ package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
|
||||||
import androidx.media.utils.MediaConstants
|
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite
|
import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite
|
||||||
|
@ -40,17 +35,22 @@ class AudiobookManager {
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
var sharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
var sharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
||||||
serverUrl = sharedPreferences.getString("serverUrl", null).toString()
|
serverUrl = sharedPreferences.getString("serverUrl", "").toString()
|
||||||
Log.d(tag, "SHARED PREF SERVERURL $serverUrl")
|
Log.d(tag, "SHARED PREF SERVERURL $serverUrl")
|
||||||
token = sharedPreferences.getString("token", null).toString()
|
token = sharedPreferences.getString("token", "").toString()
|
||||||
Log.d(tag, "SHARED PREF TOKEN $token")
|
Log.d(tag, "SHARED PREF TOKEN $token")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAudiobooks(cb: (() -> Unit)) {
|
fun loadAudiobooks(cb: (() -> Unit)) {
|
||||||
|
Log.d(tag, "LOAD AUDIBOOOSK $serverUrl | $token")
|
||||||
if (serverUrl == "" || token == "") {
|
if (serverUrl == "" || token == "") {
|
||||||
Log.d(tag, "No Server or Token set")
|
Log.d(tag, "No Server or Token set")
|
||||||
cb()
|
cb()
|
||||||
return
|
return
|
||||||
|
} else if (!serverUrl.startsWith("http")) {
|
||||||
|
Log.e(tag, "Invalid server url $serverUrl")
|
||||||
|
cb()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "$serverUrl/api/library/main/audiobooks"
|
var url = "$serverUrl/api/library/main/audiobooks"
|
||||||
|
@ -98,78 +98,6 @@ class AudiobookManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchAudiobooks(result: MediaBrowserServiceCompat.Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
|
||||||
var url = "$serverUrl/api/library/main/audiobooks"
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url).addHeader("Authorization", "Bearer $token")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
Log.d(tag, "FAILURE TO CONNECT")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
response.use {
|
|
||||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
|
||||||
|
|
||||||
var bodyString = response.body!!.string()
|
|
||||||
var json = JSArray(bodyString)
|
|
||||||
var totalBooks = json.length() - 1
|
|
||||||
for (i in 0..totalBooks) {
|
|
||||||
var abobj = json.get(i)
|
|
||||||
var jsobj = JSObject(abobj.toString())
|
|
||||||
jsobj.put("isDownloaded", false)
|
|
||||||
var audiobook = Audiobook(jsobj, serverUrl, token)
|
|
||||||
|
|
||||||
if (audiobook.isMissing || audiobook.isInvalid) {
|
|
||||||
Log.d(tag, "Audiobook ${audiobook.book.title} is missing or invalid")
|
|
||||||
} else if (audiobook.numTracks <= 0) {
|
|
||||||
Log.d(tag, "Audiobook ${audiobook.book.title} has audio tracks")
|
|
||||||
} else {
|
|
||||||
var audiobookExists = audiobooks.find { it.id == audiobook.id }
|
|
||||||
if (audiobookExists == null) {
|
|
||||||
audiobooks.add(audiobook)
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "Audiobook already there from downloaded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(tag, "${audiobooks.size} Audiobooks Loaded")
|
|
||||||
|
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
|
||||||
audiobooks.forEach {
|
|
||||||
var builder = MediaDescriptionCompat.Builder()
|
|
||||||
.setMediaId(it.id)
|
|
||||||
.setTitle(it.book.title)
|
|
||||||
.setSubtitle(it.book.authorFL)
|
|
||||||
.setMediaUri(null)
|
|
||||||
.setIconUri(it.getCover())
|
|
||||||
|
|
||||||
val extras = Bundle()
|
|
||||||
if (it.isDownloaded) {
|
|
||||||
extras.putLong(
|
|
||||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
|
||||||
MediaDescriptionCompat.STATUS_DOWNLOADED)
|
|
||||||
}
|
|
||||||
// extras.putInt(
|
|
||||||
// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
|
||||||
// MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
|
|
||||||
builder.setExtras(extras)
|
|
||||||
|
|
||||||
var mediaDescription = builder.build()
|
|
||||||
var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
mediaItems.add(newMediaItem)
|
|
||||||
|
|
||||||
}
|
|
||||||
Log.d(tag, "AudiobookManager: Sending ${mediaItems.size} Audiobooks")
|
|
||||||
result.sendResult(mediaItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
|
@ -261,4 +189,58 @@ class AudiobookManager {
|
||||||
var audiobookStreamData = AudiobookStreamData(abStreamDataObj)
|
var audiobookStreamData = AudiobookStreamData(abStreamDataObj)
|
||||||
return audiobookStreamData
|
return audiobookStreamData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
|
||||||
|
val lhsLength = lhs.length + 1
|
||||||
|
val rhsLength = rhs.length + 1
|
||||||
|
|
||||||
|
var cost = Array(lhsLength) { it }
|
||||||
|
var newCost = Array(lhsLength) { 0 }
|
||||||
|
|
||||||
|
for (i in 1..rhsLength-1) {
|
||||||
|
newCost[0] = i
|
||||||
|
|
||||||
|
for (j in 1..lhsLength-1) {
|
||||||
|
val match = if(lhs[j - 1] == rhs[i - 1]) 0 else 1
|
||||||
|
|
||||||
|
val costReplace = cost[j - 1] + match
|
||||||
|
val costInsert = cost[j] + 1
|
||||||
|
val costDelete = newCost[j - 1] + 1
|
||||||
|
|
||||||
|
newCost[j] = Math.min(Math.min(costInsert, costDelete), costReplace)
|
||||||
|
}
|
||||||
|
|
||||||
|
val swap = cost
|
||||||
|
cost = newCost
|
||||||
|
newCost = swap
|
||||||
|
}
|
||||||
|
|
||||||
|
return cost[lhsLength - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchForAudiobook(query:String):Audiobook? {
|
||||||
|
var closestDistance = 99
|
||||||
|
var closestMatch:Audiobook? = null
|
||||||
|
audiobooks.forEach {
|
||||||
|
var dist = levenshtein(it.book.title, query)
|
||||||
|
Log.d(tag, "LEVENSHTEIN $dist")
|
||||||
|
if (dist < closestDistance) {
|
||||||
|
closestDistance = dist
|
||||||
|
closestMatch = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closestMatch != null) {
|
||||||
|
Log.d(tag, "Closest Search is ${closestMatch?.book?.title} with distance $closestDistance")
|
||||||
|
if (closestDistance < 2) {
|
||||||
|
return closestMatch
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFirstAudiobook():Audiobook? {
|
||||||
|
if (audiobooks.isEmpty()) return null
|
||||||
|
return audiobooks[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
|
import android.app.SearchManager
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
import com.getcapacitor.JSObject
|
|
||||||
import com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlite
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BridgeActivity() {
|
class MainActivity : BridgeActivity() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -12,7 +13,9 @@ import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
|
@ -31,11 +34,9 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
import com.google.android.exoplayer2.upstream.*
|
import com.google.android.exoplayer2.upstream.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import android.view.KeyEvent
|
import okhttp3.OkHttpClient
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
|
|
||||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||||
|
@ -49,8 +50,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
interface MyCustomObjectListener {
|
interface MyCustomObjectListener {
|
||||||
fun onPlayingUpdate(isPlaying: Boolean)
|
fun onPlayingUpdate(isPlaying: Boolean)
|
||||||
fun onMetadata(metadata: JSObject)
|
fun onMetadata(metadata: JSObject)
|
||||||
fun onPrepare(audiobookId:String, playWhenReady:Boolean)
|
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
||||||
fun onSleepTimerEnded(currentPosition:Long)
|
fun onSleepTimerEnded(currentPosition: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
private lateinit var mediaSessionConnector: MediaSessionConnector
|
private lateinit var mediaSessionConnector: MediaSessionConnector
|
||||||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
|
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||||
|
|
||||||
private val serviceJob = SupervisorJob()
|
private val serviceJob = SupervisorJob()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
|
||||||
|
@ -77,8 +79,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
private var currentAudiobookStreamData:AudiobookStreamData? = null
|
private var currentAudiobookStreamData:AudiobookStreamData? = null
|
||||||
|
|
||||||
// private var audiobooks = mutableListOf<AudiobookStreamData>()
|
|
||||||
|
|
||||||
private var mediaButtonClickCount: Int = 0
|
private var mediaButtonClickCount: Int = 0
|
||||||
var mediaButtonClickTimeout: Long = 1000 //ms
|
var mediaButtonClickTimeout: Long = 1000 //ms
|
||||||
var seekAmount: Long = 20000 //ms
|
var seekAmount: Long = 20000 //ms
|
||||||
|
@ -127,7 +127,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(intent: Intent?, startId: Int) {
|
override fun onStart(intent: Intent?, startId: Int) {
|
||||||
Log.d(tag, "onStart $startId" )
|
Log.d(tag, "onStart $startId")
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@ -141,6 +141,57 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
return channelId
|
return channelId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun playAudiobookFromMediaBrowser(audiobook: Audiobook, playWhenReady: Boolean) {
|
||||||
|
if (!audiobook.isDownloaded) {
|
||||||
|
var streamListener = object : AudiobookManager.OnStreamData {
|
||||||
|
override fun onStreamReady(asd: AudiobookStreamData) {
|
||||||
|
Log.d(tag, "Stream Ready ${asd.playlistUrl}")
|
||||||
|
asd.playWhenReady = playWhenReady
|
||||||
|
initPlayer(asd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audiobookManager.openStream(audiobook, streamListener)
|
||||||
|
} else {
|
||||||
|
var asd = audiobookManager.initLocalPlay(audiobook)
|
||||||
|
asd.playWhenReady = playWhenReady
|
||||||
|
initPlayer(asd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playFirstAudiobook(playWhenReady: Boolean) {
|
||||||
|
var firstAudiobook = audiobookManager.getFirstAudiobook()
|
||||||
|
if (firstAudiobook != null) {
|
||||||
|
playAudiobookFromMediaBrowser(firstAudiobook, playWhenReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openFromMediaId(mediaId: String, playWhenReady: Boolean) {
|
||||||
|
var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
|
||||||
|
if (audiobook == null) {
|
||||||
|
Log.e(tag, "Audiobook NOT FOUND")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
playAudiobookFromMediaBrowser(audiobook, playWhenReady)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openFromSearch(query: String?, playWhenReady: Boolean) {
|
||||||
|
if (query?.isNullOrEmpty() == true) {
|
||||||
|
Log.d(tag, "Empty search query play first audiobook")
|
||||||
|
playFirstAudiobook(playWhenReady)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var audiobook = audiobookManager.searchForAudiobook(query)
|
||||||
|
if (audiobook == null) {
|
||||||
|
Log.e(tag, "No Audiobook found for search $query")
|
||||||
|
pause()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
playAudiobookFromMediaBrowser(audiobook, playWhenReady)
|
||||||
|
}
|
||||||
|
|
||||||
// detach player
|
// detach player
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
|
@ -193,6 +244,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
isActive = true
|
isActive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Log.d(tag, "Media Session Set")
|
Log.d(tag, "Media Session Set")
|
||||||
|
|
||||||
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
|
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
|
||||||
|
@ -247,6 +300,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
// Unknown action
|
// Unknown action
|
||||||
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||||
|
|
||||||
|
transportControls = mediaController.transportControls
|
||||||
|
|
||||||
// Color is set based on the art - cannot override
|
// Color is set based on the art - cannot override
|
||||||
// playerNotificationManager.setColor(Color.RED)
|
// playerNotificationManager.setColor(Color.RED)
|
||||||
// playerNotificationManager.setColorized(true)
|
// playerNotificationManager.setColorized(true)
|
||||||
|
@ -267,114 +322,112 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
|
val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
|
||||||
// override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||||
// Log.d(tag, "ON COMMAND $command")
|
Log.d(tag, "ON COMMAND $command")
|
||||||
// return false
|
return false
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// override fun getSupportedPrepareActions(): Long {
|
override fun getSupportedPrepareActions(): Long {
|
||||||
// return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
||||||
// PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||||
// PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
|
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
|
||||||
// PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// override fun onPrepare(playWhenReady: Boolean) {
|
override fun onPrepare(playWhenReady: Boolean) {
|
||||||
// Log.d(tag, "ON PREPARE $playWhenReady")
|
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||||
// var audiobook = audiobookManager.audiobooks[0]
|
playFirstAudiobook(playWhenReady)
|
||||||
// if (audiobook == null) {
|
}
|
||||||
// Log.e(tag, "Audiobook NOT FOUND")
|
|
||||||
// return
|
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
// }
|
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
||||||
//
|
openFromMediaId(mediaId, playWhenReady)
|
||||||
// var streamListener = object : AudiobookManager.OnStreamData {
|
}
|
||||||
// override fun onStreamReady(asd: AudiobookStreamData) {
|
|
||||||
// Log.d(tag, "Stream Ready ${asd.playlistUrl}")
|
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
// initPlayer(asd)
|
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||||
// }
|
openFromSearch(query, playWhenReady)
|
||||||
// }
|
}
|
||||||
// audiobookManager.openStream(audiobook, streamListener)
|
|
||||||
// }
|
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
//
|
Log.d(tag, "ON PREPARE FROM URI $uri")
|
||||||
// override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
}
|
||||||
// Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
}
|
||||||
// var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
|
|
||||||
// if (audiobook == null) {
|
mediaSessionConnector.setEnabledPlaybackActions(
|
||||||
// Log.e(tag, "Audiobook NOT FOUND")
|
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||||
// return
|
or PlaybackStateCompat.ACTION_PLAY
|
||||||
// }
|
or PlaybackStateCompat.ACTION_PAUSE
|
||||||
//
|
or PlaybackStateCompat.ACTION_SEEK_TO
|
||||||
// var streamListener = object : AudiobookManager.OnStreamData {
|
or PlaybackStateCompat.ACTION_FAST_FORWARD
|
||||||
// override fun onStreamReady(asd: AudiobookStreamData) {
|
or PlaybackStateCompat.ACTION_REWIND
|
||||||
// Log.d(tag, "Stream Ready ${asd.playlistUrl}")
|
or PlaybackStateCompat.ACTION_STOP
|
||||||
// initPlayer(asd)
|
)
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// audiobookManager.openStream(audiobook, streamListener)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
|
||||||
// Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
|
|
||||||
// Log.d(tag, "ON PREPARE FROM URI $uri")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
// mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
|
mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
|
||||||
mediaSessionConnector.setPlayer(mPlayer)
|
mediaSessionConnector.setPlayer(mPlayer)
|
||||||
|
|
||||||
//attach player to playerNotificationManager
|
//attach player to playerNotificationManager
|
||||||
playerNotificationManager.setPlayer(mPlayer)
|
playerNotificationManager.setPlayer(mPlayer)
|
||||||
|
|
||||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS)
|
|
||||||
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
|
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
|
||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
super.onPrepare()
|
playFirstAudiobook(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareFromSearch(query: String?, extras: Bundle?) {
|
||||||
|
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||||
|
super.onPrepareFromSearch(query, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
|
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||||
|
openFromSearch(query, true)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PLAY MEDIA SESSION COMPAT")
|
||||||
pause()
|
pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
pause()
|
pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToPrevious() {
|
override fun onSkipToPrevious() {
|
||||||
seekBackward(seekAmount)
|
seekBackward(seekAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToNext() {
|
override fun onSkipToNext() {
|
||||||
seekForward(seekAmount)
|
seekForward(seekAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFastForward() {
|
||||||
|
seekForward(seekAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRewind() {
|
||||||
|
seekForward(seekAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSeekTo(pos: Long) {
|
||||||
|
seekPlayer(pos)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
||||||
|
if (mediaId.isNullOrEmpty()) {
|
||||||
var audiobook = audiobookManager.audiobooks.find { it.id == mediaId }
|
playFirstAudiobook(true)
|
||||||
if (audiobook == null) {
|
|
||||||
Log.e(tag, "Audiobook NOT FOUND")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
openFromMediaId(mediaId, true)
|
||||||
if (!audiobook.isDownloaded) {
|
|
||||||
|
|
||||||
var streamListener = object : AudiobookManager.OnStreamData {
|
|
||||||
override fun onStreamReady(asd: AudiobookStreamData) {
|
|
||||||
Log.d(tag, "Stream Ready ${asd.playlistUrl}")
|
|
||||||
initPlayer(asd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
audiobookManager.openStream(audiobook, streamListener)
|
|
||||||
} else {
|
|
||||||
var asd = audiobookManager.initLocalPlay(audiobook)
|
|
||||||
initPlayer(asd)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
||||||
|
@ -390,7 +443,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
|
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
|
||||||
when (keyEvent?.getKeyCode()) {
|
when (keyEvent?.getKeyCode()) {
|
||||||
KeyEvent.KEYCODE_HEADSETHOOK -> {
|
KeyEvent.KEYCODE_HEADSETHOOK -> {
|
||||||
if(0 == mediaButtonClickCount) {
|
if (0 == mediaButtonClickCount) {
|
||||||
if (mPlayer.isPlaying)
|
if (mPlayer.isPlaying)
|
||||||
pause()
|
pause()
|
||||||
else
|
else
|
||||||
|
@ -399,11 +452,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY -> {
|
||||||
if(0 == mediaButtonClickCount) play()
|
if (0 == mediaButtonClickCount) play()
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||||
if(0 == mediaButtonClickCount) pause()
|
if (0 == mediaButtonClickCount) pause()
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
||||||
|
@ -415,6 +468,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
||||||
terminateStream()
|
terminateStream()
|
||||||
}
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
|
Log.d(tag, "PLAY PAUSE TEST")
|
||||||
|
transportControls.playFromSearch("Brave New World", Bundle())
|
||||||
|
|
||||||
|
// if (mPlayer.isPlaying) {
|
||||||
|
// if (0 == mediaButtonClickCount) pause()
|
||||||
|
// handleMediaButtonClickCount()
|
||||||
|
// } else {
|
||||||
|
// if (0 == mediaButtonClickCount) play()
|
||||||
|
// handleMediaButtonClickCount()
|
||||||
|
// }
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
|
Log.d(tag, "KeyCode:${keyEvent?.getKeyCode()}")
|
||||||
return false
|
return false
|
||||||
|
@ -526,8 +591,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
if (lastPauseTime == 0L) {
|
if (lastPauseTime == 0L) {
|
||||||
sendClientMetadata("ready_no_sync")
|
sendClientMetadata("ready_no_sync")
|
||||||
lastPauseTime = -1;
|
lastPauseTime = -1;
|
||||||
}
|
} else sendClientMetadata("ready")
|
||||||
else sendClientMetadata("ready")
|
|
||||||
}
|
}
|
||||||
if (mPlayer.playbackState == Player.STATE_BUFFERING) {
|
if (mPlayer.playbackState == Player.STATE_BUFFERING) {
|
||||||
Log.d(tag, "STATE_BUFFERING : " + mPlayer.currentPosition.toString())
|
Log.d(tag, "STATE_BUFFERING : " + mPlayer.currentPosition.toString())
|
||||||
|
@ -565,8 +629,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else lastPauseTime = System.currentTimeMillis()
|
||||||
else lastPauseTime = System.currentTimeMillis()
|
|
||||||
listener?.onPlayingUpdate(player.isPlaying)
|
listener?.onPlayingUpdate(player.isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -676,6 +739,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
fun pause() {
|
||||||
|
|
||||||
mPlayer.pause()
|
mPlayer.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -683,15 +747,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
mPlayer.seekTo(time)
|
mPlayer.seekTo(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekForward(amount:Long) {
|
fun seekForward(amount: Long) {
|
||||||
mPlayer.seekTo(mPlayer.currentPosition + amount)
|
mPlayer.seekTo(mPlayer.currentPosition + amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekBackward(amount:Long) {
|
fun seekBackward(amount: Long) {
|
||||||
mPlayer.seekTo(mPlayer.currentPosition - amount)
|
mPlayer.seekTo(mPlayer.currentPosition - amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPlaybackSpeed(speed:Float) {
|
fun setPlaybackSpeed(speed: Float) {
|
||||||
mPlayer.setPlaybackSpeed(speed)
|
mPlayer.setPlaybackSpeed(speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -718,10 +782,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
|
private val ALL_ROOT = "__ALL__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
|
|
||||||
|
|
||||||
private fun isValid(packageName:String, uid:Int) : Boolean {
|
private fun isValid(packageName: String, uid: Int) : Boolean {
|
||||||
Log.d(tag, "Check package $packageName is valid with uid $uid")
|
Log.d(tag, "Check package $packageName is valid with uid $uid")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -734,16 +799,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
// No further calls will be made to other media browsing methods.
|
// No further calls will be made to other media browsing methods.
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
//
|
||||||
val maximumRootChildLimit = rootHints?.getInt(
|
// val maximumRootChildLimit = rootHints?.getInt(
|
||||||
MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
|
// MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
|
||||||
/* defaultValue= */ 4)
|
// /* defaultValue= */ 4)
|
||||||
// val supportedRootChildFlags = rootHints.getInt(
|
// val supportedRootChildFlags = rootHints.getInt(
|
||||||
// MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
|
// MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
|
||||||
// /* defaultValue= */ android.media.browse.MediaBrowser.MediaItem.FLAG_BROWSABLE)
|
// /* defaultValue= */ android.media.browse.MediaBrowser.MediaItem.FLAG_BROWSABLE)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
extras.putBoolean(
|
extras.putBoolean(
|
||||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
||||||
|
@ -761,9 +825,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
||||||
|
var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
|
||||||
if (!audiobookManager.hasLoaded) {
|
if (!audiobookManager.hasLoaded) {
|
||||||
Log.d(tag, "audiobook manager loading")
|
|
||||||
result.detach()
|
result.detach()
|
||||||
audiobookManager.load()
|
audiobookManager.load()
|
||||||
audiobookManager.loadAudiobooks() {
|
audiobookManager.loadAudiobooks() {
|
||||||
|
@ -772,7 +836,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
Log.d(tag, "LOADED AUDIOBOOKS")
|
Log.d(tag, "LOADED AUDIOBOOKS")
|
||||||
browseTree = BrowseTree(this, audiobookManager.audiobooks, null)
|
browseTree = BrowseTree(this, audiobookManager.audiobooks, null)
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||||
}
|
}
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
Log.d(tag, "BROWSE TREE CHILDREN ${children.size}")
|
Log.d(tag, "BROWSE TREE CHILDREN ${children.size}")
|
||||||
|
@ -784,8 +848,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
Log.d(tag, "AUDIOBOOKS LOADING")
|
Log.d(tag, "AUDIOBOOKS LOADING")
|
||||||
result.detach()
|
result.detach()
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
Log.d(tag, "ABs are loaded")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audiobookManager.audiobooks.size == 0) {
|
if (audiobookManager.audiobooks.size == 0) {
|
||||||
|
@ -794,9 +856,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
|
||||||
|
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||||
}
|
}
|
||||||
|
@ -805,21 +864,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
|
||||||
// audiobookManager.audiobooks.forEach {
|
|
||||||
// var builder = MediaDescriptionCompat.Builder()
|
|
||||||
// .setMediaId(it.id)
|
|
||||||
// .setTitle(it.book.title)
|
|
||||||
// .setSubtitle(it.book.authorFL)
|
|
||||||
// .setMediaUri(null)
|
|
||||||
// .setIconUri(it.getCover(audiobookManager.serverUrl, audiobookManager.token))
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// var mediaDescription = builder.build()
|
|
||||||
// var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
|
||||||
// mediaItems.add(newMediaItem)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Check if this is the root menu:
|
// Check if this is the root menu:
|
||||||
if (AUTO_MEDIA_ROOT == parentMediaId) {
|
if (AUTO_MEDIA_ROOT == parentMediaId) {
|
||||||
// build the MediaItem objects for the top level,
|
// build the MediaItem objects for the top level,
|
||||||
|
@ -833,11 +877,53 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
// result.sendResult(mediaItems)
|
// result.sendResult(mediaItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
|
if (!audiobookManager.hasLoaded) {
|
||||||
|
result.detach()
|
||||||
|
audiobookManager.load()
|
||||||
|
audiobookManager.loadAudiobooks() {
|
||||||
|
audiobookManager.isLoading = false
|
||||||
|
|
||||||
|
Log.d(tag, "LOADED AUDIOBOOKS")
|
||||||
|
browseTree = BrowseTree(this, audiobookManager.audiobooks, null)
|
||||||
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
if (children != null) {
|
||||||
|
Log.d(tag, "BROWSE TREE CHILDREN ${children.size}")
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if (audiobookManager.isLoading) {
|
||||||
|
Log.d(tag, "AUDIOBOOKS LOADING")
|
||||||
|
result.detach()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audiobookManager.audiobooks.size == 0) {
|
||||||
|
Log.d(tag, "AudiobookManager: Sending no items")
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
if (children != null) {
|
||||||
|
Log.d(tag, "NO CHILDREN ON SEARCH ${children.size}")
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// SLEEP TIMER STUFF
|
// SLEEP TIMER STUFF
|
||||||
//
|
//
|
||||||
|
|
||||||
fun setSleepTimer(time:Long, isChapterTime:Boolean) : Boolean {
|
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
|
||||||
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
sleepChapterTime = 0L
|
sleepChapterTime = 0L
|
||||||
|
@ -852,7 +938,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sleepChapterTime = time
|
sleepChapterTime = time
|
||||||
sleepTimerTask = Timer("SleepTimer",false).schedule(0L, 1000L) {
|
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (mPlayer.isPlaying && mPlayer.currentPosition > sleepChapterTime) {
|
if (mPlayer.isPlaying && mPlayer.currentPosition > sleepChapterTime) {
|
||||||
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
|
||||||
|
@ -864,7 +950,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sleepTimerTask = Timer("SleepTimer",false).schedule(time) {
|
sleepTimerTask = Timer("SleepTimer", false).schedule(time) {
|
||||||
Log.d(tag, "Sleep Timer Done")
|
Log.d(tag, "Sleep Timer Done")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (mPlayer.isPlaying) {
|
if (mPlayer.isPlaying) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<automotiveApp>
|
<automotiveApp>
|
||||||
<uses name="media" />
|
<uses name="media" />
|
||||||
</automotiveApp>
|
</automotiveApp>
|
||||||
|
|
|
@ -18,4 +18,28 @@
|
||||||
|
|
||||||
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookshelfRow {
|
||||||
|
background-image: url(/wood_panels.jpg);
|
||||||
|
}
|
||||||
|
.bookshelfDivider {
|
||||||
|
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%);
|
||||||
|
box-shadow: 2px 10px 8px #1111117e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bookshelf Label
|
||||||
|
*/
|
||||||
|
.categoryPlacard {
|
||||||
|
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.shinyBlack {
|
||||||
|
background-color: #2d3436;
|
||||||
|
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
||||||
|
border-color: rgba(255, 244, 182, 0.6);
|
||||||
|
border-style: solid;
|
||||||
|
color: #fce3a6;
|
||||||
}
|
}
|
|
@ -1,12 +1,18 @@
|
||||||
/* fallback */
|
|
||||||
@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(/material-icons.woff2) format('woff2');
|
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Icons Outlined';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
/* .material-icons {
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
@ -27,6 +33,9 @@
|
||||||
.material-icons.text-lg {
|
.material-icons.text-lg {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
.material-icons.text-2xl {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
.material-icons.text-3xl {
|
.material-icons.text-3xl {
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
}
|
}
|
||||||
|
@ -38,6 +47,42 @@
|
||||||
}
|
}
|
||||||
.material-icons.text-base {
|
.material-icons.text-base {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.material-icons {
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
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: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons-outlined {
|
||||||
|
font-family: 'Material Icons Outlined';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
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-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-face {
|
@font-face {
|
||||||
|
@ -45,7 +90,7 @@
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/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 */
|
||||||
|
@ -54,6 +99,6 @@
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/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;
|
||||||
}
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<div class="w-full h-16 bg-primary relative">
|
||||||
<div id="appbar" class="absolute top-0 left-0 w-full h-full z-30 flex items-center px-2">
|
<div id="appbar" class="absolute top-0 left-0 w-full h-full z-10 flex items-center px-2">
|
||||||
<nuxt-link v-show="!showBack" to="/" class="mr-3">
|
<nuxt-link v-show="!showBack" to="/" class="mr-3">
|
||||||
<img src="/Logo.png" class="h-10 w-10" />
|
<img src="/Logo.png" class="h-10 w-10" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<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>
|
<div v-if="socketConnected">
|
||||||
<div class="px-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="px-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||||
|
@ -20,18 +20,13 @@
|
||||||
|
|
||||||
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
||||||
|
|
||||||
<span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span>
|
<!-- <span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span> -->
|
||||||
|
|
||||||
<widgets-connection-icon />
|
<!-- <widgets-connection-icon /> -->
|
||||||
|
|
||||||
<!-- <nuxt-link to="/account" class="relative w-28 bg-fg border border-gray-500 rounded shadow-sm ml-5 pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
<div class="h-7 mx-2">
|
||||||
<span class="flex items-center">
|
<span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
|
||||||
<span class="block truncate">{{ username }}</span>
|
</div>
|
||||||
</span>
|
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<span class="material-icons text-gray-100">person</span>
|
|
||||||
</span>
|
|
||||||
</nuxt-link> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -54,6 +49,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
socketConnected() {
|
||||||
|
return this.$store.state.socketConnected
|
||||||
|
},
|
||||||
currentLibrary() {
|
currentLibrary() {
|
||||||
return this.$store.getters['libraries/getCurrentLibrary']
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
},
|
},
|
||||||
|
@ -61,7 +59,7 @@ export default {
|
||||||
return this.currentLibrary ? this.currentLibrary.name : 'Main'
|
return this.currentLibrary ? this.currentLibrary.name : 'Main'
|
||||||
},
|
},
|
||||||
showBack() {
|
showBack() {
|
||||||
return this.$route.name !== 'index'
|
return this.$route.name !== 'index' && !this.$route.name.startsWith('bookshelf')
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
|
@ -81,6 +79,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickShowSideDrawer() {
|
||||||
|
this.$store.commit('setShowSideDrawer', true)
|
||||||
|
},
|
||||||
clickShowLibraryModal() {
|
clickShowLibraryModal() {
|
||||||
this.$store.commit('libraries/setShowModal', true)
|
this.$store.commit('libraries/setShowModal', true)
|
||||||
},
|
},
|
||||||
|
@ -88,7 +89,7 @@ export default {
|
||||||
if (this.$route.name === 'audiobook-id-edit') {
|
if (this.$route.name === 'audiobook-id-edit') {
|
||||||
this.$router.push(`/audiobook/${this.$route.params.id}`)
|
this.$router.push(`/audiobook/${this.$route.params.id}`)
|
||||||
} else {
|
} else {
|
||||||
this.$router.push('/')
|
this.$router.push('/bookshelf')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
|
|
116
components/app/SideDrawer.vue
Normal file
116
components/app/SideDrawer.vue
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed top-0 left-0 right-0 bottom-0 w-full h-full z-50 overflow-hidden pointer-events-none">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
|
||||||
|
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
|
||||||
|
<div class="px-6 mb-4">
|
||||||
|
<p v-if="socketConnected" class="text-base">
|
||||||
|
Welcome, <strong>{{ username }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full overflow-y-auto">
|
||||||
|
<template v-for="item in navItems">
|
||||||
|
<nuxt-link :to="item.to" :key="item.text" class="w-full hover:bg-bg hover:bg-opacity-60 flex items-center py-3 px-6 text-gray-300">
|
||||||
|
<span class="text-lg" :class="item.iconOutlined ? 'material-icons-outlined' : 'material-icons'">{{ item.icon }}</span>
|
||||||
|
<p class="pl-4">{{ item.text }}</p>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-full flex items-center py-6 px-6 text-gray-300">
|
||||||
|
<p class="text-xs">{{ $config.version }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<div v-if="socketConnected" class="flex items-center" @click="logout">
|
||||||
|
<p class="text-xs pr-2">Logout</p>
|
||||||
|
<span class="material-icons text-sm">logout</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: {
|
||||||
|
handler() {
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.showSideDrawer
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('setShowSideDrawer', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
|
username() {
|
||||||
|
return this.user ? this.user.username : ''
|
||||||
|
},
|
||||||
|
socketConnected() {
|
||||||
|
return this.$store.state.socketConnected
|
||||||
|
},
|
||||||
|
navItems() {
|
||||||
|
var items = [
|
||||||
|
{
|
||||||
|
icon: 'home',
|
||||||
|
text: 'Home',
|
||||||
|
to: '/bookshelf'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'person',
|
||||||
|
text: 'Account',
|
||||||
|
to: '/account'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'folder',
|
||||||
|
iconOutlined: true,
|
||||||
|
text: 'Downloads',
|
||||||
|
to: '/downloads'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'settings',
|
||||||
|
text: 'Settings',
|
||||||
|
to: '/config'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (!this.socketConnected) {
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
icon: 'cloud_off',
|
||||||
|
text: 'Connect to Server',
|
||||||
|
to: '/connect'
|
||||||
|
}
|
||||||
|
].concat(items)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickBackground() {
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
await this.$axios.$post('/logout').catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
this.$server.logout()
|
||||||
|
this.$router.push('/connect')
|
||||||
|
|
||||||
|
this.$store.commit('audiobooks/reset')
|
||||||
|
this.$store.dispatch('audiobooks/useDownloaded')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
35
components/bookshelf/GroupShelf.vue
Normal file
35
components/bookshelf/GroupShelf.vue
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full relative">
|
||||||
|
<div class="bookshelfRow flex items-end justify-around px-3 max-w-full" :class="shelfHeightClass">
|
||||||
|
<template v-for="group in groups">
|
||||||
|
<cards-series-card v-if="groupType === 'series'" :key="group.id" :group="group" :width="112" class="mx-2" />
|
||||||
|
<cards-collection-card v-if="groupType === 'collection'" :key="group.id" :collection="group" :width="90" class="mx-2" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full h-5 z-40 bookshelfDivider"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
groupType: String,
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
shelfHeightClass() {
|
||||||
|
if (this.groupType === 'series') return 'h-48'
|
||||||
|
return 'h-44'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
28
components/bookshelf/LibraryShelf.vue
Normal file
28
components/bookshelf/LibraryShelf.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full relative">
|
||||||
|
<div class="bookshelfRow h-48 flex items-end justify-around px-3 max-w-full">
|
||||||
|
<template v-for="book in books">
|
||||||
|
<cards-book-card :key="book.id" :audiobook="book" :width="108" class="mx-2" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full h-4 z-40 bookshelfDivider"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
books: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
34
components/bookshelf/Shelf.vue
Normal file
34
components/bookshelf/Shelf.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full relative">
|
||||||
|
<div class="bookshelfRow h-44 flex items-end px-3 max-w-full overflow-x-auto">
|
||||||
|
<template v-for="book in books">
|
||||||
|
<cards-book-card :key="book.id" :audiobook="book" :width="100" class="mx-2" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
||||||
|
<p class="transform text-xs">{{ label }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-5 z-40 bookshelfDivider"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
label: String,
|
||||||
|
books: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
86
components/cards/CollectionCard.vue
Normal file
86
components/cards/CollectionCard.vue
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="rounded-sm h-full relative" @click="clickCard">
|
||||||
|
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||||
|
<div class="w-full relative bg-primary" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||||
|
<cards-collection-cover ref="groupcover" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-5 h-5 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.8 * sizeMultiplier}rem` }">
|
||||||
|
<p class="truncate pt-px" :style="{ fontSize: labelFontSize + 'rem' }">{{ collectionName }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 120
|
||||||
|
},
|
||||||
|
paddingY: {
|
||||||
|
type: Number,
|
||||||
|
default: 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
width(newVal) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.groupcover && this.$refs.groupcover.init) {
|
||||||
|
this.$refs.groupcover.init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.coverWidth < 160) return 0.7
|
||||||
|
return 0.75
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
_collection() {
|
||||||
|
return this.collection || {}
|
||||||
|
},
|
||||||
|
groupTo() {
|
||||||
|
return `/collection/${this._collection.id}`
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
return this.width * 2
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.width * 1.6
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 120
|
||||||
|
},
|
||||||
|
paddingX() {
|
||||||
|
return 16 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
bookItems() {
|
||||||
|
return this._collection.books || []
|
||||||
|
},
|
||||||
|
collectionName() {
|
||||||
|
return this._collection.name || 'No Name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickCard() {
|
||||||
|
this.$emit('click', this.collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
63
components/cards/CollectionCover.vue
Normal file
63
components/cards/CollectionCover.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||||
|
<!-- <div class="absolute top-0 left-0 w-full h-full rounded-sm overflow-hidden z-10">
|
||||||
|
<div class="w-full h-full border border-white border-opacity-10" />
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div v-if="hasOwnCover" class="w-full h-full relative rounded-sm">
|
||||||
|
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||||
|
<div class="w-full h-full z-0" ref="coverBg" />
|
||||||
|
</div>
|
||||||
|
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="books.length" class="flex justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
|
||||||
|
<cards-book-cover :audiobook="books[0]" :width="width / 2" />
|
||||||
|
<cards-book-cover v-if="books.length > 1" :audiobook="books[1]" :width="width / 2" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
|
||||||
|
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
bookItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imageFailed: false,
|
||||||
|
showCoverBg: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 120
|
||||||
|
},
|
||||||
|
hasOwnCover() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
fullCoverUrl() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
books() {
|
||||||
|
return this.bookItems || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
imageError() {},
|
||||||
|
imageLoaded() {}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
113
components/cards/SeriesCard.vue
Normal file
113
components/cards/SeriesCard.vue
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<template>
|
||||||
|
<div class="rounded-sm relative" @click="clickCard">
|
||||||
|
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||||
|
<div class="w-full relative bg-primary" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||||
|
<cards-series-cover ref="groupcover" :name="groupName" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
||||||
|
|
||||||
|
<div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none z-40">
|
||||||
|
<p class="font-book text-xl">{{ bookItems.length }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-full h-1 flex flex-nowrap z-40">
|
||||||
|
<div v-for="userProgress in userProgressItems" :key="userProgress.audiobookId" class="h-full w-full" :class="userProgress.isRead ? 'bg-success' : userProgress.progress > 0 ? 'bg-yellow-400' : ''" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-5 h-5 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.8 * sizeMultiplier}rem` }">
|
||||||
|
<p class="truncate pt-px" :style="{ fontSize: labelFontSize + 'rem' }">{{ groupName }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 120
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
width(newVal) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.groupcover && this.$refs.groupcover.init) {
|
||||||
|
this.$refs.groupcover.init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.coverWidth < 160) return 0.7
|
||||||
|
return 0.75
|
||||||
|
},
|
||||||
|
_group() {
|
||||||
|
return this.group || {}
|
||||||
|
},
|
||||||
|
groupType() {
|
||||||
|
return this._group.type
|
||||||
|
},
|
||||||
|
groupTo() {
|
||||||
|
if (this.groupType === 'series') {
|
||||||
|
return `/bookshelf/series?series=${this.groupEncode}`
|
||||||
|
} else {
|
||||||
|
return `/bookshelf?filter=tags.${this.groupEncode}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
return this.coverHeight
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.width * 1.6
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 120
|
||||||
|
},
|
||||||
|
paddingX() {
|
||||||
|
return 16 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
bookItems() {
|
||||||
|
return this._group.books || []
|
||||||
|
},
|
||||||
|
userAudiobooks() {
|
||||||
|
return Object.values(this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {})
|
||||||
|
},
|
||||||
|
userProgressItems() {
|
||||||
|
return this.bookItems.map((item) => {
|
||||||
|
var userAudiobook = this.userAudiobooks.find((ab) => ab.audiobookId === item.id)
|
||||||
|
return userAudiobook || {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
groupName() {
|
||||||
|
return this._group.name || 'No Name'
|
||||||
|
},
|
||||||
|
groupEncode() {
|
||||||
|
return this.$encode(this.groupName)
|
||||||
|
},
|
||||||
|
filter() {
|
||||||
|
return `${this.groupType}.${this.$encode(this.groupName)}`
|
||||||
|
},
|
||||||
|
hasValidCovers() {
|
||||||
|
var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover)
|
||||||
|
return !!validCovers.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickCard() {
|
||||||
|
this.$emit('click', this.group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
171
components/cards/SeriesCover.vue
Normal file
171
components/cards/SeriesCover.vue
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<template>
|
||||||
|
<div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative">
|
||||||
|
<div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
bookItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
noValidCovers: false,
|
||||||
|
coverDiv: null,
|
||||||
|
coverWrapperEl: null,
|
||||||
|
coverImageEls: [],
|
||||||
|
coverWidth: 0,
|
||||||
|
offsetIncrement: 0,
|
||||||
|
windowWidth: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
bookItems: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
// ensure wrapper is initialized
|
||||||
|
this.$nextTick(this.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 192
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCoverUrl(book) {
|
||||||
|
return this.$store.getters['audiobooks/getBookCoverSrc'](book, '')
|
||||||
|
},
|
||||||
|
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||||
|
var src = coverData.coverUrl
|
||||||
|
|
||||||
|
var showCoverBg =
|
||||||
|
forceCoverBg ||
|
||||||
|
(await new Promise((resolve) => {
|
||||||
|
var image = new Image()
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
var { naturalWidth, naturalHeight } = image
|
||||||
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
|
var arDiff = Math.abs(aspectRatio - 1.6)
|
||||||
|
|
||||||
|
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||||
|
if (arDiff > 0.15) {
|
||||||
|
resolve(true)
|
||||||
|
} else {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image.onerror = (err) => {
|
||||||
|
console.error(err)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
image.src = src
|
||||||
|
}))
|
||||||
|
|
||||||
|
var imgdiv = document.createElement('div')
|
||||||
|
imgdiv.style.height = this.height + 'px'
|
||||||
|
imgdiv.style.width = bgCoverWidth + 'px'
|
||||||
|
imgdiv.style.left = offsetLeft + 'px'
|
||||||
|
imgdiv.style.zIndex = zIndex
|
||||||
|
imgdiv.dataset.audiobookId = coverData.id
|
||||||
|
imgdiv.dataset.volumeNumber = coverData.volumeNumber || ''
|
||||||
|
imgdiv.className = 'absolute top-0 box-shadow-book transition-transform'
|
||||||
|
imgdiv.style.boxShadow = '4px 0px 4px #11111166'
|
||||||
|
// imgdiv.style.transform = 'skew(0deg, 15deg)'
|
||||||
|
|
||||||
|
if (showCoverBg) {
|
||||||
|
var coverbgwrapper = document.createElement('div')
|
||||||
|
coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full bg-primary'
|
||||||
|
|
||||||
|
var coverbg = document.createElement('div')
|
||||||
|
coverbg.className = 'w-full h-full'
|
||||||
|
coverbg.style.backgroundImage = `url("${src}")`
|
||||||
|
coverbg.style.backgroundSize = 'cover'
|
||||||
|
coverbg.style.backgroundPosition = 'center'
|
||||||
|
coverbg.style.opacity = 0.25
|
||||||
|
coverbg.style.filter = 'blur(1px)'
|
||||||
|
|
||||||
|
coverbgwrapper.appendChild(coverbg)
|
||||||
|
imgdiv.appendChild(coverbgwrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
var img = document.createElement('img')
|
||||||
|
img.src = src
|
||||||
|
img.className = 'absolute top-0 left-0 w-full h-full'
|
||||||
|
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
||||||
|
|
||||||
|
imgdiv.appendChild(img)
|
||||||
|
return imgdiv
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
if (this.coverDiv) {
|
||||||
|
this.coverDiv.remove()
|
||||||
|
this.coverDiv = null
|
||||||
|
}
|
||||||
|
var validCovers = this.bookItems
|
||||||
|
.map((bookItem) => {
|
||||||
|
return {
|
||||||
|
id: bookItem.id,
|
||||||
|
volumeNumber: bookItem.book ? bookItem.book.volumeNumber : null,
|
||||||
|
coverUrl: this.getCoverUrl(bookItem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((b) => b.coverUrl !== '')
|
||||||
|
if (!validCovers.length) {
|
||||||
|
this.noValidCovers = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.noValidCovers = false
|
||||||
|
|
||||||
|
var coverWidth = this.width
|
||||||
|
var widthPer = this.width
|
||||||
|
if (validCovers.length > 1) {
|
||||||
|
coverWidth = this.height / 1.6
|
||||||
|
widthPer = (this.width - coverWidth) / (validCovers.length - 1)
|
||||||
|
}
|
||||||
|
this.coverWidth = coverWidth
|
||||||
|
this.offsetIncrement = widthPer
|
||||||
|
|
||||||
|
var outerdiv = document.createElement('div')
|
||||||
|
this.coverWrapperEl = outerdiv
|
||||||
|
outerdiv.className = 'w-full h-full relative'
|
||||||
|
|
||||||
|
var coverImageEls = []
|
||||||
|
var offsetLeft = 0
|
||||||
|
for (let i = 0; i < validCovers.length; i++) {
|
||||||
|
offsetLeft = widthPer * i
|
||||||
|
var zIndex = validCovers.length - i
|
||||||
|
var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, zIndex, validCovers.length === 1)
|
||||||
|
outerdiv.appendChild(img)
|
||||||
|
coverImageEls.push(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.coverImageEls = coverImageEls
|
||||||
|
|
||||||
|
if (this.$refs.wrapper) {
|
||||||
|
this.coverDiv = outerdiv
|
||||||
|
this.$refs.wrapper.appendChild(outerdiv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.coverWrapperEl) this.coverWrapperEl.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
42
components/home/BookshelfNavBar.vue
Normal file
42
components/home/BookshelfNavBar.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-9 bg-bg relative">
|
||||||
|
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
|
||||||
|
<nuxt-link to="/bookshelf" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf' ? 'bg-primary' : 'text-gray-400'">
|
||||||
|
<p>Home</p>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/bookshelf/library" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf-library' ? 'bg-primary' : 'text-gray-400'">
|
||||||
|
<p>Library</p>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/bookshelf/series" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf-series' ? 'bg-primary' : 'text-gray-400'">
|
||||||
|
<p>Series</p>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/bookshelf/collections" class="w-1/4 h-full flex items-center justify-center" :class="routeName === 'bookshelf-collections' ? 'bg-primary' : 'text-gray-400'">
|
||||||
|
<p>Collections</p>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
routeName() {
|
||||||
|
return this.$route.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#bookshelf-navbar {
|
||||||
|
box-shadow: 0px 5px 5px #11111155;
|
||||||
|
}
|
||||||
|
#bookshelf-navbar a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
138
components/home/BookshelfToolbar.vue
Normal file
138
components/home/BookshelfToolbar.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-9 bg-bg relative z-20">
|
||||||
|
<div id="bookshelf-toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-2">
|
||||||
|
<div class="flex items-center w-full text-sm">
|
||||||
|
<nuxt-link to="/bookshelf/series" v-if="selectedSeriesName" class="pt-1">
|
||||||
|
<span class="material-icons">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ numEntities }} {{ entityTitle }}</p>
|
||||||
|
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }}</p>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="page === 'library'">
|
||||||
|
<span class="material-icons px-2" @click="changeView">{{ viewIcon }}</span>
|
||||||
|
<div class="relative flex items-center px-2">
|
||||||
|
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
||||||
|
<div v-show="hasFilters" class="absolute top-0 right-2 w-2 h-2 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<modals-order-modal v-model="showSortModal" :order-by.sync="settings.mobileOrderBy" :descending.sync="settings.mobileOrderDesc" @change="updateOrder" />
|
||||||
|
<modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.mobileFilterBy" @change="updateFilter" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showSortModal: false,
|
||||||
|
showFilterModal: false,
|
||||||
|
settings: {},
|
||||||
|
isListView: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasFilters() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
||||||
|
},
|
||||||
|
page() {
|
||||||
|
var routeName = this.$route.name || ''
|
||||||
|
return routeName.split('-')[1]
|
||||||
|
},
|
||||||
|
routeQuery() {
|
||||||
|
return this.$route.query || {}
|
||||||
|
},
|
||||||
|
entityTitle() {
|
||||||
|
if (this.page === 'library') return 'Audiobooks'
|
||||||
|
else if (this.page === 'series') {
|
||||||
|
if (this.selectedSeriesName) return 'Books in ' + this.selectedSeriesName
|
||||||
|
return 'Series'
|
||||||
|
} else if (this.page === 'collections') {
|
||||||
|
return 'Collections'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
numEntities() {
|
||||||
|
if (this.page === 'library') return this.numAudiobooks
|
||||||
|
else if (this.page === 'series') {
|
||||||
|
if (this.selectedSeriesName) return this.numBooksInSeries
|
||||||
|
return this.series.length
|
||||||
|
} else if (this.page === 'collections') return this.numCollections
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
series() {
|
||||||
|
return this.$store.getters['audiobooks/getSeriesGroups']() || []
|
||||||
|
},
|
||||||
|
numCollections() {
|
||||||
|
return (this.$store.state.user.collections || []).length
|
||||||
|
},
|
||||||
|
numAudiobooks() {
|
||||||
|
return this.$store.getters['audiobooks/getFiltered']().length
|
||||||
|
},
|
||||||
|
numBooksInSeries() {
|
||||||
|
return this.selectedSeries ? (this.selectedSeries.books || []).length : 0
|
||||||
|
},
|
||||||
|
selectedSeries() {
|
||||||
|
if (!this.selectedSeriesName) return null
|
||||||
|
return this.series.find((s) => s.name === this.selectedSeriesName)
|
||||||
|
},
|
||||||
|
selectedSeriesName() {
|
||||||
|
if (this.page === 'series' && this.routeQuery.series) {
|
||||||
|
return this.$decode(this.routeQuery.series)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
viewIcon() {
|
||||||
|
return this.isListView ? 'grid_view' : 'view_stream'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeView() {
|
||||||
|
this.isListView = !this.isListView
|
||||||
|
|
||||||
|
var bookshelfView = this.isListView ? 'list' : 'grid'
|
||||||
|
this.$localStore.setBookshelfView(bookshelfView)
|
||||||
|
},
|
||||||
|
updateOrder() {
|
||||||
|
this.saveSettings()
|
||||||
|
},
|
||||||
|
updateFilter() {
|
||||||
|
this.saveSettings()
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
this.$store.commit('user/setSettings', this.settings) // Immediate update
|
||||||
|
this.$store.dispatch('user/updateUserSettings', this.settings)
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
this.settings = { ...this.$store.state.user.settings }
|
||||||
|
|
||||||
|
var bookshelfView = await this.$localStore.getBookshelfView()
|
||||||
|
this.isListView = bookshelfView === 'list'
|
||||||
|
this.bookshelfReady = true
|
||||||
|
console.log('Bookshelf view', bookshelfView)
|
||||||
|
},
|
||||||
|
settingsUpdated(settings) {
|
||||||
|
for (const key in settings) {
|
||||||
|
this.settings[key] = settings[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#bookshelf-toolbar {
|
||||||
|
box-shadow: 0px 5px 5px #11111155;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,8 +7,8 @@
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate text-lg">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-lg">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-300 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-icons text-4xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-icons text-3xl">{{ descending ? 'south' : 'north' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
80
components/tables/CollectionBooksTable.vue
Normal file
80
components/tables/CollectionBooksTable.vue
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full bg-primary bg-opacity-40">
|
||||||
|
<div class="w-full h-14 flex items-center px-4 bg-primary">
|
||||||
|
<p>Collection List</p>
|
||||||
|
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
|
||||||
|
<p class="font-mono text-sm">{{ books.length }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p v-if="totalDuration">{{ totalDurationPretty }}</p>
|
||||||
|
</div>
|
||||||
|
<template v-for="book in booksCopy">
|
||||||
|
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item" :class="drag ? '' : 'collection-book-item'" @edit="editBook" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
collectionId: String,
|
||||||
|
books: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
booksCopy: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
books: {
|
||||||
|
handler(newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
totalDuration() {
|
||||||
|
var _total = 0
|
||||||
|
this.books.forEach((book) => {
|
||||||
|
_total += book.duration
|
||||||
|
})
|
||||||
|
return _total
|
||||||
|
},
|
||||||
|
totalDurationPretty() {
|
||||||
|
return this.$elapsedPretty(this.totalDuration)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editBook(book) {
|
||||||
|
var bookIds = this.books.map((b) => b.id)
|
||||||
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
|
this.$store.commit('showEditModal', book)
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.booksCopy = this.books.map((b) => ({ ...b }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.collection-book-item {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-book-enter-from,
|
||||||
|
.collection-book-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-book-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
122
components/tables/collection/BookTableRow.vue
Normal file
122
components/tables/collection/BookTableRow.vue
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full px-2 py-2 overflow-hidden relative">
|
||||||
|
<div v-if="book" class="flex h-20">
|
||||||
|
<div class="h-full relative" :style="{ width: '50px' }">
|
||||||
|
<cards-book-cover :audiobook="book" :width="50" />
|
||||||
|
</div>
|
||||||
|
<div class="w-80 h-full px-2 flex items-center">
|
||||||
|
<div>
|
||||||
|
<nuxt-link :to="`/audiobook/${book.id}`" class="truncate hover:underline">{{ bookTitle }}</nuxt-link>
|
||||||
|
<nuxt-link :to="`/bookshelf/library?filter=authors.${$encode(bookAuthor)}`" class="truncate block text-gray-400 text-sm hover:underline">{{ bookAuthor }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
collectionId: String,
|
||||||
|
book: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isProcessingReadUpdate: false,
|
||||||
|
processingRemove: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
userIsRead: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.isRead = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_book() {
|
||||||
|
return this.book.book || {}
|
||||||
|
},
|
||||||
|
bookTitle() {
|
||||||
|
return this._book.title || ''
|
||||||
|
},
|
||||||
|
bookAuthor() {
|
||||||
|
return this._book.authorFL || ''
|
||||||
|
},
|
||||||
|
bookDuration() {
|
||||||
|
return this.$secondsToTimestamp(this.book.duration)
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.book.isMissing
|
||||||
|
},
|
||||||
|
isIncomplete() {
|
||||||
|
return this.book.isIncomplete
|
||||||
|
},
|
||||||
|
numTracks() {
|
||||||
|
return this.book.numTracks
|
||||||
|
},
|
||||||
|
isStreaming() {
|
||||||
|
return this.$store.getters['getAudiobookIdStreaming'] === this.book.id
|
||||||
|
},
|
||||||
|
showPlayBtn() {
|
||||||
|
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks
|
||||||
|
},
|
||||||
|
userAudiobooks() {
|
||||||
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
},
|
||||||
|
userAudiobook() {
|
||||||
|
return this.userAudiobooks[this.book.id] || null
|
||||||
|
},
|
||||||
|
userIsRead() {
|
||||||
|
return this.userAudiobook ? !!this.userAudiobook.isRead : false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
playClick() {
|
||||||
|
// this.$store.commit('setStreamAudiobook', this.book)
|
||||||
|
// this.$root.socket.emit('open_stream', this.book.id)
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.book)
|
||||||
|
},
|
||||||
|
toggleRead() {
|
||||||
|
var updatePayload = {
|
||||||
|
isRead: !this.isRead
|
||||||
|
}
|
||||||
|
this.isProcessingReadUpdate = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/user/audiobook/${this.book.id}`, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.success(`"${this.bookTitle}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
this.processingRemove = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/collection/${this.collectionId}/book/${this.book.id}`)
|
||||||
|
.then((updatedCollection) => {
|
||||||
|
console.log(`Book removed from collection`, updatedCollection)
|
||||||
|
this.$toast.success('Book removed from collection')
|
||||||
|
this.processingRemove = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove book from collection', error)
|
||||||
|
this.$toast.error('Failed to remove book from collection')
|
||||||
|
this.processingRemove = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -4,10 +4,10 @@
|
||||||
<div id="content" class="overflow-hidden" :class="playerIsOpen ? 'playerOpen' : ''">
|
<div id="content" class="overflow-hidden" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||||
<Nuxt />
|
<Nuxt />
|
||||||
</div>
|
</div>
|
||||||
<!-- <app-stream-container ref="streamContainer" /> -->
|
|
||||||
<app-audio-player-container ref="streamContainer" />
|
<app-audio-player-container ref="streamContainer" />
|
||||||
<modals-downloads-modal ref="downloadsModal" @deleteDownload="deleteDownload" />
|
<!-- <modals-downloads-modal ref="downloadsModal" @deleteDownload="deleteDownload" /> -->
|
||||||
<modals-libraries-modal />
|
<modals-libraries-modal />
|
||||||
|
<app-side-drawer />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,12 +24,24 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
networkConnected: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.attemptConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
playerIsOpen() {
|
playerIsOpen() {
|
||||||
return this.$store.getters['playerIsOpen']
|
return this.$store.getters['playerIsOpen']
|
||||||
},
|
},
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
|
},
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -152,9 +164,6 @@ export default {
|
||||||
|
|
||||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||||
if (downloadObj) {
|
if (downloadObj) {
|
||||||
if (this.$refs.downloadsModal) {
|
|
||||||
this.$refs.downloadsModal.updateDownloadProgress({ audiobookId, progress })
|
|
||||||
}
|
|
||||||
this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` })
|
this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -331,20 +340,6 @@ export default {
|
||||||
// this.checkLoadCurrent()
|
// this.checkLoadCurrent()
|
||||||
// this.$store.dispatch('audiobooks/setNativeAudiobooks')
|
// this.$store.dispatch('audiobooks/setNativeAudiobooks')
|
||||||
// },
|
// },
|
||||||
async deleteDownload(download) {
|
|
||||||
console.log('Delete download', download.filename)
|
|
||||||
|
|
||||||
if (this.$store.state.playingDownload && this.$store.state.playingDownload.id === download.id) {
|
|
||||||
console.warn('Deleting download when currently playing download - terminate play')
|
|
||||||
if (this.$refs.streamContainer) {
|
|
||||||
this.$refs.streamContainer.cancelStream()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (download.contentUrl) {
|
|
||||||
await StorageManager.delete(download)
|
|
||||||
}
|
|
||||||
this.$store.commit('downloads/removeDownload', download)
|
|
||||||
},
|
|
||||||
async initMediaStore() {
|
async initMediaStore() {
|
||||||
// Request and setup listeners for media files on native
|
// Request and setup listeners for media files on native
|
||||||
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
||||||
|
@ -420,9 +415,33 @@ export default {
|
||||||
},
|
},
|
||||||
showSuccessToast(message) {
|
showSuccessToast(message) {
|
||||||
this.$toast.success(message)
|
this.$toast.success(message)
|
||||||
|
},
|
||||||
|
async attemptConnection() {
|
||||||
|
if (!this.$server) return
|
||||||
|
if (!this.networkConnected) {
|
||||||
|
console.warn('No network connection')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var localServerUrl = await this.$localStore.getServerUrl()
|
||||||
|
var localUserToken = await this.$localStore.getToken()
|
||||||
|
if (localServerUrl) {
|
||||||
|
// Server and Token are stored
|
||||||
|
if (localUserToken) {
|
||||||
|
var isSocketAlreadyEstablished = this.$server.socket
|
||||||
|
var success = await this.$server.connect(localServerUrl, localUserToken)
|
||||||
|
if (!success && !this.$server.url) {
|
||||||
|
// Bad URL
|
||||||
|
} else if (!success) {
|
||||||
|
// Failed to connect
|
||||||
|
} else if (isSocketAlreadyEstablished) {
|
||||||
|
// No need to wait for connect event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
if (!this.$server) return console.error('No Server')
|
if (!this.$server) return console.error('No Server')
|
||||||
// console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`)
|
// console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`)
|
||||||
|
|
||||||
|
@ -435,28 +454,33 @@ export default {
|
||||||
|
|
||||||
if (this.$store.state.isFirstLoad) {
|
if (this.$store.state.isFirstLoad) {
|
||||||
this.$store.commit('setIsFirstLoad', false)
|
this.$store.commit('setIsFirstLoad', false)
|
||||||
this.setupNetworkListener()
|
await this.setupNetworkListener()
|
||||||
|
this.attemptConnection()
|
||||||
this.checkForUpdate()
|
this.checkForUpdate()
|
||||||
this.initMediaStore()
|
this.initMediaStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
MyNativeAudio.addListener('onPrepareMedia', (data) => {
|
if (!this.$server.connected) {
|
||||||
var audiobookId = data.audiobookId
|
}
|
||||||
var playWhenReady = data.playWhenReady
|
|
||||||
|
|
||||||
var audiobook = this.$store.getters['audiobooks/getAudiobook'](audiobookId)
|
// Old bad attempt at AA
|
||||||
|
// MyNativeAudio.addListener('onPrepareMedia', (data) => {
|
||||||
|
// var audiobookId = data.audiobookId
|
||||||
|
// var playWhenReady = data.playWhenReady
|
||||||
|
|
||||||
var download = this.$store.getters['downloads/getDownloadIfReady'](audiobookId)
|
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](audiobookId)
|
||||||
this.$store.commit('setPlayOnLoad', playWhenReady)
|
|
||||||
if (!download) {
|
// var download = this.$store.getters['downloads/getDownloadIfReady'](audiobookId)
|
||||||
// Stream
|
// this.$store.commit('setPlayOnLoad', playWhenReady)
|
||||||
this.$store.commit('setStreamAudiobook', audiobook)
|
// if (!download) {
|
||||||
this.$server.socket.emit('open_stream', audiobook.id)
|
// // Stream
|
||||||
} else {
|
// this.$store.commit('setStreamAudiobook', audiobook)
|
||||||
// Local
|
// this.$server.socket.emit('open_stream', audiobook.id)
|
||||||
this.$store.commit('setPlayingDownload', download)
|
// } else {
|
||||||
}
|
// // Local
|
||||||
})
|
// this.$store.commit('setPlayingDownload', download)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (!this.$server) {
|
if (!this.$server) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.19-beta",
|
"version": "v0.9.20-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
|
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
|
||||||
|
|
||||||
<p>UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}</p>
|
<p class="text-xs text-gray-400">UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -34,11 +34,18 @@
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ redirect, store }) {
|
||||||
|
if (!store.state.socketConnected) {
|
||||||
|
return redirect('/connect')
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
username() {
|
username() {
|
||||||
|
if (!this.user) return ''
|
||||||
return this.user.username
|
return this.user.username
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
|
|
102
pages/bookshelf.vue
Normal file
102
pages/bookshelf.vue
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<home-bookshelf-nav-bar />
|
||||||
|
<home-bookshelf-toolbar v-show="!isHome" />
|
||||||
|
<div class="main-content overflow-y-auto overflow-x-hidden relative" :class="isHome ? 'home-page' : ''">
|
||||||
|
<nuxt-child />
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!audiobooks.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<p class="mb-4 text-center text-xl">
|
||||||
|
Bookshelf empty<span v-show="isSocketConnected">
|
||||||
|
for library <strong>{{ currentLibraryName }}</strong></span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<div v-if="!isSocketConnected" class="flex items-center mb-3">
|
||||||
|
<span class="material-icons text-error text-lg">cloud_off</span>
|
||||||
|
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32"> Connect </ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isHome() {
|
||||||
|
return this.$route.name === 'bookshelf'
|
||||||
|
},
|
||||||
|
isLoading() {
|
||||||
|
return this.$store.state.audiobooks.isLoading
|
||||||
|
},
|
||||||
|
audiobooks() {
|
||||||
|
return this.$store.state.audiobooks.audiobooks
|
||||||
|
},
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
currentLibraryName() {
|
||||||
|
return this.currentLibrary ? this.currentLibrary.name : 'Main'
|
||||||
|
},
|
||||||
|
isSocketConnected() {
|
||||||
|
return this.$store.state.socketConnected
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadAudiobooks() {
|
||||||
|
var currentLibrary = await this.$localStore.getCurrentLibrary()
|
||||||
|
if (currentLibrary) {
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
|
||||||
|
}
|
||||||
|
this.$store.dispatch('audiobooks/load')
|
||||||
|
},
|
||||||
|
async loadCollections() {
|
||||||
|
this.$store.dispatch('user/loadUserCollections')
|
||||||
|
},
|
||||||
|
socketConnected(isConnected) {
|
||||||
|
if (isConnected) {
|
||||||
|
console.log('Connected - Load from server')
|
||||||
|
this.loadAudiobooks()
|
||||||
|
if (this.$route.name === 'bookshelf-collections') this.loadCollections()
|
||||||
|
} else {
|
||||||
|
console.log('Disconnected - Reset to local storage')
|
||||||
|
this.$store.commit('audiobooks/reset')
|
||||||
|
this.$store.dispatch('audiobooks/useDownloaded')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$server.on('connected', this.socketConnected)
|
||||||
|
if (this.$server.connected) {
|
||||||
|
this.loadAudiobooks()
|
||||||
|
} else {
|
||||||
|
console.log('Bookshelf - Server not connected using downloaded')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$server.off('connected', this.socketConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.main-content {
|
||||||
|
max-height: calc(100% - 72px);
|
||||||
|
min-height: calc(100% - 72px);
|
||||||
|
}
|
||||||
|
.main-content.home-page {
|
||||||
|
max-height: calc(100% - 36px);
|
||||||
|
min-height: calc(100% - 36px);
|
||||||
|
}
|
||||||
|
</style>
|
51
pages/bookshelf/collections.vue
Normal file
51
pages/bookshelf/collections.vue
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<template v-for="(shelf, index) in shelves">
|
||||||
|
<bookshelf-group-shelf :key="shelf.id" group-type="collection" :groups="shelf.groups" :style="{ zIndex: shelves.length - index }" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
groupsPerRow: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {
|
||||||
|
collections() {
|
||||||
|
return this.$store.state.user.collections || []
|
||||||
|
},
|
||||||
|
shelves() {
|
||||||
|
var shelves = []
|
||||||
|
var shelf = {
|
||||||
|
id: 0,
|
||||||
|
groups: []
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.collections.length; i++) {
|
||||||
|
var shelfNum = Math.floor((i + 1) / this.groupsPerRow)
|
||||||
|
shelf.id = shelfNum
|
||||||
|
shelf.groups.push(this.collections[i])
|
||||||
|
|
||||||
|
if ((i + 1) % this.groupsPerRow === 0) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
shelf = {
|
||||||
|
id: 0,
|
||||||
|
groups: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shelf.groups.length) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
}
|
||||||
|
return shelves
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('user/loadUserCollections')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
93
pages/bookshelf/index.vue
Normal file
93
pages/bookshelf/index.vue
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<template v-for="(shelf, index) in shelves">
|
||||||
|
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :books="shelf.books" :style="{ zIndex: shelves.length - index }" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
settings: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
books() {
|
||||||
|
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||||
|
},
|
||||||
|
booksWithUserAbData() {
|
||||||
|
var books = this.books.map((b) => {
|
||||||
|
var userAbData = this.$store.getters['user/getMostRecentUserAudiobookData'](b.id)
|
||||||
|
return { ...b, userAbData }
|
||||||
|
})
|
||||||
|
return books
|
||||||
|
},
|
||||||
|
booksCurrentlyReading() {
|
||||||
|
var books = this.booksWithUserAbData
|
||||||
|
.map((b) => ({ ...b }))
|
||||||
|
.filter((b) => b.userAbData && !b.userAbData.isRead && b.userAbData.progress > 0)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
|
||||||
|
})
|
||||||
|
return books
|
||||||
|
},
|
||||||
|
booksRecentlyAdded() {
|
||||||
|
var books = this.books
|
||||||
|
.map((b) => {
|
||||||
|
return { ...b }
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.addedAt - b.addedAt)
|
||||||
|
return books.slice(0, 10)
|
||||||
|
},
|
||||||
|
booksRead() {
|
||||||
|
var books = this.booksWithUserAbData
|
||||||
|
.filter((b) => b.userAbData && b.userAbData.isRead)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
|
||||||
|
})
|
||||||
|
return books.slice(0, 10)
|
||||||
|
},
|
||||||
|
shelves() {
|
||||||
|
var shelves = []
|
||||||
|
|
||||||
|
if (this.booksCurrentlyReading.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'recent',
|
||||||
|
label: 'Continue Reading',
|
||||||
|
books: this.booksCurrentlyReading
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.booksRecentlyAdded.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'added',
|
||||||
|
label: 'Recently Added',
|
||||||
|
books: this.booksRecentlyAdded
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.booksRead.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'read',
|
||||||
|
label: 'Read Again',
|
||||||
|
books: this.booksRead
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return shelves
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
this.settings = { ...this.$store.state.user.settings }
|
||||||
|
|
||||||
|
// var bookshelfView = await this.$localStore.getBookshelfView()
|
||||||
|
// this.isListView = bookshelfView === 'list'
|
||||||
|
// this.bookshelfReady = true
|
||||||
|
// console.log('Bookshelf view', bookshelfView)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
48
pages/bookshelf/library.vue
Normal file
48
pages/bookshelf/library.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<template v-for="(shelf, index) in shelves">
|
||||||
|
<bookshelf-library-shelf :key="shelf.id" :books="shelf.books" :style="{ zIndex: shelves.length - index }" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
booksPerRow: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
books() {
|
||||||
|
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||||
|
},
|
||||||
|
shelves() {
|
||||||
|
var shelves = []
|
||||||
|
var shelf = {
|
||||||
|
id: 0,
|
||||||
|
books: []
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.books.length; i++) {
|
||||||
|
var shelfNum = Math.floor((i + 1) / this.booksPerRow)
|
||||||
|
shelf.id = shelfNum
|
||||||
|
shelf.books.push(this.books[i])
|
||||||
|
|
||||||
|
if ((i + 1) % this.booksPerRow === 0) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
shelf = {
|
||||||
|
id: 0,
|
||||||
|
books: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shelf.books.length) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
}
|
||||||
|
return shelves
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
104
pages/bookshelf/series.vue
Normal file
104
pages/bookshelf/series.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<template v-for="(shelf, index) in shelves">
|
||||||
|
<bookshelf-group-shelf v-if="!selectedSeriesName" :key="shelf.id" group-type="series" :groups="shelf.groups" :style="{ zIndex: shelves.length - index }" />
|
||||||
|
<bookshelf-library-shelf v-else :key="shelf.id" :books="shelf.books" :style="{ zIndex: shelves.length - index }" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
groupsPerRow: 2,
|
||||||
|
booksPerRow: 3,
|
||||||
|
selectedSeriesName: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
routeQuery: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal && newVal.series) {
|
||||||
|
console.log('Select series')
|
||||||
|
this.selectedSeriesName = this.$decode(newVal.series)
|
||||||
|
} else {
|
||||||
|
this.selectedSeriesName = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
routeQuery() {
|
||||||
|
return this.$route.query
|
||||||
|
},
|
||||||
|
series() {
|
||||||
|
return this.$store.getters['audiobooks/getSeriesGroups']()
|
||||||
|
},
|
||||||
|
seriesShelves() {
|
||||||
|
var shelves = []
|
||||||
|
var shelf = {
|
||||||
|
id: 0,
|
||||||
|
groups: []
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.series.length; i++) {
|
||||||
|
var shelfNum = Math.floor((i + 1) / this.groupsPerRow)
|
||||||
|
shelf.id = shelfNum
|
||||||
|
shelf.groups.push(this.series[i])
|
||||||
|
|
||||||
|
if ((i + 1) % this.groupsPerRow === 0) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
shelf = {
|
||||||
|
id: 0,
|
||||||
|
groups: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shelf.groups.length) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
}
|
||||||
|
return shelves
|
||||||
|
},
|
||||||
|
selectedSeries() {
|
||||||
|
if (!this.selectedSeriesName) return null
|
||||||
|
return this.series.find((s) => s.name === this.selectedSeriesName)
|
||||||
|
},
|
||||||
|
seriesBooksShelves() {
|
||||||
|
if (!this.selectedSeries) return []
|
||||||
|
var seriesBooks = this.selectedSeries.books || []
|
||||||
|
|
||||||
|
var shelves = []
|
||||||
|
var shelf = {
|
||||||
|
id: 0,
|
||||||
|
books: []
|
||||||
|
}
|
||||||
|
for (let i = 0; i < seriesBooks.length; i++) {
|
||||||
|
var shelfNum = Math.floor((i + 1) / this.booksPerRow)
|
||||||
|
shelf.id = shelfNum
|
||||||
|
shelf.books.push(seriesBooks[i])
|
||||||
|
|
||||||
|
if ((i + 1) % this.booksPerRow === 0) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
shelf = {
|
||||||
|
id: 0,
|
||||||
|
books: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shelf.books.length) {
|
||||||
|
shelves.push(shelf)
|
||||||
|
}
|
||||||
|
return shelves
|
||||||
|
},
|
||||||
|
shelves() {
|
||||||
|
if (this.selectedSeries) {
|
||||||
|
return this.seriesBooksShelves
|
||||||
|
} else {
|
||||||
|
return this.seriesShelves
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
117
pages/collection/_id.vue
Normal file
117
pages/collection/_id.vue
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||||
|
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 240px">
|
||||||
|
<div class="relative" style="height: fit-content">
|
||||||
|
<cards-collection-cover :book-items="bookItems" :width="240" :height="120 * 1.6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="text-xl font-sans">
|
||||||
|
{{ collectionName }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2 w-20" @click="clickPlay">
|
||||||
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
|
{{ streaming ? 'Streaming' : 'Play' }}
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
|
|
||||||
|
<ui-icon-btn icon="delete" class="mx-0.5" @click="removeClick" /> -->
|
||||||
|
|
||||||
|
<div class="my-8 max-w-2xl">
|
||||||
|
<p class="text-base text-gray-100">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<tables-collection-books-table :books="bookItems" :collection-id="collection.id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, params, app, redirect, route }) {
|
||||||
|
if (!store.state.user.user) {
|
||||||
|
return redirect(`/connect?redirect=${route.path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var collection = await app.$axios.$get(`/api/collection/${params.id}`).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit('user/addUpdateCollection', collection)
|
||||||
|
collection.books.forEach((book) => {
|
||||||
|
store.commit('audiobooks/addUpdate', book)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
collectionId: collection.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processingRemove: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bookItems() {
|
||||||
|
return this.collection.books || []
|
||||||
|
},
|
||||||
|
collectionName() {
|
||||||
|
return this.collection.name || ''
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.collection.description || ''
|
||||||
|
},
|
||||||
|
collection() {
|
||||||
|
return this.$store.getters['user/getCollection'](this.collectionId)
|
||||||
|
},
|
||||||
|
playableBooks() {
|
||||||
|
return this.bookItems.filter((book) => {
|
||||||
|
return !book.isMissing && !book.isIncomplete && book.numTracks
|
||||||
|
})
|
||||||
|
},
|
||||||
|
streaming() {
|
||||||
|
return !!this.playableBooks.find((b) => b.id === this.$store.getters['getAudiobookIdStreaming'])
|
||||||
|
},
|
||||||
|
showPlayButton() {
|
||||||
|
return this.playableBooks.length
|
||||||
|
},
|
||||||
|
userAudiobooks() {
|
||||||
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickPlay() {
|
||||||
|
var nextBookNotRead = this.playableBooks.find((pb) => !this.userAudiobooks[pb.id] || !this.userAudiobooks[pb.id].isRead)
|
||||||
|
if (nextBookNotRead) {
|
||||||
|
var dlObj = this.$store.getters['downloads/getDownload'](nextBookNotRead.id)
|
||||||
|
|
||||||
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
|
if (dlObj && !dlObj.isDownloading && !dlObj.isPreparing) {
|
||||||
|
// Local
|
||||||
|
console.log('[PLAYCLICK] Set Playing Local Download ' + nextBookNotRead.book.title)
|
||||||
|
this.$store.commit('setPlayingDownload', dlObj)
|
||||||
|
} else {
|
||||||
|
// Stream
|
||||||
|
console.log('[PLAYCLICK] Set Playing STREAM ' + nextBookNotRead.book.title)
|
||||||
|
this.$store.commit('setStreamAudiobook', nextBookNotRead)
|
||||||
|
this.$server.socket.emit('open_stream', nextBookNotRead.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
16
pages/config.vue
Normal file
16
pages/config.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<p class="text-xl text-center py-8">Under Construction...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -133,7 +133,7 @@ export default {
|
||||||
if (this.$route.query && this.$route.query.redirect) {
|
if (this.$route.query && this.$route.query.redirect) {
|
||||||
this.$router.replace(this.$route.query.redirect)
|
this.$router.replace(this.$route.query.redirect)
|
||||||
} else {
|
} else {
|
||||||
this.$router.replace('/')
|
this.$router.replace('/bookshelf')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
socketConnected() {
|
socketConnected() {
|
||||||
|
|
239
pages/downloads.vue
Normal file
239
pages/downloads.vue
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full py-6">
|
||||||
|
<h1 class="text-2xl px-4">Downloads</h1>
|
||||||
|
|
||||||
|
<div class="w-full px-2 py-2" :class="hasStoragePermission ? '' : 'text-error'">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
||||||
|
<p v-if="hasStoragePermission" class="text-sm px-4" @click="changeDownloadFolderClick">{{ downloadFolderSimplePath || 'No Download Folder Selected' }}</p>
|
||||||
|
<p v-else class="text-sm px-4" @click="changeDownloadFolderClick">No Storage Permissions. Click here</p>
|
||||||
|
</div>
|
||||||
|
<!-- <p v-if="hasStoragePermission" class="text-xs text-gray-400 break-all max-w-full">{{ downloadFolderUri }}</p> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full h-10 relative">
|
||||||
|
<div class="absolute top-px left-0 z-10 w-full h-full flex">
|
||||||
|
<div class="flex-grow h-full bg-primary rounded-t-md mr-px" @click="showingDownloads = true">
|
||||||
|
<div class="flex items-center justify-center rounded-t-md border-t border-l border-r border-white border-opacity-20 h-full" :class="showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
|
||||||
|
<p>Downloads</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow h-full bg-primary rounded-t-md ml-px" @click="showingDownloads = false">
|
||||||
|
<div class="flex items-center justify-center h-full rounded-t-md border-t border-l border-r border-white border-opacity-20" :class="!showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
|
||||||
|
<p>Files</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-content-body relative w-full overflow-x-hidden bg-primary">
|
||||||
|
<template v-if="showingDownloads">
|
||||||
|
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||||
|
<p>No Downloads</p>
|
||||||
|
</div>
|
||||||
|
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
|
<template v-for="download in downloadsDownloading">
|
||||||
|
<li :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="w-3/4">
|
||||||
|
<span class="text-xs">({{ downloadingProgress[download.id] || 0 }}%) {{ download.isPreparing ? 'Preparing' : 'Downloading' }}...</span>
|
||||||
|
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<div class="shadow-sm text-white flex items-center justify-center rounded-full animate-spin">
|
||||||
|
<span class="material-icons">refresh</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template v-for="download in downloadsReady">
|
||||||
|
<li :key="download.id" class="text-gray-50 select-none relative pr-4 pl-2 py-5 border-b border-white border-opacity-10" @click="jumpToAudiobook(download)">
|
||||||
|
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<div class="w-full flex justify-around py-4 px-2">
|
||||||
|
<ui-btn small @click="searchFolder">Re-Scan</ui-btn>
|
||||||
|
<ui-btn small @click="changeDownloadFolderClick">Change Folder</ui-btn>
|
||||||
|
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
|
||||||
|
</div>
|
||||||
|
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
|
||||||
|
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
|
||||||
|
<template v-else>
|
||||||
|
<template v-for="mediaFolder in mediaScanResults.folders">
|
||||||
|
<div :key="mediaFolder.uri" class="w-full px-2 py-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="material-icons text-base text-white text-opacity-50">folder</span>
|
||||||
|
<p class="ml-1 py-0.5">{{ mediaFolder.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-for="mediaFile in mediaFolder.files" :key="mediaFile.uri" class="ml-3 flex items-center">
|
||||||
|
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||||
|
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-for="mediaFile in mediaScanResults.files">
|
||||||
|
<div :key="mediaFile.uri" class="w-full px-2 py-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||||
|
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
downloadingProgress: {},
|
||||||
|
totalSize: 0,
|
||||||
|
showingDownloads: true,
|
||||||
|
isScanning: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasStoragePermission() {
|
||||||
|
return this.$store.state.hasStoragePermission
|
||||||
|
},
|
||||||
|
downloadFolder() {
|
||||||
|
return this.$store.state.downloadFolder
|
||||||
|
},
|
||||||
|
downloadFolderSimplePath() {
|
||||||
|
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
||||||
|
},
|
||||||
|
downloadFolderUri() {
|
||||||
|
return this.downloadFolder ? this.downloadFolder.uri : null
|
||||||
|
},
|
||||||
|
totalDownloads() {
|
||||||
|
return this.downloadsReady.length + this.downloadsDownloading.length
|
||||||
|
},
|
||||||
|
downloadsDownloading() {
|
||||||
|
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
|
||||||
|
},
|
||||||
|
downloadsReady() {
|
||||||
|
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
|
||||||
|
},
|
||||||
|
downloads() {
|
||||||
|
return this.$store.state.downloads.downloads
|
||||||
|
},
|
||||||
|
mediaScanResults() {
|
||||||
|
return this.$store.state.mediaScanResults
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async changeDownloadFolderClick() {
|
||||||
|
if (!this.hasStoragePermission) {
|
||||||
|
console.log('Requesting Storage Permission')
|
||||||
|
StorageManager.requestStoragePermission()
|
||||||
|
} else {
|
||||||
|
var folderObj = await StorageManager.selectFolder()
|
||||||
|
if (folderObj.error) {
|
||||||
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
|
||||||
|
console.log('Storage Permission check folder ' + permissionsGood)
|
||||||
|
|
||||||
|
if (!permissionsGood) {
|
||||||
|
this.$toast.error('Folder permissions failed')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
this.$toast.success('Folder permission success')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$localStore.setDownloadFolder(folderObj)
|
||||||
|
|
||||||
|
this.searchFolder()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async searchFolder() {
|
||||||
|
this.isScanning = true
|
||||||
|
var response = await StorageManager.searchFolder({ folderUrl: this.downloadFolderUri })
|
||||||
|
var searchResults = response
|
||||||
|
searchResults.folders = JSON.parse(searchResults.folders)
|
||||||
|
searchResults.files = JSON.parse(searchResults.files)
|
||||||
|
|
||||||
|
if (searchResults.folders.length) {
|
||||||
|
console.log('Search results folders length', searchResults.folders.length)
|
||||||
|
|
||||||
|
searchResults.folders = searchResults.folders.map((sr) => {
|
||||||
|
if (sr.files) {
|
||||||
|
sr.files = JSON.parse(sr.files)
|
||||||
|
}
|
||||||
|
return sr
|
||||||
|
})
|
||||||
|
this.$store.commit('setMediaScanResults', searchResults)
|
||||||
|
} else {
|
||||||
|
this.$toast.warning('No audio or image files found')
|
||||||
|
}
|
||||||
|
this.isScanning = false
|
||||||
|
},
|
||||||
|
async resetFolder() {
|
||||||
|
await this.$localStore.setDownloadFolder(null)
|
||||||
|
this.$store.commit('setMediaScanResults', {})
|
||||||
|
this.$toast.info('Unlinked Folder')
|
||||||
|
},
|
||||||
|
jumpToAudiobook(download) {
|
||||||
|
this.show = false
|
||||||
|
this.$router.push(`/audiobook/${download.id}`)
|
||||||
|
},
|
||||||
|
async clickDeleteDownload(download) {
|
||||||
|
const { value } = await Dialog.confirm({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Delete this download?'
|
||||||
|
})
|
||||||
|
if (value) {
|
||||||
|
this.deleteDownload(download)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playDownload(download) {
|
||||||
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
|
this.$store.commit('setPlayingDownload', download)
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
async deleteDownload(download) {
|
||||||
|
console.log('Delete download', download.filename)
|
||||||
|
|
||||||
|
if (this.$store.state.playingDownload && this.$store.state.playingDownload.id === download.id) {
|
||||||
|
console.warn('Deleting download when currently playing download - terminate play')
|
||||||
|
if (this.$refs.streamContainer) {
|
||||||
|
this.$refs.streamContainer.cancelStream()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (download.contentUrl) {
|
||||||
|
await StorageManager.delete(download)
|
||||||
|
}
|
||||||
|
this.$store.commit('downloads/removeDownload', download)
|
||||||
|
},
|
||||||
|
onDownloadProgress(data) {
|
||||||
|
var progress = data.progress
|
||||||
|
var audiobookId = data.audiobookId
|
||||||
|
|
||||||
|
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||||
|
if (downloadObj) {
|
||||||
|
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
AudioDownloader.addListener('onDownloadProgress', this.onDownloadProgress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
AudioDownloader.removeListener('onDownloadProgress', this.onDownloadProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -27,6 +27,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ redirect }) {
|
||||||
|
return redirect('/bookshelf')
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showSortModal: false,
|
showSortModal: false,
|
||||||
|
|
BIN
static/fonts/MaterialIconsOutlined.woff2
Normal file
BIN
static/fonts/MaterialIconsOutlined.woff2
Normal file
Binary file not shown.
|
@ -30,7 +30,11 @@ export const getters = {
|
||||||
var filter = decode(filterBy.replace(`${group}.`, ''))
|
var filter = decode(filterBy.replace(`${group}.`, ''))
|
||||||
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
||||||
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
||||||
else if (group === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
else if (group === 'series') {
|
||||||
|
if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
|
||||||
|
else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||||
|
}
|
||||||
|
// else if (group === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
||||||
else if (group === 'progress') {
|
else if (group === 'progress') {
|
||||||
filtered = filtered.filter(ab => {
|
filtered = filtered.filter(ab => {
|
||||||
|
@ -67,6 +71,36 @@ export const getters = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getSeriesGroups: (state, getters, rootState) => () => {
|
||||||
|
var series = {}
|
||||||
|
state.audiobooks.forEach((audiobook) => {
|
||||||
|
if (audiobook.book && audiobook.book.series) {
|
||||||
|
if (series[audiobook.book.series]) {
|
||||||
|
var bookLastUpdate = audiobook.book.lastUpdate
|
||||||
|
if (bookLastUpdate > series[audiobook.book.series].lastUpdate) series[audiobook.book.series].lastUpdate = bookLastUpdate
|
||||||
|
series[audiobook.book.series].books.push(audiobook)
|
||||||
|
} else {
|
||||||
|
series[audiobook.book.series] = {
|
||||||
|
type: 'series',
|
||||||
|
name: audiobook.book.series || '',
|
||||||
|
books: [audiobook],
|
||||||
|
lastUpdate: audiobook.book.lastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var seriesArray = Object.values(series).map((_series) => {
|
||||||
|
_series.books = sort(_series.books)['asc']((ab) => {
|
||||||
|
return ab.book && ab.book.volumeNumber && !isNaN(ab.book.volumeNumber) ? Number(ab.book.volumeNumber) : null
|
||||||
|
})
|
||||||
|
return _series
|
||||||
|
})
|
||||||
|
if (state.keywordFilter) {
|
||||||
|
const keywordFilter = state.keywordFilter.toLowerCase()
|
||||||
|
return seriesArray.filter((_series) => _series.name.toLowerCase().includes(keywordFilter))
|
||||||
|
}
|
||||||
|
return seriesArray
|
||||||
|
},
|
||||||
getUniqueAuthors: (state) => {
|
getUniqueAuthors: (state) => {
|
||||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||||
return [...new Set(_authors)]
|
return [...new Set(_authors)]
|
||||||
|
|
|
@ -15,7 +15,8 @@ export const state = () => ({
|
||||||
selectedBook: null,
|
selectedBook: null,
|
||||||
showReader: false,
|
showReader: false,
|
||||||
downloadFolder: null,
|
downloadFolder: null,
|
||||||
mediaScanResults: {}
|
mediaScanResults: {},
|
||||||
|
showSideDrawer: false
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -27,6 +28,9 @@ export const getters = {
|
||||||
},
|
},
|
||||||
isAudiobookPlaying: (state) => id => {
|
isAudiobookPlaying: (state) => id => {
|
||||||
return (state.playingDownload && state.playingDownload.id === id) || (state.streamAudiobook && state.streamAudiobook.id === id)
|
return (state.playingDownload && state.playingDownload.id === id) || (state.streamAudiobook && state.streamAudiobook.id === id)
|
||||||
|
},
|
||||||
|
getAudiobookIdStreaming: state => {
|
||||||
|
return state.streamAudiobook ? state.streamAudiobook.id : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,5 +101,8 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
setMediaScanResults(state, val) {
|
setMediaScanResults(state, val) {
|
||||||
state.mediaScanResults = val
|
state.mediaScanResults = val
|
||||||
|
},
|
||||||
|
setShowSideDrawer(state, val) {
|
||||||
|
state.showSideDrawer = val
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -12,7 +12,9 @@ export const state = () => ({
|
||||||
bookshelfCoverSize: 120
|
bookshelfCoverSize: 120
|
||||||
},
|
},
|
||||||
settingsListeners: [],
|
settingsListeners: [],
|
||||||
userAudiobooksListeners: []
|
userAudiobooksListeners: [],
|
||||||
|
collections: [],
|
||||||
|
collectionsLoaded: false
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -38,6 +40,9 @@ export const getters = {
|
||||||
},
|
},
|
||||||
getFilterOrderKey: (state) => {
|
getFilterOrderKey: (state) => {
|
||||||
return Object.values(state.settings).join('-')
|
return Object.values(state.settings).join('-')
|
||||||
|
},
|
||||||
|
getCollection: state => id => {
|
||||||
|
return state.collections.find(c => c.id === id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +67,24 @@ export const actions = {
|
||||||
console.log('Update settings without server')
|
console.log('Update settings without server')
|
||||||
commit('setSettings', payload)
|
commit('setSettings', payload)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
loadUserCollections({ state, commit }) {
|
||||||
|
if (!this.$server.connected) {
|
||||||
|
console.error('Not loading collections - not connected')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (state.collectionsLoaded) {
|
||||||
|
console.log('Collections already loaded')
|
||||||
|
return state.collections
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$axios.$get('/api/collections').then((collections) => {
|
||||||
|
commit('setCollections', collections)
|
||||||
|
return collections
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to get collections', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,5 +143,17 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
removeUserAudiobookListener(state, listenerId) {
|
removeUserAudiobookListener(state, listenerId) {
|
||||||
state.userAudiobooksListeners = state.userAudiobooksListeners.filter(l => l.id !== listenerId)
|
state.userAudiobooksListeners = state.userAudiobooksListeners.filter(l => l.id !== listenerId)
|
||||||
}
|
},
|
||||||
|
setCollections(state, collections) {
|
||||||
|
state.collections = collections
|
||||||
|
state.collectionsLoaded = true
|
||||||
|
},
|
||||||
|
addUpdateCollection(state, collection) {
|
||||||
|
var index = state.collections.findIndex(c => c.id === collection.id)
|
||||||
|
if (index >= 0) {
|
||||||
|
state.collections.splice(index, 1, collection)
|
||||||
|
} else {
|
||||||
|
state.collections.push(collection)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
|
@ -15,8 +15,8 @@ module.exports = {
|
||||||
'short': { 'raw': '(max-height: 500px)' }
|
'short': { 'raw': '(max-height: 500px)' }
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
// bg: '#1e272e',
|
|
||||||
bg: '#373838',
|
bg: '#373838',
|
||||||
|
secondary: '#2F3030',
|
||||||
yellowgreen: 'yellowgreen',
|
yellowgreen: 'yellowgreen',
|
||||||
primary: '#262626',
|
primary: '#262626',
|
||||||
accent: '#1ad691',
|
accent: '#1ad691',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue