Compare commits

...
Author SHA1 Message Date
advplyr 1f60a552e5 Version bump 0.9.53-beta 2022-07-20 18:02:07 -05:00
advplyr 3b34029ca2 Fix:M4B downloads adding MP3 extension by downloading to SimpleStorage v0.14.0 #292 2022-07-20 17:38:13 -05:00
advplyr 5f6a1ef7e9 Update:Show error icon on player cover when local media progress fails to sync & remove sync failure toast 2022-07-19 18:50:14 -05:00
advplyr b51f65d2a4 Fix:Closing playback first closes media progress syncer #268 2022-07-19 18:09:55 -05:00
advplyr dc9526beb3 Fix:Edit server config showing duplicate config already exists 2022-07-19 17:32:49 -05:00
advplyr 7188089f63 Fix:Set server settings after user logs in 2022-07-19 16:40:50 -05:00
advplyr db1d6da491 Version bump 0.9.52-beta 2022-07-15 19:59:51 -05:00
advplyr 329a51f886 Example of adding custom action icon for android auto 2022-07-15 19:48:36 -05:00
advplyr 0c0badbae4 Fix:Android Auto use saved playback speed #229 2022-07-15 18:40:20 -05:00
advplyr 2da272c270 Fix:Android auto starting items from continue listening #167 2022-07-15 18:13:04 -05:00
advplyr 25ec5b3dcb Fix:ANR with starting foreground service before starting session 2022-07-15 17:46:33 -05:00
advplyr d9cdc16ff9 Fix:Catch api handler invalid json response body 2022-07-15 16:22:44 -05:00
advplyr 066b72d54d Fix:Media progress sync wait for api request to return before stopping 2022-07-15 16:10:42 -05:00
advplyr e7c913643a Add:Last local media progress sync with server widget, Update:Remove local media progress with bad id 2022-07-13 19:17:34 -05:00
advplyr 9847ed9fcb Fix network connection indicator 2022-07-13 17:13:15 -05:00
advplyr 721f812df0 Update:Library widget UI 2022-07-13 17:10:47 -05:00
advplyr a58965306a Fix misleading plugin function name 2022-07-13 16:44:02 -05:00
advplyrandGitHub 2b5377db04 Merge pull request #278 from Jnewbon/master
Added ability to Give write permission to the SD card
2022-07-11 14:57:38 -05:00
Jnewbon 7fe3849705 Re Added read Permission check as i probably shouldn't have removed it 2022-07-11 00:43:36 +01:00
Jnewbon f998deb725 Added Storage Media type Write access 2022-07-11 00:26:53 +01:00
advplyr cb2aaede67 Fix:Socket reconnection on disconnect, Add:Connection indicator icon showing socket/cellular 2022-07-07 17:24:26 -05:00
advplyr e07e7f70d6 Fix:Buffered progress bar for chapter track #250 2022-07-04 18:42:43 -05:00
advplyr 48313b60c1 iOS version bump 0.9.51 2022-07-04 16:56:04 -05:00
advplyr 3be13025a8 Fix:iOS swipe navigation to close modal 2022-07-04 16:55:34 -05:00
advplyr 505717e580 Version bump 0.9.51 2022-07-04 16:05:20 -05:00
advplyr b4bb6bf81b Fix:Back button to close modal #267 and back button to minimize fullscreen player #267 2022-07-04 15:42:59 -05:00
advplyr 6a8d901ce1 Update:Collections table row UI updates and include book duration #240 2022-07-04 14:59:56 -05:00
advplyr 68f3f0e276 Fix:Prevent crashes and catch exceptions for crash reports 2022-07-04 12:47:18 -05:00
advplyr ddf8e66a08 Fix:Remove server connection configs and prevent duplicate configs 2022-07-04 12:15:08 -05:00
advplyr 5b7a492a48 Update DB initializing to fix crashes for Paper db not initialized 2022-07-03 13:45:24 -05:00
46 changed files with 599 additions and 221 deletions
+3 -3
View File
@@ -29,8 +29,8 @@ android {
applicationId "com.audiobookshelf.app" applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 81 versionCode 84
versionName "0.9.50-beta" versionName "0.9.53-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.
@@ -111,7 +111,7 @@ dependencies {
implementation 'io.github.pilgr:paperdb:2.7.2' implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage // Simple Storage
implementation "com.anggrayudi:storage:0.13.0" implementation "com.anggrayudi:storage:0.14.0"
// OK HTTP // OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2' implementation 'com.squareup.okhttp3:okhttp:4.9.2'
@@ -13,12 +13,12 @@ import androidx.core.app.ActivityCompat
import com.anggrayudi.storage.SimpleStorage import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import com.audiobookshelf.app.data.AbsDatabase import com.audiobookshelf.app.data.AbsDatabase
import com.audiobookshelf.app.data.DbManager
import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.plugins.AbsAudioPlayer import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDownloader import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem import com.audiobookshelf.app.plugins.AbsFileSystem
import com.getcapacitor.BridgeActivity import com.getcapacitor.BridgeActivity
import io.paperdb.Paper
class MainActivity : BridgeActivity() { class MainActivity : BridgeActivity() {
@@ -53,14 +53,11 @@ class MainActivity : BridgeActivity() {
// .build()) // .build())
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(tag, "onCreate") Log.d(tag, "onCreate")
// Grant full storage access for testing DbManager.initialize(applicationContext)
// var ss = SimpleStorage(this)
// ss.requestFullStorageAccess()
var permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) { if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, ActivityCompat.requestPermissions(this,
PERMISSIONS_ALL, PERMISSIONS_ALL,
@@ -71,8 +68,6 @@ class MainActivity : BridgeActivity() {
registerPlugin(AbsDownloader::class.java) registerPlugin(AbsDownloader::class.java)
registerPlugin(AbsFileSystem::class.java) registerPlugin(AbsFileSystem::class.java)
registerPlugin(AbsDatabase::class.java) registerPlugin(AbsDatabase::class.java)
Paper.init(applicationContext)
} }
override fun onDestroy() { override fun onDestroy() {
@@ -97,9 +92,7 @@ class MainActivity : BridgeActivity() {
foregroundService = mLocalBinder.getService() foregroundService = mLocalBinder.getService()
// Let NativeAudio know foreground service is ready and setup event listener // Let NativeAudio know foreground service is ready and setup event listener
if (pluginCallback != null) { pluginCallback()
pluginCallback()
}
} }
} }
@@ -1,14 +1,25 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.content.Context
import android.util.Log import android.util.Log
import com.audiobookshelf.app.plugins.AbsDownloader import com.audiobookshelf.app.plugins.AbsDownloader
import io.paperdb.Paper import io.paperdb.Paper
import org.json.JSONObject
import java.io.File import java.io.File
class DbManager { class DbManager {
val tag = "DbManager" val tag = "DbManager"
companion object {
var isDbInitialized = false
fun initialize(ctx: Context) {
if (isDbInitialized) return
Paper.init(ctx)
isDbInitialized = true
Log.i("DbManager", "Initialized Paper db")
}
}
fun getDeviceData(): DeviceData { fun getDeviceData(): DeviceData {
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default()) return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default())
} }
@@ -34,7 +45,7 @@ class DbManager {
} }
} }
fun getLocalLibraryItemByLLId(libraryItemId:String):LocalLibraryItem? { fun getLocalLibraryItemByLId(libraryItemId:String):LocalLibraryItem? {
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId } return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
} }
@@ -202,7 +213,11 @@ class DbManager {
val localLibraryItems = getLocalLibraryItems() val localLibraryItems = getLocalLibraryItems()
localMediaProgress.forEach { localMediaProgress.forEach {
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId } val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (matchingLLI == null) { if (!it.id.startsWith("local")) {
// A bug on the server when syncing local media progress was replacing the media progress id causing duplicate progress. Remove them.
Log.d(tag, "cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)")
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI == null) {
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing") Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
Paper.book("localMediaProgress").delete(it.id) Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) { } else if (matchingLLI.isPodcast) {
@@ -2,6 +2,7 @@ package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaProgress( data class LocalMediaProgress(
@@ -22,6 +23,9 @@ data class LocalMediaProgress(
var libraryItemId:String?, var libraryItemId:String?,
var episodeId:String? var episodeId:String?
) { ) {
@get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
@JsonIgnore @JsonIgnore
fun updateIsFinished(finished:Boolean) { fun updateIsFinished(finished:Boolean) {
if (isFinished != finished) { // If finished changed then set progress if (isFinished != finished) { // If finished changed then set progress
@@ -1,24 +1,24 @@
package com.audiobookshelf.app.media package com.audiobookshelf.app.media
import android.app.Activity
import android.content.Context import android.content.Context
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.JSObject
import java.util.* import java.util.*
import io.paperdb.Paper
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.json.JSONException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager" val tag = "MediaManager"
var isPaperInitialized = false var serverLibraryItems = mutableListOf<LibraryItem>()
var serverLibraryItems = listOf<LibraryItem>()
var selectedLibraryId = "" var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null var selectedLibraryItemWrapper:LibraryItemWrapper? = null
@@ -29,18 +29,35 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
var serverLibraries = listOf<Library>() var serverLibraries = listOf<Library>()
var serverConfigIdUsed:String? = null var serverConfigIdUsed:String? = null
fun initializeAndroidAuto() { var userSettingsPlaybackRate:Float? = null
if (!isPaperInitialized) {
Log.d(tag, "Android Auto started when MainActivity was never started - initializing Paper")
Paper.init(ctx)
isPaperInitialized = true
}
}
fun getIsLibrary(id:String) : Boolean { fun getIsLibrary(id:String) : Boolean {
return serverLibraries.find { it.id == id } != null return serverLibraries.find { it.id == id } != null
} }
fun getSavedPlaybackRate():Float {
if (userSettingsPlaybackRate != null) {
return userSettingsPlaybackRate ?: 1f
}
val sharedPrefs = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
if (sharedPrefs != null) {
val userSettingsPref = sharedPrefs.getString("userSettings", null)
if (userSettingsPref != null) {
try {
val userSettings = JSObject(userSettingsPref)
if (userSettings.has("playbackRate")) {
userSettingsPlaybackRate = userSettings.getDouble("playbackRate").toFloat()
return userSettingsPlaybackRate ?: 1f
}
} catch(je:JSONException) {
Log.e(tag, "Failed to parse userSettings JSON ${je.localizedMessage}")
}
}
}
return 1f
}
fun checkResetServerItems() { fun checkResetServerItems() {
// When opening android auto need to check if still connected to server // When opening android auto need to check if still connected to server
// and reset any server data already set // and reset any server data already set
@@ -50,7 +67,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverPodcastEpisodes = listOf() serverPodcastEpisodes = listOf()
serverLibraryCategories = listOf() serverLibraryCategories = listOf()
serverLibraries = listOf() serverLibraries = listOf()
serverLibraryItems = listOf() serverLibraryItems = mutableListOf()
selectedLibraryId = "" selectedLibraryId = ""
} }
} }
@@ -74,7 +91,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() } val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
serverLibraryItems = libraryItemsWithAudio libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio) cb(libraryItemsWithAudio)
} }
} }
@@ -222,6 +243,17 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
// Only using book or podcast library categories for now // Only using book or podcast library categories for now
libraryCategories.forEach { libraryCategories.forEach {
// Add items in continue listening to serverLibraryItems
if (it.id == "continue-listening") {
it.entities.forEach { libraryItemWrapper ->
val libraryItem = libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
}
// Log.d(tag, "Found library category ${it.label} with type ${it.type}") // Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) { if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}") // Log.d(tag, "Using library category ${it.id}")
@@ -276,19 +308,18 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
} }
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) { fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
if (libraryItemWrapper is LocalLibraryItem) { if (libraryItemWrapper is LocalLibraryItem) {
val localLibraryItem = libraryItemWrapper as LocalLibraryItem cb(libraryItemWrapper.getPlaybackSession(episode))
cb(localLibraryItem.getPlaybackSession(episode)) } else {
} else { val libraryItem = libraryItemWrapper as LibraryItem
val libraryItem = libraryItemWrapper as LibraryItem apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "", playItemRequestPayload) {
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",playItemRequestPayload) { if (it == null) {
if (it == null) { cb(null)
cb(null) } else {
} else { cb(it)
cb(it) }
} }
} }
}
} }
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
@@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.AnyRes import androidx.annotation.AnyRes
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.Library
@@ -10,7 +10,6 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt
data class MediaProgressSyncData( data class MediaProgressSyncData(
var timeListened:Long, // seconds var timeListened:Long, // seconds
@@ -56,7 +55,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) { if (playerNotificationService.currentPlayer.isPlaying) {
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime) sync(currentTime) {
Log.d(tag, "Sync complete")
}
} }
} }
} }
@@ -67,8 +68,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle") Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime) sync(currentTime) {
reset() reset()
}
} }
fun syncFromServerProgress(mediaProgress: MediaProgress) { fun syncFromServerProgress(mediaProgress: MediaProgress) {
@@ -81,10 +83,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
} }
} }
fun sync(currentTime:Double) { fun sync(currentTime:Double, cb: () -> Unit) {
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) { if (diffSinceLastSync < 1000L) {
return return cb()
} }
val listeningTimeToAdd = diffSinceLastSync / 1000L val listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
@@ -92,6 +94,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
currentPlaybackSession?.syncData(syncData) currentPlaybackSession?.syncData(syncData)
if (currentPlaybackSession?.progress?.isNaN() == true) {
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
return cb()
}
if (currentIsLocal) { if (currentIsLocal) {
// Save local progress sync // Save local progress sync
currentPlaybackSession?.let { currentPlaybackSession?.let {
@@ -99,16 +107,29 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
saveLocalProgress(it) saveLocalProgress(it)
// Local library item is linked to a server library item // Local library item is linked to a server library item
if (!it.libraryItemId.isNullOrEmpty()) { // Send sync to server also if connected to this server and local item belongs to this server
// Send sync to server also if connected to this server and local item belongs to this server if (!it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { apiHandler.sendLocalProgressSync(it) { syncSuccess ->
apiHandler.sendLocalProgressSync(it) { Log.d(
Log.d( tag,
tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime" )
) if (syncSuccess) {
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
} }
cb()
} }
} else {
cb()
} }
} }
} else { } else {
@@ -116,14 +137,16 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
if (it) { if (it) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime") Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
failedSyncs = 0 failedSyncs = 0
playerNotificationService.alertSyncSuccess()
} else { } else {
failedSyncs++ failedSyncs++
if (failedSyncs == 2) { if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0 failedSyncs = 0
} }
Log.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime") Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
} }
cb()
} }
} }
} }
@@ -140,9 +163,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession) currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
} }
currentLocalMediaProgress?.let { currentLocalMediaProgress?.let {
DeviceManager.dbManager.saveLocalMediaProgress(it) if (it.progress.isNaN()) {
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) Log.e(tag, "Invalid progress on local media progress")
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%") } else {
DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
}
} }
} }
@@ -7,8 +7,10 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.LibraryItemWrapper import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode import com.audiobookshelf.app.data.PodcastEpisode
import java.util.* import java.util.*
@@ -28,8 +30,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null) playerNotificationService.preparePlayer(it,true, playbackRate)
} }
} }
} }
@@ -53,8 +56,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, true, null) playerNotificationService.preparePlayer(it, true, playbackRate)
} }
} }
} }
@@ -114,8 +118,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, true, null) playerNotificationService.preparePlayer(it, true, playbackRate)
} }
} }
} }
@@ -214,4 +219,12 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
} }
} }
// Example Using a custom action in android auto
// override fun onCustomAction(action: String?, extras: Bundle?) {
// super.onCustomAction(action, extras)
//
// if ("com.audiobookshelf.app.PLAYBACK_RATE" == action) {
//
// }
// }
} }
@@ -34,8 +34,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, playWhenReady, null) playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
} }
} }
} }
@@ -61,8 +62,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, playWhenReady, null) playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
} }
} }
} }
@@ -76,8 +78,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, playWhenReady, null) playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
} }
} }
} }
@@ -15,9 +15,12 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceInfo import com.audiobookshelf.app.data.DeviceInfo
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
@@ -55,6 +58,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onPlaybackFailed(errorMessage:String) fun onPlaybackFailed(errorMessage:String)
fun onMediaPlayerChanged(mediaPlayer:String) fun onMediaPlayerChanged(mediaPlayer:String)
fun onProgressSyncFailing() fun onProgressSyncFailing()
fun onProgressSyncSuccess()
} }
private val tag = "PlayerService" private val tag = "PlayerService"
@@ -65,7 +69,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var ctx:Context private lateinit var ctx:Context
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 lateinit var mediaSession: MediaSessionCompat
private lateinit var transportControls:MediaControllerCompat.TransportControls private lateinit var transportControls:MediaControllerCompat.TransportControls
lateinit var mediaManager: MediaManager lateinit var mediaManager: MediaManager
@@ -160,6 +164,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
super.onCreate() super.onCreate()
ctx = this ctx = this
DbManager.initialize(ctx)
// Initialize API // Initialize API
apiHandler = ApiHandler(ctx) apiHandler = ApiHandler(ctx)
@@ -224,6 +230,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector = MediaSessionConnector(mediaSession) mediaSessionConnector = MediaSessionConnector(mediaSession)
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) { val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat { override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
if (currentPlaybackSession == null) {
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
return MediaDescriptionCompat.Builder().build()
}
val coverUri = currentPlaybackSession!!.getCoverUri() val coverUri = currentPlaybackSession!!.getCoverUri()
// Fix for local images crashing on Android 11 for specific devices // Fix for local images crashing on Android 11 for specific devices
@@ -254,6 +265,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector.setQueueNavigator(queueNavigator) mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this)) mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
// Example adding custom action with icon in android auto
// mediaSessionConnector.setCustomActionProviders(object : MediaSessionConnector.CustomActionProvider {
// override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
// }
// override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
// var icon = R.drawable.exo_icon_rewind
// return PlaybackStateCompat.CustomAction.Builder(
// "com.audiobookshelf.app.PLAYBACK_RATE", "Playback Rate", icon)
// .build()
// }
// })
mediaSession.setCallback(MediaSessionCallback(this)) mediaSession.setCallback(MediaSessionCallback(this))
initializeMPlayer() initializeMPlayer()
@@ -293,6 +316,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
User callable methods User callable methods
*/ */
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) { fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) {
if (!isStarted) {
Log.i(tag, "preparePlayer: foreground service not started - Starting service --")
Intent(ctx, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(ctx, intent)
}
}
isClosed = false isClosed = false
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
initialPlaybackRate = playbackRate initialPlaybackRate = playbackRate
@@ -641,13 +671,24 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun closePlayback() { fun closePlayback() {
Log.d(tag, "closePlayback") Log.d(tag, "closePlayback")
currentPlayer.clearMediaItems() if (mediaProgressSyncer.listeningTimerRunning) {
currentPlayer.stop() Log.i(tag, "About to close playback so stopping media progress syncer first")
mediaProgressSyncer.stop()
}
try {
currentPlayer.stop()
currentPlayer.clearMediaItems()
} catch(e:Exception) {
Log.e(tag, "Exception clearing exoplayer $e")
}
currentPlaybackSession = null currentPlaybackSession = null
clientEventEmitter?.onPlaybackClosed() clientEventEmitter?.onPlaybackClosed()
PlayerListener.lastPauseTime = 0 PlayerListener.lastPauseTime = 0
isClosed = true isClosed = true
stopForeground(true) stopForeground(true)
stopSelf()
} }
fun sendClientMetadata(playerState: PlayerState) { fun sendClientMetadata(playerState: PlayerState) {
@@ -682,6 +723,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onProgressSyncFailing() clientEventEmitter?.onProgressSyncFailing()
} }
fun alertSyncSuccess() {
clientEventEmitter?.onProgressSyncSuccess()
}
// //
// MEDIA BROWSER STUFF (ANDROID AUTO) // MEDIA BROWSER STUFF (ANDROID AUTO)
// //
@@ -716,7 +761,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
null null
} else { } else {
isStarted = true isStarted = true
mediaManager.initializeAndroidAuto()
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
isAndroidAuto = true isAndroidAuto = true
@@ -1,10 +1,8 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
@@ -82,6 +80,10 @@ class AbsAudioPlayer : Plugin() {
override fun onProgressSyncFailing() { override fun onProgressSyncFailing() {
emit("onProgressSyncFailing", "") emit("onProgressSyncFailing", "")
} }
override fun onProgressSyncSuccess() {
emit("onProgressSyncSuccess", "")
}
}) })
} }
mainActivity.pluginCallback = foregroundServiceReady mainActivity.pluginCallback = foregroundServiceReady
@@ -150,14 +152,6 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod @PluginMethod
fun prepareLibraryItem(call: PluginCall) { fun prepareLibraryItem(call: PluginCall) {
// Need to make sure the player service has been started
if (!PlayerNotificationService.isStarted) {
Log.w(tag, "prepareLibraryItem: PlayerService not started - Starting foreground service --")
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(mainActivity, intent)
}
}
val libraryItemId = call.getString("libraryItemId", "").toString() val libraryItemId = call.getString("libraryItemId", "").toString()
val episodeId = call.getString("episodeId", "").toString() val episodeId = call.getString("episodeId", "").toString()
val playWhenReady = call.getBoolean("playWhenReady") == true val playWhenReady = call.getBoolean("playWhenReady") == true
@@ -77,10 +77,10 @@ class AbsDatabase : Plugin() {
} }
@PluginMethod @PluginMethod
fun getLocalLibraryItemByLLId(call:PluginCall) { fun getLocalLibraryItemByLId(call:PluginCall) {
val libraryItemId = call.getString("libraryItemId", "").toString() val libraryItemId = call.getString("libraryItemId", "").toString()
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLLId(libraryItemId) val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem == null) { if (localLibraryItem == null) {
call.resolve() call.resolve()
} else { } else {
@@ -401,9 +401,13 @@ class AbsDownloader : Plugin() {
Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}") Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}")
val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl)) val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl))
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE if (localFolderFile == null) {
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype) Log.e(tag, "Local Folder File from uri is null")
file?.moveFileTo(mainActivity,localFolderFile!!,fileDescription,fcb) } else {
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype)
file?.moveFileTo(mainActivity,localFolderFile,fileDescription,fcb)
}
} else { } else {
// Why is kotlin requiring an else here.. // Why is kotlin requiring an else here..
} }
@@ -1,5 +1,6 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.app.AlertDialog
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@@ -65,39 +66,59 @@ class AbsFileSystem : Plugin() {
@PluginMethod @PluginMethod
fun selectFolder(call: PluginCall) { fun selectFolder(call: PluginCall) {
var mediaType = call.data.getString("mediaType", "book").toString() val mediaType = call.data.getString("mediaType", "book").toString()
val REQUEST_CODE_SELECT_FOLDER = 6
val REQUEST_CODE_SDCARD_ACCESS = 7
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback { mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) { override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}") Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
var absolutePath = folder.getAbsolutePath(activity) val absolutePath = folder.getAbsolutePath(activity)
var storageType = folder.getStorageType(activity) val storageType = folder.getStorageType(activity)
var simplePath = folder.getSimplePath(activity) val simplePath = folder.getSimplePath(activity)
var basePath = folder.getBasePath(activity) val basePath = folder.getBasePath(activity)
var folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT) val folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
var localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType) val localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
DeviceManager.dbManager.saveLocalFolder(localFolder) DeviceManager.dbManager.saveLocalFolder(localFolder)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
} }
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) { override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
Log.e(TAG, "STORAGE ACCESS DENIED") val jsobj = JSObject()
var jsobj = JSObject() if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
jsobj.put("error", "Access Denied")
call.resolve(jsobj) val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
builder.setMessage(
"You have no write access to this storage, thus selecting this folder is useless." +
"\nWould you like to grant access to this folder?")
builder.setNegativeButton("Dont Allow") { _, _ ->
run {
jsobj.put("error", "User Canceled, Access Denied")
call.resolve(jsobj)
}
}
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, storageType) }
builder.show()
} else {
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
jsobj.put("error", "Access Denied")
call.resolve(jsobj)
}
} }
override fun onStoragePermissionDenied(requestCode: Int) { override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode") Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
var jsobj = JSObject() val jsobj = JSObject()
jsobj.put("error", "Permission Denied") jsobj.put("error", "Permission Denied")
call.resolve(jsobj) call.resolve(jsobj)
} }
} }
mainActivity.storage.openFolderPicker(6) mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
} }
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@@ -1,7 +1,6 @@
package com.audiobookshelf.app.server package com.audiobookshelf.app.server
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.util.Log import android.util.Log
@@ -17,6 +16,7 @@ import com.getcapacitor.JSObject
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -28,8 +28,6 @@ class ApiHandler(var ctx:Context) {
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build() private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
var storageSharedPreferences: SharedPreferences? = null
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>) data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>) data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
@@ -106,14 +104,21 @@ class ApiHandler(var ctx:Context) {
if (bodyString == "OK") { if (bodyString == "OK") {
cb(JSObject()) cb(JSObject())
} else { } else {
var jsonObj = JSObject() try {
if (bodyString.startsWith("[")) { var jsonObj = JSObject()
val array = JSArray(bodyString) if (bodyString.startsWith("[")) {
jsonObj.put("value", array) val array = JSArray(bodyString)
} else { jsonObj.put("value", array)
jsonObj = JSObject(bodyString) } else {
jsonObj = JSObject(bodyString)
}
cb(jsonObj)
} catch(je:JSONException) {
Log.e(tag, "Invalid JSON response ${je.localizedMessage} from body $bodyString")
val jsobj = JSObject()
jsobj.put("error", "Invalid response body")
cb(jsobj)
} }
cb(jsonObj)
} }
} }
} }
@@ -225,11 +230,15 @@ class ApiHandler(var ctx:Context) {
} }
} }
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) { fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession)) val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
postRequest("/api/session/local", payload) { postRequest("/api/session/local", payload) {
cb() if (!it.getString("error").isNullOrEmpty()) {
cb(false)
} else {
cb(true)
}
} }
} }
+8 -5
View File
@@ -8,25 +8,28 @@
<span class="material-icons text-3xl text-white">arrow_back</span> <span class="material-icons text-3xl text-white">arrow_back</span>
</a> </a>
<div v-if="user && currentLibrary"> <div v-if="user && currentLibrary">
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal"> <div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<widgets-library-icon :icon="currentLibraryIcon" :size="4" /> <widgets-library-icon :icon="currentLibraryIcon" :size="4" />
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p> <p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
</div> </div>
</div> </div>
<widgets-connection-indicator />
<div class="flex-grow" /> <div class="flex-grow" />
<widgets-download-progress-indicator /> <widgets-download-progress-indicator />
<!-- Must be connected to a server to cast, only supports media items on server --> <!-- Must be connected to a server to cast, only supports media items on server -->
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5"> <div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.6rem" @click="castClick">cast</span> <span class="material-icons text-2xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
</div> </div>
<nuxt-link v-if="user" class="h-7 mx-2" to="/search"> <nuxt-link v-if="user" class="h-7 mx-1.5" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span> <span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link> </nuxt-link>
<div class="h-7 mx-2"> <div class="h-7 mx-1.5">
<span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span> <span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
</div> </div>
</div> </div>
+29 -4
View File
@@ -34,6 +34,10 @@
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full"> <div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
<span class="material-icons text-error text-3xl">error</span>
</div>
</div> </div>
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor"> <div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
@@ -129,18 +133,22 @@ export default {
onPlaybackClosedListener: null, onPlaybackClosedListener: null,
onPlayingUpdateListener: null, onPlayingUpdateListener: null,
onMetadataListener: null, onMetadataListener: null,
onProgressSyncFailing: null,
onProgressSyncSuccess: null,
touchStartY: 0, touchStartY: 0,
touchStartTime: 0, touchStartTime: 0,
touchEndY: 0, touchEndY: 0,
useChapterTrack: false, useChapterTrack: false,
isLoading: false, isLoading: false,
touchTrackStart: false, touchTrackStart: false,
dragPercent: 0 dragPercent: 0,
syncStatus: 0
} }
}, },
watch: { watch: {
showFullscreen() { showFullscreen(val) {
this.updateScreenSize() this.updateScreenSize()
this.$store.commit('setPlayerFullscreen', !!val)
} }
}, },
computed: { computed: {
@@ -446,7 +454,7 @@ export default {
if (this.useChapterTrack && this.currentChapter) { if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start var currChapTime = this.currentTime - this.currentChapter.start
percentDone = currChapTime / this.currentChapterDuration percentDone = currChapTime / this.currentChapterDuration
bufferedPercent = (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
} }
var ptWidth = Math.round(percentDone * this.trackWidth) var ptWidth = Math.round(percentDone * this.trackWidth)
this.$refs.playedTrack.style.width = ptWidth + 'px' this.$refs.playedTrack.style.width = ptWidth + 'px'
@@ -522,7 +530,6 @@ export default {
var data = await AbsAudioPlayer.getCurrentTime() var data = await AbsAudioPlayer.getCurrentTime()
this.currentTime = Number(data.value.toFixed(2)) this.currentTime = Number(data.value.toFixed(2))
this.bufferedTime = Number(data.bufferedTime.toFixed(2)) this.bufferedTime = Number(data.bufferedTime.toFixed(2))
console.log('[AudioPlayer] Got Current Time', this.currentTime)
this.timeupdate() this.timeupdate()
}, 1000) }, 1000)
}, },
@@ -675,6 +682,7 @@ export default {
this.isEnded = false this.isEnded = false
this.isLoading = true this.isLoading = true
this.syncStatus = 0
this.$store.commit('setPlayerItem', this.playbackSession) this.$store.commit('setPlayerItem', this.playbackSession)
// Set track width // Set track width
@@ -703,6 +711,8 @@ export default {
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed) this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate) this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata) this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
}, },
screenOrientationChange() { screenOrientationChange() {
setTimeout(this.updateScreenSize, 50) setTimeout(this.updateScreenSize, 50)
@@ -712,6 +722,16 @@ export default {
var coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio var coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px') document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px') document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
},
minimizePlayerEvt() {
console.log('Minimize Player Evt')
this.showFullscreen = false
},
showProgressSyncIsFailing() {
this.syncStatus = this.$constants.SyncStatus.FAILED
},
showProgressSyncSuccess() {
this.syncStatus = this.$constants.SyncStatus.SUCCESS
} }
}, },
mounted() { mounted() {
@@ -719,6 +739,8 @@ export default {
if (screen.orientation) { if (screen.orientation) {
screen.orientation.addEventListener('change', this.screenOrientationChange) screen.orientation.addEventListener('change', this.screenOrientationChange)
} }
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
document.body.addEventListener('touchstart', this.touchstart) document.body.addEventListener('touchstart', this.touchstart)
document.body.addEventListener('touchend', this.touchend) document.body.addEventListener('touchend', this.touchend)
document.body.addEventListener('touchmove', this.touchmove) document.body.addEventListener('touchmove', this.touchmove)
@@ -735,6 +757,7 @@ export default {
} }
this.forceCloseDropdownMenu() this.forceCloseDropdownMenu()
this.$eventBus.$off('minimize-player', this.minimizePlayerEvt)
document.body.removeEventListener('touchstart', this.touchstart) document.body.removeEventListener('touchstart', this.touchstart)
document.body.removeEventListener('touchend', this.touchend) document.body.removeEventListener('touchend', this.touchend)
document.body.removeEventListener('touchmove', this.touchmove) document.body.removeEventListener('touchmove', this.touchmove)
@@ -744,6 +767,8 @@ export default {
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove() if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove() if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove() if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
clearInterval(this.playInterval) clearInterval(this.playInterval)
} }
} }
+1 -9
View File
@@ -30,11 +30,9 @@ export default {
onSleepTimerEndedListener: null, onSleepTimerEndedListener: null,
onSleepTimerSetListener: null, onSleepTimerSetListener: null,
onMediaPlayerChangedListener: null, onMediaPlayerChangedListener: null,
onProgressSyncFailing: null,
sleepInterval: null, sleepInterval: null,
currentEndOfChapterTime: 0, currentEndOfChapterTime: 0,
serverLibraryItemId: null, serverLibraryItemId: null
syncFailedToast: null
} }
}, },
watch: { watch: {
@@ -255,10 +253,6 @@ export default {
onMediaPlayerChanged(data) { onMediaPlayerChanged(data) {
var mediaPlayer = data.value var mediaPlayer = data.value
this.$store.commit('setMediaPlayer', mediaPlayer) this.$store.commit('setMediaPlayer', mediaPlayer)
},
showProgressSyncIsFailing() {
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
this.syncFailedToast = this.$toast('Progress is not being synced', { timeout: false, type: 'error' })
} }
}, },
mounted() { mounted() {
@@ -266,7 +260,6 @@ export default {
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded) this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet) this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged) this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate') this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`) console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
@@ -283,7 +276,6 @@ export default {
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove() if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove() if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove() if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
// if (this.$server.socket) { // if (this.$server.socket) {
// this.$server.socket.off('stream_open', this.streamOpen) // this.$server.socket.off('stream_open', this.streamOpen)
+1 -1
View File
@@ -19,7 +19,7 @@
</div> </div>
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300"> <div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center"> <div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
<p class="text-xs" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p> <p class="text-xs text-gray-400" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p class="text-xs">{{ $config.version }}</p> <p class="text-xs">{{ $config.version }}</p>
+23 -12
View File
@@ -16,8 +16,11 @@
</template> </template>
<div v-else class="w-full"> <div v-else class="w-full">
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full"> <form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<div v-if="serverConnectionConfigs.length" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
</div>
<h2 class="text-lg leading-7 mb-2">Server address</h2> <h2 class="text-lg leading-7 mb-2">Server address</h2>
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" /> <ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || !!serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" />
<div class="flex justify-end items-center mt-6"> <div class="flex justify-end items-center mt-6">
<!-- <div class="relative flex"> <!-- <div class="relative flex">
<button class="outline-none uppercase tracking-wide font-semibold text-xs text-gray-300" type="button" @click="addCustomHeaders">Add Custom Headers</button> <button class="outline-none uppercase tracking-wide font-semibold text-xs text-gray-300" type="button" @click="addCustomHeaders">Add Custom Headers</button>
@@ -76,12 +79,6 @@
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
export default { export default {
props: {
deviceData: {
type: Object,
default: () => {}
}
},
data() { data() {
return { return {
loggedIn: false, loggedIn: false,
@@ -99,6 +96,9 @@ export default {
} }
}, },
computed: { computed: {
deviceData() {
return this.$store.state.deviceData || {}
},
networkConnected() { networkConnected() {
return this.$store.state.networkConnected return this.$store.state.networkConnected
}, },
@@ -152,7 +152,7 @@ export default {
var payload = await this.authenticateToken() var payload = await this.authenticateToken()
if (payload) { if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId) this.setUserAndConnection(payload)
} else { } else {
this.showAuth = true this.showAuth = true
} }
@@ -167,7 +167,10 @@ export default {
if (value) { if (value) {
this.processing = true this.processing = true
await this.$db.removeServerConnectionConfig(this.serverConfig.id) await this.$db.removeServerConnectionConfig(this.serverConfig.id)
this.deviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id) const updatedDeviceData = { ...this.deviceData }
updatedDeviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id)
this.$store.commit('setDeviceData', updatedDeviceData)
this.serverConfig = { this.serverConfig = {
address: null, address: null,
userId: null, userId: null,
@@ -185,7 +188,6 @@ export default {
} }
this.showForm = true this.showForm = true
this.showAuth = true this.showAuth = true
console.log('Edit server config', serverConfig)
}, },
newServerConfigClick() { newServerConfigClick() {
this.serverConfig = { this.serverConfig = {
@@ -274,20 +276,29 @@ export default {
this.error = 'Invalid username' this.error = 'Invalid username'
return return
} }
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === this.serverConfig.username && this.serverConfig.id !== scc.id)
if (duplicateConfig) {
this.error = 'Config already exists for this address and username'
return
}
this.error = null this.error = null
this.processing = true this.processing = true
var payload = await this.requestServerLogin() var payload = await this.requestServerLogin()
this.processing = false this.processing = false
if (payload) { if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId) this.setUserAndConnection(payload)
} }
}, },
async setUserAndConnection(user, userDefaultLibraryId) { async setUserAndConnection({ user, userDefaultLibraryId, serverSettings }) {
if (!user) return if (!user) return
console.log('Successfully logged in', JSON.stringify(user)) console.log('Successfully logged in', JSON.stringify(user))
this.$store.commit('setServerSettings', serverSettings)
// Set library - Use last library if set and available fallback to default user library // Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId() var lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) { if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
+12
View File
@@ -77,6 +77,8 @@ export default {
} }
}, },
setShow() { setShow() {
this.$store.commit('globals/setIsModalOpen', true)
document.body.appendChild(this.el) document.body.appendChild(this.el)
setTimeout(() => { setTimeout(() => {
this.content.style.transform = 'scale(1)' this.content.style.transform = 'scale(1)'
@@ -84,18 +86,28 @@ export default {
document.documentElement.classList.add('modal-open') document.documentElement.classList.add('modal-open')
}, },
setHide() { setHide() {
this.$store.commit('globals/setIsModalOpen', false)
this.content.style.transform = 'scale(0)' this.content.style.transform = 'scale(0)'
this.el.remove() this.el.remove()
document.documentElement.classList.remove('modal-open') document.documentElement.classList.remove('modal-open')
},
closeModalEvt() {
console.log('Close modal event')
this.show = false
} }
}, },
mounted() { mounted() {
this.$eventBus.$on('close-modal', this.closeModalEvt)
this.el = this.$refs.wrapper this.el = this.$refs.wrapper
this.content = this.$refs.content this.content = this.$refs.content
this.content.style.transform = 'scale(0)' this.content.style.transform = 'scale(0)'
this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)' this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)'
this.el.style.opacity = 1 this.el.style.opacity = 1
this.el.remove() this.el.remove()
},
beforeDestroy() {
this.$eventBus.$off('close-modal', this.closeModalEvt)
} }
} }
</script> </script>
+15 -7
View File
@@ -1,13 +1,14 @@
<template> <template>
<div class="w-full px-2 py-2 overflow-hidden relative"> <div class="w-full px-2 py-2 overflow-hidden relative">
<div v-if="book" class="flex h-20"> <div v-if="book" class="flex w-full">
<div class="h-full relative" :style="{ width: bookWidth + 'px' }"> <div class="h-full relative" :style="{ width: bookWidth + 'px' }">
<covers-book-cover :library-item="book" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="book" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div class="w-80 h-full px-2 flex items-center"> <div class="flex-grow book-table-content h-full px-2 flex items-center">
<div> <div class="max-w-full">
<nuxt-link :to="`/item/${book.id}`" class="truncate text-sm">{{ bookTitle }}</nuxt-link> <nuxt-link :to="`/item/${book.id}`" class="truncate block text-sm">{{ bookTitle }}</nuxt-link>
<p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p> <p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p>
<p class="text-xxs text-gray-500">{{ bookDuration }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -46,13 +47,13 @@ export default {
return this.mediaMetadata.authorName || '' return this.mediaMetadata.authorName || ''
}, },
bookDuration() { bookDuration() {
return this.$secondsToTimestamp(this.media.duration) return this.$elapsedPretty(this.media.duration)
}, },
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
}, },
bookWidth() { bookWidth() {
if (this.bookCoverAspectRatio === 1) return 80 if (this.bookCoverAspectRatio === 1) return 50
return 50 return 50
}, },
isMissing() { isMissing() {
@@ -78,4 +79,11 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
<style>
.book-table-content {
width: calc(100% - 50px);
max-width: calc(100% - 50px);
}
</style>
@@ -0,0 +1,71 @@
<template>
<div v-if="icon" class="flex h-full items-center px-2">
<span class="material-icons-outlined text-lg" :class="iconClass" @click="showAlertDialog">{{ icon }}</span>
</div>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
export default {
data() {
return {}
},
computed: {
user() {
return this.$store.state.user.user
},
socketConnected() {
return this.$store.state.socketConnected
},
networkConnected() {
return this.$store.state.networkConnected
},
networkConnectionType() {
return this.$store.state.networkConnectionType
},
isCellular() {
return this.networkConnectionType === 'cellular'
},
icon() {
if (!this.user) return null // hide when not connected to server
if (!this.networkConnected) {
return 'wifi_off'
} else if (!this.socketConnected) {
return 'cloud_off'
} else if (this.isCellular) {
return 'signal_cellular_alt'
} else {
return 'cloud_done'
}
},
iconClass() {
if (!this.networkConnected) return 'text-error'
else if (!this.socketConnected) return 'text-warning'
else if (this.isCellular) return 'text-gray-200'
else return 'text-success'
}
},
methods: {
showAlertDialog() {
var msg = ''
if (!this.networkConnected) {
msg = 'No internet'
} else if (!this.socketConnected) {
msg = 'Socket not connected'
} else if (this.isCellular) {
msg = 'Socket connected over cellular'
} else {
msg = 'Socket connected over wifi'
}
Dialog.alert({
title: 'Connection Status',
message: msg
})
}
},
mounted() {},
beforeDestroy() {}
}
</script>
+4 -4
View File
@@ -479,12 +479,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10; CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.50; MARKETING_VERSION = 0.9.51;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -503,12 +503,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10; CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.50; MARKETING_VERSION = 0.9.51;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+2 -1
View File
@@ -23,8 +23,9 @@ public class AbsAudioPlayer: CAPPlugin {
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
self.bridge?.webView?.allowsBackForwardNavigationGestures = true; self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
} }
@objc func prepareLibraryItem(_ call: CAPPluginCall) { @objc func prepareLibraryItem(_ call: CAPPluginCall) {
let libraryItemId = call.getString("libraryItemId") let libraryItemId = call.getString("libraryItemId")
let episodeId = call.getString("episodeId") let episodeId = call.getString("episodeId")
+1 -1
View File
@@ -17,7 +17,7 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemByLLId, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
) )
+1 -1
View File
@@ -80,7 +80,7 @@ public class AbsDatabase: CAPPlugin {
@objc func getLocalLibraryItem(_ call: CAPPluginCall) { @objc func getLocalLibraryItem(_ call: CAPPluginCall) {
call.resolve() call.resolve()
} }
@objc func getLocalLibraryItemByLLId(_ call: CAPPluginCall) { @objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
call.resolve() call.resolve()
} }
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) { @objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
+6 -6
View File
@@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog' pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage' pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
end end
target 'App' do target 'App' do
+10 -15
View File
@@ -66,9 +66,6 @@ export default {
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
isSocketConnected() {
return this.$store.state.socketConnected
} }
}, },
methods: { methods: {
@@ -154,15 +151,9 @@ export default {
// Only cancels stream if streamining not playing downloaded // Only cancels stream if streamining not playing downloaded
this.$eventBus.$emit('close-stream') this.$eventBus.$emit('close-stream')
}, },
socketConnectionUpdate(isConnected) {
console.log('Socket connection update', isConnected)
},
socketConnectionFailed(err) { socketConnectionFailed(err) {
this.$toast.error('Socket connection error: ' + err.message) this.$toast.error('Socket connection error: ' + err.message)
}, },
socketInit(data) {
console.log('Socket init', data)
},
async initLibraries() { async initLibraries() {
if (this.inittingLibraries) { if (this.inittingLibraries) {
return return
@@ -177,6 +168,7 @@ export default {
async syncLocalMediaProgress() { async syncLocalMediaProgress() {
if (!this.user) { if (!this.user) {
console.log('[default] No need to sync local media progress - not connected to server') console.log('[default] No need to sync local media progress - not connected to server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return return
} }
@@ -184,10 +176,15 @@ export default {
var response = await this.$db.syncLocalMediaProgressWithServer() var response = await this.$db.syncLocalMediaProgressWithServer()
if (!response) { if (!response) {
if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server') if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return return
} }
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
if (numLocalMediaProgressForServer > 0) { if (numLocalMediaProgressForServer > 0) {
response.syncedAt = Date.now()
response.serverConfigName = this.$store.getters['user/getServerConfigName']
this.$store.commit('setLastLocalMediaSyncResults', response)
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) { if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`) console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
} else { } else {
@@ -195,6 +192,7 @@ export default {
} }
} else { } else {
console.log('[default] syncLocalMediaProgress No local media progress to sync') console.log('[default] syncLocalMediaProgress No local media progress to sync')
this.$store.commit('setLastLocalMediaSyncResults', null)
} }
}, },
async userUpdated(user) { async userUpdated(user) {
@@ -216,9 +214,10 @@ export default {
mediaProgress: prog mediaProgress: prog
} }
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload) newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
} else { } else if (!localProg) {
// Check if local library item exists // Check if local library item exists
var localLibraryItem = await this.$db.getLocalLibraryItemByLLId(prog.libraryItemId) // local media progress may not exist yet if it hasn't been played
var localLibraryItem = await this.$db.getLocalLibraryItemByLId(prog.libraryItemId)
if (localLibraryItem) { if (localLibraryItem) {
if (prog.episodeId) { if (prog.episodeId) {
// If episode check if local episode exists // If episode check if local episode exists
@@ -253,8 +252,6 @@ export default {
} }
}, },
async mounted() { async mounted() {
this.$socket.on('connection-update', this.socketConnectionUpdate)
this.$socket.on('initialized', this.socketInit)
this.$socket.on('user_updated', this.userUpdated) this.$socket.on('user_updated', this.userUpdated)
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated) this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
@@ -282,8 +279,6 @@ export default {
} }
}, },
beforeDestroy() { beforeDestroy() {
this.$socket.off('connection-update', this.socketConnectionUpdate)
this.$socket.off('initialized', this.socketInit)
this.$socket.off('user_updated', this.userUpdated) this.$socket.off('user_updated', this.userUpdated)
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated) this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.50-beta", "version": "0.9.53-beta",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.50-beta", "version": "0.9.53-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337", "dev": "nuxt --hostname 0.0.0.0 --port 1337",
-2
View File
@@ -183,12 +183,10 @@ export default {
}) })
}, },
initListeners() { initListeners() {
// this.$server.on('initialized', this.socketInit)
this.$eventBus.$on('library-changed', this.libraryChanged) this.$eventBus.$on('library-changed', this.libraryChanged)
// this.$eventBus.$on('downloads-loaded', this.downloadsLoaded) // this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
}, },
removeListeners() { removeListeners() {
// this.$server.off('initialized', this.socketInit)
this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('library-changed', this.libraryChanged)
// this.$eventBus.$off('downloads-loaded', this.downloadsLoaded) // this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
} }
+3 -3
View File
@@ -6,8 +6,8 @@
<covers-collection-cover :book-items="bookItems" :width="240" :height="120 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-collection-cover :book-items="bookItems" :width="240" :height="120 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
</div> </div>
<div class="flex-grow px-2 py-6 md:py-0 md:px-10"> <div class="flex-grow py-6">
<div class="flex items-center"> <div class="flex items-center px-2">
<h1 class="text-xl font-sans"> <h1 class="text-xl font-sans">
{{ collectionName }} {{ collectionName }}
</h1> </h1>
@@ -18,7 +18,7 @@
</ui-btn> </ui-btn>
</div> </div>
<div class="my-8 max-w-2xl"> <div class="my-8 max-w-2xl px-2">
<p class="text-base text-gray-100">{{ description }}</p> <p class="text-base text-gray-100">{{ description }}</p>
</div> </div>
+1 -1
View File
@@ -12,7 +12,7 @@
<!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> --> <!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
<connection-server-connect-form v-if="deviceData" :device-data="deviceData" /> <connection-server-connect-form v-if="deviceData" />
</div> </div>
<div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0"> <div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0">
+1 -1
View File
@@ -136,7 +136,7 @@ export default {
}) })
// Check if // Check if
if (libraryItem) { if (libraryItem) {
var localLibraryItem = await app.$db.getLocalLibraryItemByLLId(libraryItemId) var localLibraryItem = await app.$db.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem) { if (localLibraryItem) {
console.log('Library item has local library item also', localLibraryItem.id) console.log('Library item has local library item also', localLibraryItem.id)
libraryItem.localLibraryItem = localLibraryItem libraryItem.localLibraryItem = localLibraryItem
+45 -2
View File
@@ -1,5 +1,28 @@
<template> <template>
<div class="w-full h-full py-6"> <div class="w-full h-full py-6">
<div v-if="lastLocalMediaSyncResults" class="px-2 mb-4">
<div class="w-full pl-2 pr-2 py-2 bg-black bg-opacity-25 rounded-lg relative">
<div class="flex items-center mb-1">
<span class="material-icons text-success text-xl">sync</span>
<p class="text-sm text-gray-300 pl-2">Local media progress synced with server</p>
</div>
<div class="flex justify-between mb-1.5">
<p class="text-xs text-gray-400 font-semibold">{{ syncedServerConfigName }}</p>
<p class="text-xs text-gray-400 italic">{{ $dateDistanceFromNow(syncedAt) }}</p>
</div>
<div v-if="!numLocalProgressUpdates && !numServerProgressUpdates">
<p class="text-sm text-gray-300">Local media progress was up-to-date with server ({{ numLocalMediaSynced }} item{{ numLocalMediaSynced == 1 ? '' : 's' }})</p>
</div>
<div v-else>
<p v-if="numServerProgressUpdates" class="text-sm text-gray-300">- {{ numServerProgressUpdates }} local media item{{ numServerProgressUpdates === 1 ? '' : 's' }} progress was updated on the server (local more recent).</p>
<p v-else class="text-sm text-gray-300">- No local media progress had to be synced on the server.</p>
<p v-if="numLocalProgressUpdates" class="text-sm text-gray-300">- {{ numLocalProgressUpdates }} local media item{{ numLocalProgressUpdates === 1 ? '' : 's' }} progress was updated to match the server (server more recent).</p>
<p v-else class="text-sm text-gray-300">- No server progress had to be synced with local media progress.</p>
</div>
</div>
</div>
<h1 class="text-base font-semibold px-3 mb-2">Local Folders</h1> <h1 class="text-base font-semibold px-3 mb-2">Local Folders</h1>
<div v-if="!isIos" class="w-full max-w-full px-3 py-2"> <div v-if="!isIos" class="w-full max-w-full px-3 py-2">
@@ -49,8 +72,28 @@ export default {
isIos() { isIos() {
return this.$platform === 'ios' return this.$platform === 'ios'
}, },
isSocketConnected() { lastLocalMediaSyncResults() {
return this.$store.state.socketConnected return this.$store.state.lastLocalMediaSyncResults
},
numLocalMediaSynced() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numLocalMediaProgressForServer || 0
},
syncedAt() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.syncedAt || 0
},
syncedServerConfigName() {
if (!this.lastLocalMediaSyncResults) return ''
return this.lastLocalMediaSyncResults.serverConfigName
},
numLocalProgressUpdates() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numLocalProgressUpdates || 0
},
numServerProgressUpdates() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numServerProgressUpdates || 0
} }
}, },
methods: { methods: {
+5
View File
@@ -131,6 +131,11 @@ class AbsAudioPlayerWeb extends WebPlugin {
} }
} }
// PluginMethod
async getIsCastAvailable() {
return false
}
initializePlayer() { initializePlayer() {
if (document.getElementById('audio-player')) { if (document.getElementById('audio-player')) {
document.getElementById('audio-player').remove() document.getElementById('audio-player').remove()
+2 -2
View File
@@ -52,7 +52,7 @@ class AbsDatabaseWeb extends WebPlugin {
async removeServerConnectionConfig(serverConnectionConfigCallObject) { async removeServerConnectionConfig(serverConnectionConfigCallObject) {
var serverConnectionConfigId = serverConnectionConfigCallObject.serverConnectionConfigId var serverConnectionConfigId = serverConnectionConfigCallObject.serverConnectionConfigId
var deviceData = await this.getDeviceData() var deviceData = await this.getDeviceData()
deviceData.serverConnectionConfigs = deviceData.serverConnectionConfigs.filter(ssc => ssc.id == serverConnectionConfigId) deviceData.serverConnectionConfigs = deviceData.serverConnectionConfigs.filter(ssc => ssc.id != serverConnectionConfigId)
localStorage.setItem('device', JSON.stringify(deviceData)) localStorage.setItem('device', JSON.stringify(deviceData))
} }
@@ -164,7 +164,7 @@ class AbsDatabaseWeb extends WebPlugin {
async getLocalLibraryItem({ id }) { async getLocalLibraryItem({ id }) {
return this.getLocalLibraryItems().then((data) => data.value[0]) return this.getLocalLibraryItems().then((data) => data.value[0])
} }
async getLocalLibraryItemByLLId({ libraryItemId }) { async getLocalLibraryItemByLId({ libraryItemId }) {
return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId)) return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId))
} }
async getAllLocalMediaProgress() { async getAllLocalMediaProgress() {
+7
View File
@@ -5,6 +5,12 @@ const DownloadStatus = {
FAILED: 3 FAILED: 3
} }
const SyncStatus = {
UNSET: 0,
SUCCESS: 1,
FAILED: 2
}
const CoverDestination = { const CoverDestination = {
METADATA: 0, METADATA: 0,
AUDIOBOOK: 1 AUDIOBOOK: 1
@@ -31,6 +37,7 @@ const PlayerState = {
const Constants = { const Constants = {
DownloadStatus, DownloadStatus,
SyncStatus,
CoverDestination, CoverDestination,
BookCoverAspectRatio, BookCoverAspectRatio,
PlayMethod, PlayMethod,
+2 -2
View File
@@ -54,8 +54,8 @@ class DbService {
return AbsDatabase.getLocalLibraryItem({ id }) return AbsDatabase.getLocalLibraryItem({ id })
} }
getLocalLibraryItemByLLId(libraryItemId) { getLocalLibraryItemByLId(libraryItemId) {
return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId }) return AbsDatabase.getLocalLibraryItemByLId({ libraryItemId })
} }
getAllLocalMediaProgress() { getAllLocalMediaProgress() {
+46 -19
View File
@@ -14,21 +14,6 @@ if (Capacitor.getPlatform() != 'web') {
setStatusBarStyleDark() setStatusBarStyleDark()
} }
App.addListener('backButton', async ({ canGoBack }) => {
if (!canGoBack) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: `Did you want to exit the app?`,
})
if (value) {
App.exitApp()
}
} else {
window.history.back()
}
})
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production' Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
Vue.prototype.$dateDistanceFromNow = (unixms) => { Vue.prototype.$dateDistanceFromNow = (unixms) => {
@@ -52,17 +37,20 @@ Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
} }
Vue.prototype.$elapsedPretty = (seconds) => { Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
if (seconds < 60) {
return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}`
}
var minutes = Math.floor(seconds / 60) var minutes = Math.floor(seconds / 60)
if (minutes < 70) { if (minutes < 70) {
return `${minutes} min` return `${minutes} min${useFullNames ? `ute${minutes === 1 ? '' : 's'}` : ''}`
} }
var hours = Math.floor(minutes / 60) var hours = Math.floor(minutes / 60)
minutes -= hours * 60 minutes -= hours * 60
if (!minutes) { if (!minutes) {
return `${hours} hr` return `${hours} ${useFullNames ? 'hours' : 'hr'}`
} }
return `${hours} hr ${minutes} min` return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
} }
Vue.prototype.$secondsToTimestamp = (seconds) => { Vue.prototype.$secondsToTimestamp = (seconds) => {
@@ -132,6 +120,45 @@ Vue.prototype.$encode = encode
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString() const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
Vue.prototype.$decode = decode Vue.prototype.$decode = decode
export default ({ store, app }) => {
// iOS Only
// backButton event does not work with iOS swipe navigation so use this workaround
if (app.router && Capacitor.getPlatform() === 'ios') {
app.router.beforeEach((to, from, next) => {
if (store.state.globals.isModalOpen) {
Vue.prototype.$eventBus.$emit('close-modal')
}
if (store.state.playerIsFullscreen) {
Vue.prototype.$eventBus.$emit('minimize-player')
}
next()
})
}
// Android only
App.addListener('backButton', async ({ canGoBack }) => {
if (store.state.globals.isModalOpen) {
Vue.prototype.$eventBus.$emit('close-modal')
return
}
if (store.state.playerIsFullscreen) {
Vue.prototype.$eventBus.$emit('minimize-player')
return
}
if (!canGoBack) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: `Did you want to exit the app?`,
})
if (value) {
App.exitApp()
}
} else {
window.history.back()
}
})
}
export { export {
encode, encode,
decode decode
+9 -8
View File
@@ -39,6 +39,7 @@ class ServerSocket extends EventEmitter {
logout() { logout() {
if (this.socket) this.socket.disconnect() if (this.socket) this.socket.disconnect()
this.removeListeners()
} }
setSocketListeners() { setSocketListeners() {
@@ -54,6 +55,14 @@ class ServerSocket extends EventEmitter {
// }) // })
} }
removeListeners() {
if (!this.socket) return
this.socket.removeAllListeners()
if (this.socket.io && this.socket.io.removeAllListeners) {
this.socket.io.removeAllListeners()
}
}
onConnect() { onConnect() {
console.log('[SOCKET] Socket Connected ' + this.socket.id) console.log('[SOCKET] Socket Connected ' + this.socket.id)
this.connected = true this.connected = true
@@ -67,18 +76,10 @@ class ServerSocket extends EventEmitter {
this.connected = false this.connected = false
this.$store.commit('setSocketConnected', false) this.$store.commit('setSocketConnected', false)
this.emit('connection-update', false) this.emit('connection-update', false)
this.socket.removeAllListeners()
if (this.socket.io && this.socket.io.removeAllListeners) {
this.socket.io.removeAllListeners()
}
} }
onInit(data) { onInit(data) {
console.log('[SOCKET] Initial socket data received', data) console.log('[SOCKET] Initial socket data received', data)
if (data.serverSettings) {
this.$store.commit('setServerSettings', data.serverSettings)
}
this.emit('initialized', true) this.emit('initialized', true)
} }
+4
View File
@@ -1,4 +1,5 @@
export const state = () => ({ export const state = () => ({
isModalOpen: false,
itemDownloads: [], itemDownloads: [],
bookshelfListView: false, bookshelfListView: false,
series: null, series: null,
@@ -93,6 +94,9 @@ export const actions = {
} }
export const mutations = { export const mutations = {
setIsModalOpen(state, val) {
state.isModalOpen = val
},
addUpdateItemDownload(state, downloadItem) { addUpdateItemDownload(state, downloadItem) {
var index = state.itemDownloads.findIndex(i => i.id == downloadItem.id) var index = state.itemDownloads.findIndex(i => i.id == downloadItem.id)
if (index >= 0) { if (index >= 0) {
+9 -1
View File
@@ -6,6 +6,7 @@ export const state = () => ({
playerEpisodeId: null, playerEpisodeId: null,
playerIsLocal: false, playerIsLocal: false,
playerIsPlaying: false, playerIsPlaying: false,
playerIsFullscreen: false,
isCasting: false, isCasting: false,
isCastAvailable: false, isCastAvailable: false,
socketConnected: false, socketConnected: false,
@@ -18,7 +19,8 @@ export const state = () => ({
showSideDrawer: false, showSideDrawer: false,
isNetworkListenerInit: false, isNetworkListenerInit: false,
serverSettings: null, serverSettings: null,
lastBookshelfScrollData: {} lastBookshelfScrollData: {},
lastLocalMediaSyncResults: null
}) })
export const getters = { export const getters = {
@@ -93,6 +95,9 @@ export const mutations = {
setPlayerPlaying(state, val) { setPlayerPlaying(state, val) {
state.playerIsPlaying = val state.playerIsPlaying = val
}, },
setPlayerFullscreen(state, val) {
state.playerIsFullscreen = val
},
setHasStoragePermission(state, val) { setHasStoragePermission(state, val) {
state.hasStoragePermission = val state.hasStoragePermission = val
}, },
@@ -122,5 +127,8 @@ export const mutations = {
setServerSettings(state, val) { setServerSettings(state, val) {
state.serverSettings = val state.serverSettings = val
this.$localStore.setServerSettings(state.serverSettings) this.$localStore.setServerSettings(state.serverSettings)
},
setLastLocalMediaSyncResults(state, val) {
state.lastLocalMediaSyncResults = val
} }
} }
+3
View File
@@ -22,6 +22,9 @@ export const getters = {
getServerAddress: (state) => { getServerAddress: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null return state.serverConnectionConfig ? state.serverConnectionConfig.address : null
}, },
getServerConfigName: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.name : null
},
getCustomHeaders: (state) => { getCustomHeaders: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null
}, },
+6
View File
@@ -33,6 +33,12 @@ module.exports = {
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans], sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono], mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
book: ['Gentium Book Basic', 'serif'] book: ['Gentium Book Basic', 'serif']
},
fontSize: {
xxs: '0.625rem'
},
maxWidth: {
'24': '6rem'
} }
} }
}, },