Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16da0c909f | ||
|
|
1a555eab63 | ||
|
|
4cdcbf79d7 | ||
|
|
c07b527a1d | ||
|
|
a2031f1d88 | ||
|
|
42fec4831e | ||
|
|
9da21b57c7 | ||
|
|
2d7b539e90 | ||
|
|
4dbaf3c864 | ||
|
|
e6e7ad8266 | ||
|
|
080a08aebc | ||
|
|
8d4831f912 | ||
|
|
72732025ae | ||
|
|
b0c316d2f0 | ||
|
|
f7a2393d99 | ||
|
|
6229a7f0a8 | ||
|
|
a0768da143 | ||
|
|
04b453060d | ||
|
|
4bf75c606a |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 55
|
||||
versionName "0.9.35-beta"
|
||||
versionCode 58
|
||||
versionName "0.9.38-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -13,6 +13,7 @@ dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-dialog')
|
||||
implementation project(':capacitor-network')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capacitor-storage')
|
||||
implementation project(':robingenz-capacitor-app-update')
|
||||
implementation project(':capacitor-data-storage-sqlite')
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
"pkg": "@capacitor/network",
|
||||
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/status-bar",
|
||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/storage",
|
||||
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.audiobookshelf.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
@@ -33,6 +34,8 @@ class AudiobookManager {
|
||||
var audiobooks:MutableList<Audiobook> = mutableListOf()
|
||||
var audiobooksInProgress:MutableList<Audiobook> = mutableListOf()
|
||||
|
||||
var storageSharedPreferences: SharedPreferences? = null
|
||||
|
||||
constructor(_ctx:Context, _client:OkHttpClient) {
|
||||
ctx = _ctx
|
||||
client = _client
|
||||
@@ -41,13 +44,27 @@ class AudiobookManager {
|
||||
}
|
||||
|
||||
fun init() {
|
||||
var sharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
||||
serverUrl = sharedPreferences.getString("serverUrl", "").toString()
|
||||
storageSharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
||||
serverUrl = storageSharedPreferences?.getString("serverUrl", "").toString()
|
||||
Log.d(tag, "SHARED PREF SERVERURL $serverUrl")
|
||||
token = sharedPreferences.getString("token", "").toString()
|
||||
token = storageSharedPreferences?.getString("token", "").toString()
|
||||
Log.d(tag, "SHARED PREF TOKEN $token")
|
||||
}
|
||||
|
||||
fun getPlaybackRate() : Float {
|
||||
if (storageSharedPreferences != null) {
|
||||
var userSettings = storageSharedPreferences?.getString("userSettings", "").toString()
|
||||
if (userSettings != "") {
|
||||
var json = JSObject(userSettings)
|
||||
var playbackRate = json.getString("playbackRate", "1")
|
||||
if (playbackRate != null) {
|
||||
return playbackRate.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1f
|
||||
}
|
||||
|
||||
fun loadCategories(cb: (() -> Unit)) {
|
||||
Log.d(tag, "LOAD Categories $serverUrl | $token")
|
||||
var url = "$serverUrl/api/libraries/main/categories"
|
||||
@@ -214,6 +231,8 @@ class AudiobookManager {
|
||||
response.use {
|
||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
||||
|
||||
var playbackRate = getPlaybackRate()
|
||||
|
||||
var bodyString = response.body!!.string()
|
||||
var stream = JSObject(bodyString)
|
||||
var streamId = stream.getString("streamId", "").toString()
|
||||
@@ -232,7 +251,7 @@ class AudiobookManager {
|
||||
abStreamDataObj.put("cover", audiobook.getCover())
|
||||
abStreamDataObj.put("duration", audiobook.getDurationLong())
|
||||
abStreamDataObj.put("startTime", startTimeLong)
|
||||
abStreamDataObj.put("playbackSpeed", 1)
|
||||
abStreamDataObj.put("playbackSpeed", playbackRate)
|
||||
abStreamDataObj.put("playWhenReady", true)
|
||||
abStreamDataObj.put("isLocal", false)
|
||||
|
||||
@@ -250,6 +269,8 @@ class AudiobookManager {
|
||||
}
|
||||
|
||||
fun initDownloadPlay(audiobook:Audiobook):AudiobookStreamData {
|
||||
var playbackRate = getPlaybackRate()
|
||||
|
||||
var abStreamDataObj = JSObject()
|
||||
abStreamDataObj.put("id", "download")
|
||||
abStreamDataObj.put("audiobookId", audiobook.id)
|
||||
@@ -260,7 +281,7 @@ class AudiobookManager {
|
||||
abStreamDataObj.put("cover", audiobook.getCover())
|
||||
abStreamDataObj.put("duration", audiobook.getDurationLong())
|
||||
abStreamDataObj.put("startTime", 0)
|
||||
abStreamDataObj.put("playbackSpeed", 1)
|
||||
abStreamDataObj.put("playbackSpeed", playbackRate)
|
||||
abStreamDataObj.put("playWhenReady", true)
|
||||
abStreamDataObj.put("isLocal", true)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotifi
|
||||
var listeningTimerRunning:Boolean = false
|
||||
|
||||
private var webviewOpenOnStart:Boolean = false
|
||||
private var webviewClosedMidSession:Boolean = false
|
||||
private var listeningBookTitle:String? = ""
|
||||
private var listeningBookIsLocal:Boolean = false
|
||||
private var listeningBookId:String? = ""
|
||||
@@ -54,9 +55,16 @@ class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotifi
|
||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
// Webview was closed while android auto is open - switch to native sync
|
||||
if (!playerNotificationService.getIsWebviewOpen() && webviewOpenOnStart) {
|
||||
var isWebviewOpen = playerNotificationService.getIsWebviewOpen()
|
||||
if (!isWebviewOpen && webviewOpenOnStart) {
|
||||
Log.d(tag, "Listening Timer: webview closed Switching to native sync tracking")
|
||||
webviewOpenOnStart = false
|
||||
webviewClosedMidSession = true
|
||||
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||
} else if (isWebviewOpen && webviewClosedMidSession) {
|
||||
Log.d(tag, "Listening Timer: webview re-opened Switching back to webview sync tracking")
|
||||
webviewClosedMidSession = false
|
||||
webviewOpenOnStart = true
|
||||
lastUpdateTime = System.currentTimeMillis() / 1000L
|
||||
}
|
||||
if (!webviewOpenOnStart && playerNotificationService.currentPlayer.isPlaying) {
|
||||
@@ -70,8 +78,10 @@ class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotifi
|
||||
if (!listeningTimerRunning) return
|
||||
Log.d(tag, "stop: Stopping listening for $listeningBookTitle")
|
||||
|
||||
if (!webviewOpenOnStart) {
|
||||
sync()
|
||||
}
|
||||
reset()
|
||||
sync()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
@@ -95,7 +105,7 @@ class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotifi
|
||||
// Send sync data only for streaming books
|
||||
var syncData: JSObject = JSObject()
|
||||
syncData.put("timeListened", elapsed)
|
||||
syncData.put("currentTime", playerNotificationService.getCurrentTime())
|
||||
syncData.put("currentTime", playerNotificationService.getCurrentTime() / 1000)
|
||||
syncData.put("streamId", listeningStreamId)
|
||||
syncData.put("audiobookId", listeningBookId)
|
||||
sendStreamSyncData(syncData) {
|
||||
@@ -104,9 +114,36 @@ class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotifi
|
||||
} else if (listeningStreamId == "download") {
|
||||
// TODO: Save downloaded audiobook progress & send to server if connected
|
||||
Log.d(tag, "ListeningTimer: Is listening download")
|
||||
|
||||
// Send sync data only for local books
|
||||
var syncData: JSObject = JSObject()
|
||||
var duration = playerNotificationService.getAudiobookDuration() / 1000
|
||||
var currentTime = playerNotificationService.getCurrentTime() / 1000
|
||||
syncData.put("totalDuration", duration)
|
||||
syncData.put("currentTime", currentTime)
|
||||
syncData.put("progress", if (duration > 0) (currentTime / duration) else 0)
|
||||
syncData.put("isRead", false)
|
||||
syncData.put("lastUpdate", System.currentTimeMillis())
|
||||
syncData.put("audiobookId", listeningBookId)
|
||||
sendLocalSyncData(syncData) {
|
||||
Log.d(tag, "Local sync done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendLocalSyncData(payload:JSObject, cb: (() -> Unit)) {
|
||||
var serverUrl = playerNotificationService.getServerUrl()
|
||||
var token = playerNotificationService.getUserToken()
|
||||
|
||||
if (serverUrl == "" || token == "") {
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(tag, "Sync Local $serverUrl | $token")
|
||||
var url = "$serverUrl/api/syncLocal"
|
||||
sendServerRequest(url, token, payload, cb)
|
||||
}
|
||||
|
||||
fun sendStreamSyncData(payload:JSObject, cb: (() -> Unit)) {
|
||||
var serverUrl = playerNotificationService.getServerUrl()
|
||||
var token = playerNotificationService.getUserToken()
|
||||
@@ -117,7 +154,10 @@ class AudiobookProgressSyncer constructor(playerNotificationService:PlayerNotifi
|
||||
|
||||
Log.d(tag, "Sync Stream $serverUrl | $token")
|
||||
var url = "$serverUrl/api/syncStream"
|
||||
sendServerRequest(url, token, payload, cb)
|
||||
}
|
||||
|
||||
fun sendServerRequest(url:String, token:String, payload:JSObject, cb: () -> Unit) {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = payload.toString().toRequestBody(mediaType)
|
||||
val request = Request.Builder().post(requestBody)
|
||||
|
||||
@@ -89,6 +89,11 @@ class AudiobookStreamData {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCover() {
|
||||
coverUri = Uri.EMPTY
|
||||
cover = ""
|
||||
}
|
||||
|
||||
fun getMediaMetadataCompat():MediaMetadataCompat {
|
||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
@@ -99,10 +104,10 @@ class AudiobookStreamData {
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, series)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
|
||||
if (cover != "") {
|
||||
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, cover)
|
||||
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, cover)
|
||||
}
|
||||
// if (cover != "") {
|
||||
// metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, cover)
|
||||
// metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, cover)
|
||||
// }
|
||||
return metadataBuilder.build()
|
||||
}
|
||||
|
||||
@@ -114,9 +119,9 @@ class AudiobookStreamData {
|
||||
.setAlbumArtist(author)
|
||||
.setSubtitle(author)
|
||||
|
||||
if (coverUri != Uri.EMPTY) {
|
||||
metadataBuilder.setArtworkUri(coverUri)
|
||||
}
|
||||
// if (coverUri != Uri.EMPTY) {
|
||||
// metadataBuilder.setArtworkUri(coverUri)
|
||||
// }
|
||||
if (playlistUri != Uri.EMPTY) {
|
||||
metadataBuilder.setMediaUri(playlistUri)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.anggrayudi.storage.file.isExternalStorageDocument
|
||||
import com.getcapacitor.Bridge
|
||||
import com.getcapacitor.JSObject
|
||||
import com.google.android.exoplayer2.*
|
||||
@@ -646,8 +648,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
Log.d(tag, "Playing ${getCurrentBookTitle()} | ${currentPlayer.mediaMetadata.title} | ${currentPlayer.mediaMetadata.displayTitle}")
|
||||
if (player.isPlaying) {
|
||||
audiobookProgressSyncer.start()
|
||||
}
|
||||
if (!player.isPlaying && audiobookProgressSyncer.listeningTimerRunning) {
|
||||
} else {
|
||||
audiobookProgressSyncer.stop()
|
||||
}
|
||||
|
||||
@@ -670,6 +671,33 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
Log.d(tag, "Init Player audiobook already playing")
|
||||
}
|
||||
|
||||
// Issue with onenote plus crashing when using local cover art. https://github.com/advplyr/audiobookshelf-app/issues/35
|
||||
if (currentAudiobookStreamData?.coverUri != null && currentAudiobookStreamData?.isLocal == true) {
|
||||
var deviceName = Build.DEVICE
|
||||
var deviceMan = Build.MANUFACTURER
|
||||
var deviceModel = Build.MODEL
|
||||
Log.d(tag, "Checking device $deviceName | Model $deviceModel | Manufacturer $deviceMan")
|
||||
if (deviceMan.toLowerCase().contains("oneplus") || deviceName.toLowerCase().contains("oneplus")) {
|
||||
Log.d(tag, "Detected OnePlus device - removing local cover")
|
||||
currentAudiobookStreamData?.clearCover()
|
||||
}
|
||||
|
||||
// OnePlus devices were showing valid permissions for image
|
||||
// try {
|
||||
// Log.d(tag, "CHECKING COVER ${currentAudiobookStreamData?.coverUri}")
|
||||
// var file = DocumentFile.fromTreeUri(ctx, currentAudiobookStreamData!!.coverUri)
|
||||
// Log.d(tag, "GOT FILE ${file?.name} | ${file?.type} | Can Read: ${file?.canRead()} |isExternalStorageDocument: ${file?.isExternalStorageDocument}")
|
||||
// if (file?.canRead() !== true) {
|
||||
// Log.d(tag, "Invalid cover: no read access")
|
||||
// currentAudiobookStreamData?.clearCover()
|
||||
// }
|
||||
// } catch(e:Exception) {
|
||||
// Log.d(tag, "Invalid cover: Failed to read local cover file $e")
|
||||
// currentAudiobookStreamData?.clearCover()
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
}
|
||||
|
||||
var metadata = currentAudiobookStreamData!!.getMediaMetadataCompat()
|
||||
mediaSession.setMetadata(metadata)
|
||||
|
||||
@@ -755,6 +783,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
return currentAudiobookStreamData?.id
|
||||
}
|
||||
|
||||
// The duration stored on the audiobook
|
||||
fun getAudiobookDuration() : Long {
|
||||
if (currentAudiobookStreamData == null) return 0L
|
||||
return currentAudiobookStreamData!!.duration
|
||||
}
|
||||
|
||||
fun getServerUrl(): String {
|
||||
return audiobookManager.serverUrl
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/d
|
||||
include ':capacitor-network'
|
||||
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
||||
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':capacitor-storage'
|
||||
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
||||
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
@import "./fonts.css";
|
||||
|
||||
body {
|
||||
background-color: #262626;
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
height: calc(100vh - env(safe-area-inset-top));
|
||||
min-height: calc(100vh - env(safe-area-inset-top));
|
||||
max-height: calc(100vh - env(safe-area-inset-top));
|
||||
margin-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
#content {
|
||||
height: calc(100% - 64px);
|
||||
min-height: calc(100% - 64px);
|
||||
max-height: calc(100% - 64px);
|
||||
}
|
||||
#content.playerOpen {
|
||||
height: calc(100% - 164px);
|
||||
min-height: calc(100% - 164px);
|
||||
max-height: calc(100% - 164px);
|
||||
}
|
||||
|
||||
#bookshelf {
|
||||
min-height: calc(100vh - 48px);
|
||||
height: calc(100% - 48px);
|
||||
min-height: calc(100% - 48px);
|
||||
}
|
||||
|
||||
.box-shadow-sm {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="fixed top-0 bottom-0 left-0 right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
|
||||
<div class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
|
||||
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
||||
<div class="top-2 left-4 absolute cursor-pointer">
|
||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class="material-icons text-3xl" @click="castClick">cast</span>
|
||||
</div>
|
||||
<div class="top-4 right-4 absolute cursor-pointer">
|
||||
<ui-dropdown-menu :items="menuItems" @action="clickMenuAction">
|
||||
<ui-dropdown-menu ref="dropdownMenu" :items="menuItems" @action="clickMenuAction">
|
||||
<span class="material-icons text-3xl">more_vert</span>
|
||||
</ui-dropdown-menu>
|
||||
</div>
|
||||
@@ -185,15 +185,20 @@ export default {
|
||||
return this.book.authorFL
|
||||
},
|
||||
chapters() {
|
||||
return this.audiobook ? this.audiobook.chapters || [] : []
|
||||
return (this.audiobook ? this.audiobook.chapters || [] : []).map((chapter) => {
|
||||
var chap = { ...chapter }
|
||||
chap.start = Number(chap.start)
|
||||
chap.end = Number(chap.end)
|
||||
return chap
|
||||
})
|
||||
},
|
||||
currentChapter() {
|
||||
if (!this.audiobook || !this.chapters.length) return null
|
||||
return this.chapters.find((ch) => Number(ch.start.toFixed(2)) <= this.currentTime && Number(ch.end.toFixed(2)) > this.currentTime)
|
||||
return this.chapters.find((ch) => Number(Number(ch.start).toFixed(2)) <= this.currentTime && Number(Number(ch.end).toFixed(2)) > this.currentTime)
|
||||
},
|
||||
nextChapter() {
|
||||
if (!this.chapters.length) return
|
||||
return this.chapters.find((c) => Number(c.start.toFixed(2)) > this.currentTime)
|
||||
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
|
||||
},
|
||||
currentChapterTitle() {
|
||||
return this.currentChapter ? this.currentChapter.title : ''
|
||||
@@ -321,6 +326,7 @@ export default {
|
||||
},
|
||||
collapseFullscreen() {
|
||||
this.showFullscreen = false
|
||||
this.forceCloseDropdownMenu()
|
||||
},
|
||||
jumpNextChapter() {
|
||||
if (this.loading) return
|
||||
@@ -335,7 +341,7 @@ export default {
|
||||
|
||||
// If 1 second or less into current chapter, then go to previous
|
||||
if (this.currentTime - this.currentChapter.start <= 1) {
|
||||
var currChapterIndex = this.chapters.findIndex((ch) => ch.start <= this.currentTime && ch.end >= this.currentTime)
|
||||
var currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
|
||||
if (currChapterIndex > 0) {
|
||||
var prevChapter = this.chapters[currChapterIndex - 1]
|
||||
this.seek(prevChapter.start)
|
||||
@@ -524,6 +530,7 @@ export default {
|
||||
this.streamId = stream ? stream.id : null
|
||||
this.audiobookId = audiobookStreamData.audiobookId
|
||||
this.initObject = { ...audiobookStreamData }
|
||||
console.log('[AudioPlayer] Set Audio Player', !!stream)
|
||||
|
||||
var init = true
|
||||
if (!!stream) {
|
||||
@@ -612,6 +619,7 @@ export default {
|
||||
var data = await MyNativeAudio.getCurrentTime()
|
||||
this.currentTime = Number((data.value / 1000).toFixed(2))
|
||||
this.bufferedTime = Number((data.bufferedTime / 1000).toFixed(2))
|
||||
console.log('[AudioPlayer] Got Current Time', this.currentTime)
|
||||
this.timeupdate()
|
||||
}, 1000)
|
||||
},
|
||||
@@ -704,6 +712,11 @@ export default {
|
||||
} else if (action === 'close') {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
forceCloseDropdownMenu() {
|
||||
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
||||
this.$refs.dropdownMenu.closeMenu()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -713,6 +726,7 @@ export default {
|
||||
this.$nextTick(this.init)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.forceCloseDropdownMenu()
|
||||
document.body.removeEventListener('touchstart', this.touchstart)
|
||||
document.body.removeEventListener('touchend', this.touchend)
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
},
|
||||
currentChapter() {
|
||||
if (!this.audiobook || !this.chapters.length) return null
|
||||
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
||||
return this.chapters.find((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) > this.currentTime)
|
||||
},
|
||||
socketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
@@ -378,7 +378,7 @@ export default {
|
||||
console.log('[StreamContainer] Stream Open: ' + this.title)
|
||||
|
||||
if (!this.$refs.audioPlayer) {
|
||||
console.error('No Audio Player Mini')
|
||||
console.error('[StreamContainer] No Audio Player Mini')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -406,7 +406,12 @@ export default {
|
||||
audiobookId: this.audiobookId,
|
||||
tracks: this.tracksForCast
|
||||
}
|
||||
|
||||
console.log('[StreamContainer] Set Audio Player', JSON.stringify(audiobookStreamData))
|
||||
if (!this.$refs.audioPlayer) {
|
||||
console.error('[StreamContainer] Invalid no audio player')
|
||||
} else {
|
||||
console.log('[StreamContainer] Has Audio Player Ref')
|
||||
}
|
||||
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)
|
||||
|
||||
this.stream = stream
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="fixed top-0 left-0 right-0 bottom-0 w-full h-full z-50 overflow-hidden pointer-events-none">
|
||||
<div class="fixed top-0 left-0 right-0 layout-wrapper w-full z-50 overflow-hidden pointer-events-none">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
|
||||
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
|
||||
<div class="px-6 mb-4">
|
||||
<p v-if="socketConnected" class="text-base">
|
||||
Welcome, <strong>{{ username }}</strong>
|
||||
Welcome,
|
||||
<strong>{{ username }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full overflow-y-auto">
|
||||
|
||||
@@ -90,8 +90,9 @@ export default {
|
||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
||||
},
|
||||
bookWidth() {
|
||||
// var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||
var coverSize = 100
|
||||
if (window.innerWidth <= 375) coverSize = 90
|
||||
|
||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
||||
return coverSize
|
||||
},
|
||||
@@ -318,7 +319,6 @@ export default {
|
||||
if (this.isFirstInit) return
|
||||
this.isFirstInit = true
|
||||
this.initSizeData()
|
||||
|
||||
await this.loadPage(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-20 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" style="top: 72px">
|
||||
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
||||
<p class="text-xs">
|
||||
<strong>{{ key }}</strong
|
||||
>: {{ comicMetadata[key] }}
|
||||
<strong>{{ key }}</strong>
|
||||
: {{ comicMetadata[key] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,10 +210,10 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
#comic-reader {
|
||||
height: calc(100vh - 32px);
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
.pagemenu {
|
||||
max-height: calc(100vh - 80px);
|
||||
max-height: calc(100% - 80px);
|
||||
}
|
||||
.comicimg {
|
||||
height: 100%;
|
||||
|
||||
@@ -91,10 +91,15 @@ export default {
|
||||
this.closeMenu()
|
||||
},
|
||||
clickedOption(itemValue) {
|
||||
this.$emit('action', itemValue)
|
||||
this.closeMenu()
|
||||
this.$emit('action', itemValue)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
mounted() {},
|
||||
beforeDestroy() {
|
||||
if (this.menu) {
|
||||
this.menu.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -357,9 +357,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.35;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -378,9 +381,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.35;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 774 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,116 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "40-2.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "58-1.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29x29@2x-1.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "80-1.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"filename" : "120-1.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"filename" : "40-1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-20x20@2x-1.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"filename" : "58.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"filename" : "80.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40x40@2x-1.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"filename" : "76-1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
@@ -30,6 +30,7 @@
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
@@ -44,6 +45,7 @@
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
|
||||
@@ -6,8 +6,11 @@ CAP_PLUGIN(MyNativeAudio, "MyNativeAudio",
|
||||
CAP_PLUGIN_METHOD(initPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekForward10, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekBackward10, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(seekPlayer, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(terminateStream, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getStreamSyncData, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
|
||||
)
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import MediaPlayer
|
||||
import AVKit
|
||||
|
||||
|
||||
extension UIImageView {
|
||||
public func imageFromUrl(urlString: String) {
|
||||
if let url = NSURL(string: urlString) {
|
||||
let request = NSURLRequest(url: url as URL)
|
||||
NSURLConnection.sendAsynchronousRequest(request as URLRequest, queue: OperationQueue.main) {
|
||||
(response: URLResponse?, data: Data?, error: Error?) -> Void in
|
||||
if let imageData = data as Data? {
|
||||
self.image = UIImage(data: imageData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Audiobook {
|
||||
var streamId = ""
|
||||
var audiobookId = ""
|
||||
var title = "No Title"
|
||||
var author = "Unknown"
|
||||
var playWhenReady = false
|
||||
@@ -16,18 +34,43 @@ struct Audiobook {
|
||||
|
||||
@objc(MyNativeAudio)
|
||||
public class MyNativeAudio: CAPPlugin {
|
||||
|
||||
var avPlayer: AVPlayer!
|
||||
var currentCall: CAPPluginCall?
|
||||
var audioPlayer: AVPlayer!
|
||||
var audiobook: Audiobook?
|
||||
|
||||
enum PlayerState {
|
||||
case stopped
|
||||
case playing
|
||||
case paused
|
||||
}
|
||||
|
||||
private var playerState: PlayerState = .stopped
|
||||
|
||||
// Key-value observing context
|
||||
private var playerItemContext = 0
|
||||
|
||||
override public func load() {
|
||||
NSLog("Load MyNativeAudio")
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(stop),
|
||||
name:Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(appDidEnterBackground),
|
||||
name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(appWillEnterForeground),
|
||||
name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
setupRemoteTransportControls()
|
||||
}
|
||||
|
||||
@objc func initPlayer(_ call: CAPPluginCall) {
|
||||
NSLog("Init Player")
|
||||
audiobook = Audiobook(
|
||||
streamId: call.getString("id") ?? "",
|
||||
audiobookId: call.getString("audiobookId") ?? "",
|
||||
title: call.getString("title") ?? "No Title",
|
||||
author: call.getString("author") ?? "Unknown",
|
||||
playWhenReady: call.getBool("playWhenReady", false),
|
||||
@@ -50,51 +93,85 @@ public class MyNativeAudio: CAPPlugin {
|
||||
url: url!,
|
||||
options: ["AVURLAssetHTTPHeaderFieldsKey": headers]
|
||||
)
|
||||
|
||||
print("Playing audiobook url \(String(describing: url))")
|
||||
|
||||
// For play in background
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowAirPlay])
|
||||
NSLog("[TEST] Playback OK")
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
NSLog("[TEST] Session is Active")
|
||||
} catch {
|
||||
NSLog("[TEST] Failed to set BG Data")
|
||||
print(error)
|
||||
}
|
||||
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
self.audioPlayer = AVPlayer(playerItem: playerItem)
|
||||
// self.audioPlayer = AVPlayer(url: url)
|
||||
|
||||
// Register as an observer of the player item's status property
|
||||
playerItem.addObserver(self,
|
||||
forKeyPath: #keyPath(AVPlayerItem.status),
|
||||
options: [.old, .new],
|
||||
context: &playerItemContext)
|
||||
|
||||
self.audioPlayer.play()
|
||||
self.audioPlayer = AVPlayer(playerItem: playerItem)
|
||||
let time = self.audioPlayer.currentItem?.currentTime()
|
||||
|
||||
print("Audio Player Initialized \(String(describing: time))")
|
||||
|
||||
call.resolve(["success": true])
|
||||
}
|
||||
|
||||
@objc func seekForward10() {
|
||||
@objc func seekForward(_ call: CAPPluginCall) {
|
||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||
|
||||
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||
var destinationTime = currentTime + 10
|
||||
var destinationTime = currentTime + amount
|
||||
if (destinationTime > duration) { destinationTime = duration }
|
||||
|
||||
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
|
||||
self.audioPlayer.seek(to: time)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func seekBackward10() {
|
||||
@objc func seekBackward(_ call: CAPPluginCall) {
|
||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||
|
||||
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||
var destinationTime = currentTime - 10
|
||||
var destinationTime = currentTime - amount
|
||||
if (destinationTime < 0) { destinationTime = 0 }
|
||||
|
||||
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
|
||||
self.audioPlayer.seek(to: time)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func seekPlayer(_ call: CAPPluginCall) {
|
||||
var seekTime = call.getInt("timeMs") ?? 0
|
||||
seekTime /= 1000
|
||||
var seekTime = (Int(call.getString("timeMs", "0")) ?? 0) / 1000
|
||||
NSLog("Seek Player \(seekTime)")
|
||||
|
||||
if (seekTime < 0) { seekTime = 0 }
|
||||
|
||||
let time = CMTime(seconds:Double(seekTime),preferredTimescale: 1000)
|
||||
self.audioPlayer.seek(to: time)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func pausePlayer() {
|
||||
self.audioPlayer.pause()
|
||||
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||
pause()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func playPlayer() {
|
||||
self.audioPlayer.play()
|
||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||
play()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func terminateStream() {
|
||||
self.audioPlayer.pause()
|
||||
@objc func terminateStream(_ call: CAPPluginCall) {
|
||||
pause()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func stop() {
|
||||
@@ -103,4 +180,190 @@ public class MyNativeAudio: CAPPlugin {
|
||||
call.resolve([ "result": true])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||
let buffTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||
NSLog("AVPlayer getCurrentTime \(currTime)")
|
||||
call.resolve([ "value": currTime * 1000, "bufferedTime": buffTime * 1000 ])
|
||||
}
|
||||
|
||||
@objc func getStreamSyncData(_ call: CAPPluginCall) {
|
||||
let streamId = audiobook?.streamId ?? ""
|
||||
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": streamId ])
|
||||
}
|
||||
|
||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||
let speed = call.getFloat("speed") ?? 0
|
||||
NSLog("[TEST] Set Playback Speed \(speed)")
|
||||
audioPlayer.rate = speed
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
func play() {
|
||||
audioPlayer.play()
|
||||
self.notifyListeners("onPlayingUpdate", data: [
|
||||
"value": true
|
||||
])
|
||||
|
||||
playerState = .playing
|
||||
setupNowPlaying()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
audioPlayer.pause()
|
||||
self.notifyListeners("onPlayingUpdate", data: [
|
||||
"value": false
|
||||
])
|
||||
|
||||
playerState = .paused
|
||||
}
|
||||
|
||||
func currentTime() -> Double {
|
||||
return self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||
}
|
||||
|
||||
func duration() -> Double {
|
||||
return self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||
}
|
||||
|
||||
func playbackRate() -> Float {
|
||||
return self.audioPlayer.rate
|
||||
}
|
||||
|
||||
func sendMetadata() {
|
||||
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
||||
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||
self.notifyListeners("onMetadata", data: [
|
||||
"duration": duration * 1000,
|
||||
"currentTime": currTime * 1000,
|
||||
"stateName": "unknown"
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?,
|
||||
of object: Any?,
|
||||
change: [NSKeyValueChangeKey : Any]?,
|
||||
context: UnsafeMutableRawPointer?) {
|
||||
|
||||
// Only handle observations for the playerItemContext
|
||||
guard context == &playerItemContext else {
|
||||
super.observeValue(forKeyPath: keyPath,
|
||||
of: object,
|
||||
change: change,
|
||||
context: context)
|
||||
return
|
||||
}
|
||||
|
||||
if keyPath == #keyPath(AVPlayerItem.status) {
|
||||
let status: AVPlayerItem.Status
|
||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
||||
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
|
||||
print("AVPlayer Status Change \(String(status.rawValue))")
|
||||
} else {
|
||||
status = .unknown
|
||||
}
|
||||
|
||||
// Switch over status value
|
||||
switch status {
|
||||
case .readyToPlay:
|
||||
// Player item is ready to play.
|
||||
NSLog("AVPlayer ready to play")
|
||||
setNowPlayingMetadata()
|
||||
sendMetadata()
|
||||
if (audiobook?.playWhenReady == true) {
|
||||
NSLog("AVPlayer playWhenReady == true")
|
||||
play()
|
||||
}
|
||||
break
|
||||
case .failed:
|
||||
// Player item failed. See error.
|
||||
break
|
||||
case .unknown:
|
||||
// Player item is not yet ready
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func appDidEnterBackground() {
|
||||
setupNowPlaying()
|
||||
NSLog("[TEST] App Enter Backround")
|
||||
}
|
||||
|
||||
@objc func appWillEnterForeground() {
|
||||
|
||||
NSLog("[TEST] App Will Enter Foreground")
|
||||
}
|
||||
|
||||
func setupRemoteTransportControls() {
|
||||
// Get the shared MPRemoteCommandCenter
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
// Add handler for Play Command
|
||||
commandCenter.playCommand.addTarget { [unowned self] event in
|
||||
NSLog("[TEST] Play Command \(playbackRate())")
|
||||
if playbackRate() == 0.0 {
|
||||
play()
|
||||
return .success
|
||||
}
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
// Add handler for Pause Command
|
||||
commandCenter.pauseCommand.addTarget { [unowned self] event in
|
||||
NSLog("[TEST] Pause Command \(playbackRate())")
|
||||
if playbackRate() == 1.0 {
|
||||
pause()
|
||||
return .success
|
||||
}
|
||||
return .commandFailed
|
||||
}
|
||||
}
|
||||
|
||||
func setNowPlayingMetadata() {
|
||||
|
||||
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
||||
var nowPlayingInfo = [String: Any]()
|
||||
|
||||
NSLog("%@", "**** Set track metadata: title \(audiobook?.title ?? "")")
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = audiobook?.playlistUrl ?? ""
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls"
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook?.title ?? ""
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook?.author ?? ""
|
||||
|
||||
if (audiobook?.cover != nil) {
|
||||
let myImageView = UIImageView()
|
||||
myImageView.imageFromUrl(urlString: audiobook?.cover ?? "")
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = myImageView.image
|
||||
}
|
||||
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = audiobook?.author ?? ""
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = audiobook?.title ?? ""
|
||||
|
||||
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
|
||||
func setupNowPlaying() {
|
||||
|
||||
if (playerState != .playing) {
|
||||
NSLog("[TEST] Not current playing so not updating now playing info")
|
||||
return
|
||||
}
|
||||
|
||||
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
||||
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
|
||||
|
||||
NSLog("%@", "**** Set playback info: rate \(playbackRate()), position \(currentTime()), duration \(duration())")
|
||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration()
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime()
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = playbackRate()
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
||||
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,14 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
||||
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
||||
pod 'CapacitorDataStorageSqlite', :path => '../../node_modules/capacitor-data-storage-sqlite'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-full min-h-screen h-full bg-bg text-white">
|
||||
<div class="w-full layout-wrapper bg-bg text-white">
|
||||
<Nuxt />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full min-h-screen h-full bg-bg text-white">
|
||||
<div class="w-full layout-wrapper bg-bg text-white">
|
||||
<app-appbar />
|
||||
<div id="content" class="overflow-hidden" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||
<Nuxt />
|
||||
</div>
|
||||
<app-audio-player-container ref="streamContainer" />
|
||||
@@ -16,7 +16,6 @@ import { Capacitor } from '@capacitor/core'
|
||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -57,7 +56,8 @@ export default {
|
||||
this.initSocketListeners()
|
||||
|
||||
// Load libraries
|
||||
this.$store.dispatch('libraries/load')
|
||||
await this.$store.dispatch('libraries/load')
|
||||
this.$eventBus.$emit('library-changed')
|
||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||
} else {
|
||||
this.removeSocketListeners()
|
||||
@@ -414,12 +414,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#content {
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
#content.playerOpen {
|
||||
height: calc(100vh - 164px);
|
||||
}
|
||||
</style>
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
},
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ name: 'viewport', content: 'viewport-fit=cover, width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: '' },
|
||||
{ name: 'format-detection', content: 'telephone=no' }
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.33-beta",
|
||||
"version": "0.9.35-beta",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1076,6 +1076,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/network/-/network-1.0.3.tgz",
|
||||
"integrity": "sha512-DgRusTC0UkTJE9IQIAMgqBnRnTaj8nFeGH7dwRldfVBZAtHBTkU8wCK/tU1oWtaY2Wam+iyVKXUAhYDO7yeD9Q=="
|
||||
},
|
||||
"@capacitor/status-bar": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-1.0.6.tgz",
|
||||
"integrity": "sha512-5MGWFq76iiKvHpbZ/Xc0Zig3WZyzWZ62wvC4qxak8OuVHBNG4fA1p/XXY9teQPaU3SupEJHnLkw6Gn1LuDp+ew=="
|
||||
},
|
||||
"@capacitor/storage": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/storage/-/storage-1.1.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.35-beta",
|
||||
"version": "0.9.38-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
@@ -18,6 +18,7 @@
|
||||
"@capacitor/dialog": "^1.0.3",
|
||||
"@capacitor/ios": "^3.2.2",
|
||||
"@capacitor/network": "^1.0.3",
|
||||
"@capacitor/status-bar": "^1.0.6",
|
||||
"@capacitor/storage": "^1.1.0",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||
{{ $elapsedPretty(duration) }}<span class="px-4">{{ $bytesPretty(size) }}</span>
|
||||
{{ $elapsedPretty(duration) }}
|
||||
<span class="px-4">{{ $bytesPretty(size) }}</span>
|
||||
</p>
|
||||
|
||||
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
|
||||
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||
<p class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||
<span class="material-icons text-sm">close</span>
|
||||
</div>
|
||||
@@ -35,7 +36,7 @@
|
||||
<span class="material-icons">auto_stories</span>
|
||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="isConnected && showPlay" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
@@ -84,6 +85,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isIos() {
|
||||
return this.$platform === 'ios'
|
||||
},
|
||||
isConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
@@ -132,7 +136,7 @@ export default {
|
||||
return this.userAudiobook ? this.userAudiobook.currentTime : 0
|
||||
},
|
||||
userTimeRemaining() {
|
||||
return this.duration - this.userCurrentTime
|
||||
return Math.max(0, this.duration - this.userCurrentTime)
|
||||
},
|
||||
progressPercent() {
|
||||
return this.userAudiobook ? this.userAudiobook.progress : 0
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<!-- <div v-if="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div> -->
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,11 +27,13 @@ export default {
|
||||
|
||||
<style>
|
||||
.main-content {
|
||||
height: calc(100% - 72px);
|
||||
max-height: calc(100% - 72px);
|
||||
min-height: calc(100% - 72px);
|
||||
max-width: 100vw;
|
||||
}
|
||||
.main-content.home-page {
|
||||
height: calc(100% - 36px);
|
||||
max-height: calc(100% - 36px);
|
||||
min-height: calc(100% - 36px);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-full min-h-full relative">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
|
||||
</template>
|
||||
@@ -7,9 +7,11 @@
|
||||
<div v-if="!shelves.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<div>
|
||||
<p class="mb-4 text-center text-xl">
|
||||
Bookshelf empty<span v-show="isSocketConnected">
|
||||
for library <strong>{{ currentLibraryName }}</strong></span
|
||||
>
|
||||
Bookshelf empty
|
||||
<span v-show="isSocketConnected">
|
||||
for library
|
||||
<strong>{{ currentLibraryName }}</strong>
|
||||
</span>
|
||||
</p>
|
||||
<div class="w-full" v-if="!isSocketConnected">
|
||||
<div class="flex justify-center items-center mb-3">
|
||||
@@ -19,7 +21,7 @@
|
||||
<p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32"> Connect </ui-btn>
|
||||
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +133,7 @@ export default {
|
||||
this.shelves = categories
|
||||
},
|
||||
async socketInit(isConnected) {
|
||||
if (isConnected) {
|
||||
if (isConnected && this.currentLibraryId) {
|
||||
console.log('Connected - Load from server')
|
||||
await this.fetchCategories()
|
||||
} else {
|
||||
@@ -141,8 +143,7 @@ export default {
|
||||
this.loading = false
|
||||
},
|
||||
async libraryChanged(libid) {
|
||||
console.log('Library changed', libid)
|
||||
if (this.isSocketConnected) {
|
||||
if (this.isSocketConnected && this.currentLibraryId) {
|
||||
await this.fetchCategories()
|
||||
} else {
|
||||
this.shelves = this.downloadOnlyShelves
|
||||
@@ -241,7 +242,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.initListeners()
|
||||
if (this.$server.initialized) {
|
||||
if (this.$server.initialized && this.currentLibraryId) {
|
||||
this.fetchCategories()
|
||||
} else {
|
||||
this.shelves = this.downloadOnlyShelves
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">Audiobookshelf</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.<br /><span class="text-xs">For testing android auto functionality there are 2 sample audio files included.</span></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>
|
||||
|
||||
<div class="w-full max-w-md mx-auto px-4 sm:px-6 lg:px-8 z-10">
|
||||
<div v-show="loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 text-center">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="w-full h-full py-6">
|
||||
<h1 class="text-2xl px-4">Downloads</h1>
|
||||
|
||||
<div class="w-full px-2 py-2" :class="hasStoragePermission ? '' : 'text-error'">
|
||||
<div v-if="!isIos" class="w-full px-2 py-2" :class="hasStoragePermission ? '' : 'text-error'">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
||||
<p v-if="hasStoragePermission" class="text-sm px-4" @click="changeDownloadFolderClick">{{ downloadFolderSimplePath || 'No Download Folder Selected' }}</p>
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- <p v-if="hasStoragePermission" class="text-xs text-gray-400 break-all max-w-full">{{ downloadFolderUri }}</p> -->
|
||||
</div>
|
||||
|
||||
<div class="w-full h-10 relative">
|
||||
<div v-if="!isIos" class="w-full h-10 relative">
|
||||
<div class="absolute top-px left-0 z-10 w-full h-full flex">
|
||||
<div class="flex-grow h-full bg-primary rounded-t-md mr-px" @click="showingDownloads = true">
|
||||
<div class="flex items-center justify-center rounded-t-md border-t border-l border-r border-white border-opacity-20 h-full" :class="showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
|
||||
@@ -25,32 +25,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-content-body relative w-full overflow-x-hidden bg-primary">
|
||||
<div v-if="!isIos" class="list-content-body relative w-full overflow-x-hidden bg-primary">
|
||||
<template v-if="showingDownloads">
|
||||
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||
<p>No Downloads</p>
|
||||
</div>
|
||||
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="download in downloadsDownloading">
|
||||
<li :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-3/4">
|
||||
<span class="text-xs">({{ downloadingProgress[download.id] || 0 }}%) {{ download.isPreparing ? 'Preparing' : 'Downloading' }}...</span>
|
||||
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<div class="shadow-sm text-white flex items-center justify-center rounded-full animate-spin">
|
||||
<span class="material-icons">refresh</span>
|
||||
</div>
|
||||
<li v-for="download in downloadsDownloading" :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-3/4">
|
||||
<span class="text-xs">({{ downloadingProgress[download.id] || 0 }}%) {{ download.isPreparing ? 'Preparing' : 'Downloading' }}...</span>
|
||||
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<template v-for="download in downloadsReady">
|
||||
<li :key="download.id" class="text-gray-50 select-none relative pr-4 pl-2 py-5 border-b border-white border-opacity-10" @click="jumpToAudiobook(download)">
|
||||
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
|
||||
</li>
|
||||
</template>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<div class="shadow-sm text-white flex items-center justify-center rounded-full animate-spin">
|
||||
<span class="material-icons">refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="download in downloadsReady" :key="download.id" class="text-gray-50 select-none relative pr-4 pl-2 py-5 border-b border-white border-opacity-10" @click="jumpToAudiobook(download)">
|
||||
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -62,28 +58,24 @@
|
||||
</div>
|
||||
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
|
||||
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
|
||||
<template v-else>
|
||||
<template v-for="mediaFolder in mediaScanResults.folders">
|
||||
<div :key="mediaFolder.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">folder</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFolder.name }}</p>
|
||||
</div>
|
||||
<div v-for="mediaFile in mediaFolder.files" :key="mediaFile.uri" class="ml-3 flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="mediaFolder in mediaScanResults.folders" :key="mediaFolder.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">folder</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFolder.name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="mediaFile in mediaScanResults.files">
|
||||
<div :key="mediaFile.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
<div v-for="mediaFile in mediaFolder.files" :key="mediaFile.uri" class="ml-3 flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-for="mediaFile in mediaScanResults.files" :key="mediaFile.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -105,6 +97,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isIos() {
|
||||
return this.$platform === 'ios'
|
||||
},
|
||||
isSocketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import Vue from 'vue'
|
||||
import { App } from '@capacitor/app'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||
import { formatDistance, format } from 'date-fns'
|
||||
|
||||
const setStatusBarStyleDark = async () => {
|
||||
await StatusBar.setStyle({ style: Style.Dark })
|
||||
}
|
||||
setStatusBarStyleDark()
|
||||
|
||||
App.addListener('backButton', async ({ canGoBack }) => {
|
||||
if (!canGoBack) {
|
||||
const { value } = await Dialog.confirm({
|
||||
|
||||
@@ -23,7 +23,6 @@ class StoreService {
|
||||
init() {
|
||||
this.platform = Capacitor.getPlatform()
|
||||
this.store = CapacitorDataStorageSqlite
|
||||
console.log('in init ', this.platform)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,13 +2,23 @@
|
||||
|
||||
AudioBookshelf is a self-hosted audiobook server for managing and playing your audiobooks.
|
||||
|
||||
### Android (beta)
|
||||
Get the Android app on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
||||
|
||||
[Go to the main project repo github.com/advplyr/audiobookshelf](https://github.com/advplyr/audiobookshelf)
|
||||
### iOS (beta)
|
||||
|
||||
[audiobookshelf.org](https://audiobookshelf.org)
|
||||
Available to beta testers through Test Flight
|
||||
|
||||
**Currently in Beta** - **Requires an Audiobookshelf server to connect with**
|
||||
Join the beta testers and install the iOS app: https://testflight.apple.com/join/wiic7QIW
|
||||
|
||||
|
||||
Join the discussion: https://github.com/advplyr/audiobookshelf-app/discussions/60
|
||||
|
||||
---
|
||||
|
||||
[Go to the main project repo github.com/advplyr/audiobookshelf](https://github.com/advplyr/audiobookshelf) or the project site [audiobookshelf.org](https://audiobookshelf.org)
|
||||
|
||||
**Requires an Audiobookshelf server to connect with**
|
||||
|
||||
<img alt="Screenshot1" src="https://github.com/advplyr/audiobookshelf-app/raw/master/screenshots/BookshelfViews.png" />
|
||||
|
||||
|
||||
@@ -4,33 +4,25 @@ export const state = () => ({
|
||||
|
||||
export const getters = {
|
||||
getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
|
||||
if (!bookItem) return placeholder
|
||||
var book = bookItem.book
|
||||
if (!book || !book.cover || book.cover === placeholder) return placeholder
|
||||
var cover = book.cover
|
||||
|
||||
// Absolute URL covers (should no longer be used)
|
||||
if (cover.startsWith('http:') || cover.startsWith('https:')) return cover
|
||||
if (book.cover.startsWith('http:') || book.cover.startsWith('https:')) return book.cover
|
||||
|
||||
// Server hosted covers
|
||||
try {
|
||||
// Ensure cover is refreshed if cached
|
||||
var bookLastUpdate = book.lastUpdate || Date.now()
|
||||
var userToken = rootGetters['user/getToken']
|
||||
var userToken = rootGetters['user/getToken']
|
||||
var bookLastUpdate = book.lastUpdate || Date.now()
|
||||
|
||||
// Map old covers to new format /s/book/{bookid}/*
|
||||
if (cover.substr(1).startsWith('local')) {
|
||||
cover = cover.replace('local', `s/book/${bookItem.id}`)
|
||||
if (cover.includes(bookItem.path)) { // Remove book path
|
||||
cover = cover.replace(bookItem.path, '').replace('//', '/').replace('\\\\', '/')
|
||||
}
|
||||
}
|
||||
|
||||
var url = new URL(cover, rootState.serverUrl)
|
||||
return url + `?token=${userToken}&ts=${bookLastUpdate}`
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return placeholder
|
||||
if (!bookItem.id) {
|
||||
console.error('No book item id', bookItem)
|
||||
}
|
||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||
// return `http://localhost:3333/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}`
|
||||
}
|
||||
|
||||
var url = new URL(`/api/books/${bookItem.id}/cover`, rootState.serverUrl)
|
||||
return `${url}?token=${userToken}&ts=${bookLastUpdate}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export const state = () => ({
|
||||
libraries: [],
|
||||
lastLoad: 0,
|
||||
listeners: [],
|
||||
currentLibraryId: 'main',
|
||||
currentLibraryId: '',
|
||||
showModal: false,
|
||||
folders: [],
|
||||
folderLastUpdate: 0,
|
||||
@@ -65,17 +65,23 @@ export const actions = {
|
||||
return false
|
||||
}
|
||||
|
||||
this.$axios
|
||||
return this.$axios
|
||||
.$get(`/api/libraries`)
|
||||
.then((data) => {
|
||||
// Set current library
|
||||
if (data.length) {
|
||||
commit('setCurrentLibrary', data[0].id)
|
||||
}
|
||||
|
||||
commit('set', data)
|
||||
commit('setLastLoad')
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
commit('set', [])
|
||||
return false
|
||||
})
|
||||
return true
|
||||
},
|
||||
|
||||
}
|
||||
|
||||