Check app version, fix close stream bug, show audiobook progress, add toasts
@@ -33,6 +33,7 @@ class Server extends EventEmitter {
|
|||||||
setUser(user) {
|
setUser(user) {
|
||||||
this.user = user
|
this.user = user
|
||||||
this.store.commit('user/setUser', user)
|
this.store.commit('user/setUser', user)
|
||||||
|
this.store.commit('user/setSettings', user.settings)
|
||||||
if (user) {
|
if (user) {
|
||||||
localStorage.setItem('userToken', user.token)
|
localStorage.setItem('userToken', user.token)
|
||||||
} else {
|
} else {
|
||||||
@@ -149,6 +150,11 @@ class Server extends EventEmitter {
|
|||||||
this.emit('initialStream', data.stream)
|
this.emit('initialStream', data.stream)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.socket.on('user_updated', (user) => {
|
||||||
|
if (this.user && user.id === this.user.id) {
|
||||||
|
this.setUser(user)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,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 3
|
versionCode 4
|
||||||
versionName "0.1.0-beta"
|
versionName "0.2.0-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.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ android {
|
|||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':capacitor-dialog')
|
implementation project(':capacitor-dialog')
|
||||||
implementation project(':capacitor-toast')
|
implementation project(':robingenz-capacitor-app-update')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/toast",
|
"pkg": "@robingenz/capacitor-app-update",
|
||||||
"classpath": "com.capacitorjs.plugins.toast.ToastPlugin"
|
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Audiobook {
|
|||||||
var cover:String = ""
|
var cover:String = ""
|
||||||
var playWhenReady:Boolean = false
|
var playWhenReady:Boolean = false
|
||||||
var startTime:Long = 0
|
var startTime:Long = 0
|
||||||
|
var playbackSpeed:Float = 1f
|
||||||
var duration:Long = 0
|
var duration:Long = 0
|
||||||
|
|
||||||
var hasPlayerLoaded:Boolean = false
|
var hasPlayerLoaded:Boolean = false
|
||||||
@@ -30,6 +31,7 @@ class Audiobook {
|
|||||||
playlistUrl = jsondata.getString("playlistUrl", "").toString()
|
playlistUrl = jsondata.getString("playlistUrl", "").toString()
|
||||||
playWhenReady = jsondata.getBoolean("playWhenReady", false) == true
|
playWhenReady = jsondata.getBoolean("playWhenReady", false) == true
|
||||||
startTime = jsondata.getString("startTime", "0")!!.toLong()
|
startTime = jsondata.getString("startTime", "0")!!.toLong()
|
||||||
|
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
||||||
duration = jsondata.getString("duration", "0")!!.toLong()
|
duration = jsondata.getString("duration", "0")!!.toLong()
|
||||||
|
|
||||||
playlistUri = Uri.parse(playlistUrl)
|
playlistUri = Uri.parse(playlistUrl)
|
||||||
|
|||||||
@@ -102,16 +102,27 @@ class MyNativeAudio : Plugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun seekForward10(call: PluginCall) {
|
fun seekForward(call: PluginCall) {
|
||||||
|
var amount:Long = call.getString("amount", "0")!!.toLong()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.seekForward10()
|
playerNotificationService.seekForward(amount)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun seekBackward10(call: PluginCall) {
|
fun seekBackward(call: PluginCall) {
|
||||||
|
var amount:Long = call.getString("amount", "0")!!.toLong()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.seekBackward10()
|
playerNotificationService.seekBackward(amount)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@PluginMethod
|
||||||
|
fun setPlaybackSpeed(call: PluginCall) {
|
||||||
|
var playbackSpeed:Float = call.getFloat("speed", 1.0f)!!
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post() {
|
||||||
|
playerNotificationService.setPlaybackSpeed(playbackSpeed)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ class PlayerNotificationService : Service() {
|
|||||||
mPlayer.setMediaSource(mediaSource, true)
|
mPlayer.setMediaSource(mediaSource, true)
|
||||||
mPlayer.prepare()
|
mPlayer.prepare()
|
||||||
mPlayer.playWhenReady = currentAudiobook!!.playWhenReady
|
mPlayer.playWhenReady = currentAudiobook!!.playWhenReady
|
||||||
|
mPlayer.setPlaybackSpeed(audiobook.playbackSpeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -396,12 +397,16 @@ class PlayerNotificationService : Service() {
|
|||||||
mPlayer.seekTo(time)
|
mPlayer.seekTo(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekForward10() {
|
fun seekForward(amount:Long) {
|
||||||
mPlayer.seekTo(mPlayer.currentPosition + 10000)
|
mPlayer.seekTo(mPlayer.currentPosition + amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekBackward10() {
|
fun seekBackward(amount:Long) {
|
||||||
mPlayer.seekTo(mPlayer.currentPosition - 10000)
|
mPlayer.seekTo(mPlayer.currentPosition - amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPlaybackSpeed(speed:Float) {
|
||||||
|
mPlayer.setPlaybackSpeed(speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun terminateStream() {
|
fun terminateStream() {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -17,6 +17,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
||||||
<item name="android:background">@drawable/splash</item>
|
<item name="android:background">@drawable/screen</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -5,5 +5,5 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||||||
include ':capacitor-dialog'
|
include ':capacitor-dialog'
|
||||||
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
||||||
|
|
||||||
include ':capacitor-toast'
|
include ':robingenz-capacitor-app-update'
|
||||||
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
|
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ ext {
|
|||||||
androidxAppCompatVersion = '1.2.0'
|
androidxAppCompatVersion = '1.2.0'
|
||||||
androidxCoordinatorLayoutVersion = '1.1.0'
|
androidxCoordinatorLayoutVersion = '1.1.0'
|
||||||
androidxCoreVersion = '1.6.0'
|
androidxCoreVersion = '1.6.0'
|
||||||
|
androidPlayCore = '1.9.0'
|
||||||
androidxFragmentVersion = '1.3.0'
|
androidxFragmentVersion = '1.3.0'
|
||||||
junitVersion = '4.13.1'
|
junitVersion = '4.13.1'
|
||||||
androidxJunitVersion = '1.1.2'
|
androidxJunitVersion = '1.1.2'
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.box-shadow-md {
|
||||||
|
box-shadow: 2px 8px 6px #111111aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow-lg-up {
|
||||||
|
box-shadow: 0px -12px 8px #111111ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow-xl {
|
||||||
|
box-shadow: 2px 14px 8px #111111aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow-book {
|
||||||
|
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
|
||||||
<span class="material-icons text-3xl">forward_10</span>
|
<span class="material-icons text-3xl">forward_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-8" @mousedown.prevent @mouseup.prevent>
|
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-7 w-10 text-center" @mousedown.prevent @mouseup.prevent @click="$emit('selectPlaybackSpeed')">
|
||||||
<span class="font-mono text-lg uppercase">1x</span>
|
<span class="font-mono text-lg">{{ playbackRate }}x</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -58,8 +58,9 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
totalDuration: 0,
|
totalDuration: 0,
|
||||||
|
currentPlaybackRate: 1,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
isTerminated: false,
|
isResetting: false,
|
||||||
initObject: null,
|
initObject: null,
|
||||||
stateName: 'idle',
|
stateName: 'idle',
|
||||||
playInterval: null,
|
playInterval: null,
|
||||||
@@ -77,17 +78,24 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
totalDurationPretty() {
|
totalDurationPretty() {
|
||||||
return this.$secondsToTimestamp(this.totalDuration)
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
|
},
|
||||||
|
playbackRate() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updatePlaybackRate() {
|
||||||
|
this.currentPlaybackRate = this.playbackRate
|
||||||
|
MyNativeAudio.setPlaybackSpeed({ speed: this.playbackRate })
|
||||||
|
},
|
||||||
restart() {
|
restart() {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
},
|
},
|
||||||
backward10() {
|
backward10() {
|
||||||
MyNativeAudio.seekBackward10()
|
MyNativeAudio.seekBackward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
forward10() {
|
forward10() {
|
||||||
MyNativeAudio.seekForward10()
|
MyNativeAudio.seekForward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
sendStreamUpdate() {
|
sendStreamUpdate() {
|
||||||
this.$emit('updateTime', this.currentTime)
|
this.$emit('updateTime', this.currentTime)
|
||||||
@@ -191,7 +199,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
set(audiobookStreamData) {
|
set(audiobookStreamData) {
|
||||||
|
this.isResetting = false
|
||||||
this.initObject = { ...audiobookStreamData }
|
this.initObject = { ...audiobookStreamData }
|
||||||
|
this.currentPlaybackRate = this.initObject.playbackSpeed
|
||||||
MyNativeAudio.initPlayer(this.initObject)
|
MyNativeAudio.initPlayer(this.initObject)
|
||||||
},
|
},
|
||||||
setFromObj() {
|
setFromObj() {
|
||||||
@@ -199,6 +209,7 @@ export default {
|
|||||||
console.error('Cannot set from obj')
|
console.error('Cannot set from obj')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.isResetting = false
|
||||||
MyNativeAudio.initPlayer(this.initObject)
|
MyNativeAudio.initPlayer(this.initObject)
|
||||||
},
|
},
|
||||||
play() {
|
play() {
|
||||||
@@ -220,13 +231,17 @@ export default {
|
|||||||
stopPlayInterval() {
|
stopPlayInterval() {
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
},
|
},
|
||||||
terminateStream(startTime) {
|
resetStream(startTime) {
|
||||||
var _time = String(Math.floor(startTime * 1000))
|
var _time = String(Math.floor(startTime * 1000))
|
||||||
if (!this.initObject) {
|
if (!this.initObject) {
|
||||||
console.error('Terminate stream when no init object is set...')
|
console.error('Terminate stream when no init object is set...')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.isResetting = true
|
||||||
this.initObject.currentTime = _time
|
this.initObject.currentTime = _time
|
||||||
|
this.terminateStream()
|
||||||
|
},
|
||||||
|
terminateStream() {
|
||||||
MyNativeAudio.terminateStream()
|
MyNativeAudio.terminateStream()
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
@@ -245,7 +260,7 @@ export default {
|
|||||||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||||
this.stateName = data.stateName
|
this.stateName = data.stateName
|
||||||
|
|
||||||
if (this.stateName === 'ended' && this.isTerminated) {
|
if (this.stateName === 'ended' && this.isResetting) {
|
||||||
this.setFromObj()
|
this.setFromObj()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
<template v-for="(shelf, index) in groupedBooks">
|
<template v-for="(shelf, index) in groupedBooks">
|
||||||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||||
<template v-for="audiobook in shelf">
|
<template v-for="audiobook in shelf">
|
||||||
<div :key="audiobook.id" class="relative px-4">
|
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
||||||
<nuxt-link :to="`/audiobook/${audiobook.id}`">
|
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" />
|
||||||
|
<!-- <nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||||
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" style="box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166" />
|
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" style="box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166" />
|
||||||
</nuxt-link>
|
</nuxt-link> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</template>
|
</template>
|
||||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
</div>
|
</div>
|
||||||
@@ -40,6 +41,9 @@ export default {
|
|||||||
},
|
},
|
||||||
hasFilters() {
|
hasFilters() {
|
||||||
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
||||||
|
},
|
||||||
|
userAudiobooks() {
|
||||||
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
<div class="absolute left-2 -top-10">
|
<div class="absolute left-2 -top-10">
|
||||||
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
|
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
|
||||||
</div>
|
</div>
|
||||||
<audio-player-mini ref="audioPlayerMini" :loading="!stream || currStreamAudiobookId !== streamAudiobookId" @updateTime="updateTime" @hook:mounted="audioPlayerMounted" />
|
<audio-player-mini ref="audioPlayerMini" :loading="!stream || currStreamAudiobookId !== streamAudiobookId" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
|
||||||
</div>
|
</div>
|
||||||
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,7 +26,9 @@ export default {
|
|||||||
return {
|
return {
|
||||||
audioPlayerReady: false,
|
audioPlayerReady: false,
|
||||||
stream: null,
|
stream: null,
|
||||||
lastServerUpdateSentSeconds: 0
|
lastServerUpdateSentSeconds: 0,
|
||||||
|
showPlaybackSpeedModal: false,
|
||||||
|
playbackSpeed: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -85,6 +88,10 @@ export default {
|
|||||||
})
|
})
|
||||||
if (value) {
|
if (value) {
|
||||||
this.$server.socket.emit('close_stream')
|
this.$server.socket.emit('close_stream')
|
||||||
|
this.$store.commit('setStreamAudiobook', null)
|
||||||
|
if (this.$refs.audioPlayerMini) {
|
||||||
|
this.$refs.audioPlayerMini.terminateStream()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTime(currentTime) {
|
updateTime(currentTime) {
|
||||||
@@ -122,7 +129,7 @@ export default {
|
|||||||
streamReset({ streamId, startTime }) {
|
streamReset({ streamId, startTime }) {
|
||||||
if (this.$refs.audioPlayerMini) {
|
if (this.$refs.audioPlayerMini) {
|
||||||
if (this.stream && this.stream.id === streamId) {
|
if (this.stream && this.stream.id === streamId) {
|
||||||
this.$refs.audioPlayerMini.terminateStream(startTime)
|
this.$refs.audioPlayerMini.resetStream(startTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -133,10 +140,6 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stream && this.stream.id !== stream.id) {
|
|
||||||
console.error('STREAM CHANGED', this.stream.id, stream.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stream = stream
|
this.stream = stream
|
||||||
|
|
||||||
var playlistUrl = stream.clientPlaylistUri
|
var playlistUrl = stream.clientPlaylistUri
|
||||||
@@ -149,6 +152,7 @@ export default {
|
|||||||
author: this.author,
|
author: this.author,
|
||||||
playWhenReady: !!playOnLoad,
|
playWhenReady: !!playOnLoad,
|
||||||
startTime: String(Math.floor(currentTime * 1000)),
|
startTime: String(Math.floor(currentTime * 1000)),
|
||||||
|
playbackSpeed: this.playbackSpeed || 1,
|
||||||
cover: this.coverForNative,
|
cover: this.coverForNative,
|
||||||
duration: String(Math.floor(this.duration * 1000)),
|
duration: String(Math.floor(this.duration * 1000)),
|
||||||
series: this.seriesTxt,
|
series: this.seriesTxt,
|
||||||
@@ -156,8 +160,6 @@ export default {
|
|||||||
token: this.$store.getters['user/getToken']
|
token: this.$store.getters['user/getToken']
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('audiobook stream data', audiobookStreamData.token, JSON.stringify(audiobookStreamData))
|
|
||||||
|
|
||||||
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
||||||
},
|
},
|
||||||
audioPlayerMounted() {
|
audioPlayerMounted() {
|
||||||
@@ -177,11 +179,31 @@ export default {
|
|||||||
this.$server.socket.on('stream_progress', this.streamProgress)
|
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||||
this.$server.socket.on('stream_ready', this.streamReady)
|
this.$server.socket.on('stream_ready', this.streamReady)
|
||||||
this.$server.socket.on('stream_reset', this.streamReset)
|
this.$server.socket.on('stream_reset', this.streamReset)
|
||||||
|
},
|
||||||
|
changePlaybackSpeed(speed) {
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||||
|
},
|
||||||
|
settingsUpdated(settings) {
|
||||||
|
if (this.$refs.audioPlayerMini && this.$refs.audioPlayerMini.currentPlaybackRate !== settings.playbackRate) {
|
||||||
|
this.playbackSpeed = settings.playbackRate
|
||||||
|
this.$refs.audioPlayerMini.updatePlaybackRate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
console.warn('Stream Container Mounted')
|
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
|
|
||||||
this.setListeners()
|
this.setListeners()
|
||||||
|
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$server.socket.off('stream_open', this.streamOpen)
|
||||||
|
this.$server.socket.off('stream_closed', this.streamClosed)
|
||||||
|
this.$server.socket.off('stream_progress', this.streamProgress)
|
||||||
|
this.$server.socket.off('stream_ready', this.streamReady)
|
||||||
|
this.$server.socket.off('stream_reset', this.streamReset)
|
||||||
|
|
||||||
|
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- New Book Flag -->
|
||||||
|
<div v-if="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
|
||||||
|
<p class="text-center text-sm">New</p>
|
||||||
|
</div>
|
||||||
|
<div class="absolute -bottom-4 left-0 triangle-right" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
|
||||||
|
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||||
|
<div class="w-full relative" :style="{ height: height + 'px' }">
|
||||||
|
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
|
||||||
|
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||||
|
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
audiobook: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
userProgress: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 140
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isNew() {
|
||||||
|
return this.tags.includes('new')
|
||||||
|
},
|
||||||
|
tags() {
|
||||||
|
return this.audiobook.tags || []
|
||||||
|
},
|
||||||
|
audiobookId() {
|
||||||
|
return this.audiobook.id
|
||||||
|
},
|
||||||
|
book() {
|
||||||
|
return this.audiobook.book || {}
|
||||||
|
},
|
||||||
|
height() {
|
||||||
|
return this.width * 1.6
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 120
|
||||||
|
},
|
||||||
|
paddingX() {
|
||||||
|
return 16 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
author() {
|
||||||
|
return this.book.author
|
||||||
|
},
|
||||||
|
authorFL() {
|
||||||
|
return this.book.authorFL || this.author
|
||||||
|
},
|
||||||
|
authorLF() {
|
||||||
|
return this.book.authorLF || this.author
|
||||||
|
},
|
||||||
|
authorFormat() {
|
||||||
|
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
|
||||||
|
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||||
|
},
|
||||||
|
orderBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
|
},
|
||||||
|
userProgressPercent() {
|
||||||
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
|
},
|
||||||
|
showError() {
|
||||||
|
return this.hasMissingParts || this.hasInvalidParts
|
||||||
|
},
|
||||||
|
hasMissingParts() {
|
||||||
|
return this.audiobook.hasMissingParts
|
||||||
|
},
|
||||||
|
hasInvalidParts() {
|
||||||
|
return this.audiobook.hasInvalidParts
|
||||||
|
},
|
||||||
|
errorText() {
|
||||||
|
var txt = ''
|
||||||
|
if (this.hasMissingParts) {
|
||||||
|
txt = `${this.hasMissingParts} missing parts.`
|
||||||
|
}
|
||||||
|
if (this.hasInvalidParts) {
|
||||||
|
if (this.hasMissingParts) txt += ' '
|
||||||
|
txt += `${this.hasInvalidParts} invalid parts.`
|
||||||
|
}
|
||||||
|
return txt || 'Unknown Error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
play() {
|
||||||
|
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||||
|
this.$root.socket.emit('open_stream', this.audiobookId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<span class="material-icons text-4xl">close</span>
|
<span class="material-icons text-4xl">close</span>
|
||||||
</div>
|
</div>
|
||||||
<slot name="outer" />
|
<slot name="outer" />
|
||||||
<div ref="content" style="min-width: 90%; min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
|
<div ref="content" style="max-width: 90%; min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" :width="200" height="100%">
|
||||||
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||||
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
|
<template v-for="rate in rates">
|
||||||
|
<li :key="rate" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(rate)">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="font-normal ml-3 block truncate text-lg">{{ rate }}x</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
playbackSpeed: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
get() {
|
||||||
|
return this.playbackSpeed
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:playbackSpeed', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rates() {
|
||||||
|
return [0.25, 0.5, 0.8, 1, 1.3, 1.5, 2, 2.5, 3]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickedOption(speed) {
|
||||||
|
if (this.selected === speed) {
|
||||||
|
this.show = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selected = speed
|
||||||
|
this.show = false
|
||||||
|
this.$nextTick(() => this.$emit('change', speed))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="loading" :type="type" :class="classList" @click="click">
|
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @click="click">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
||||||
@@ -23,7 +23,8 @@ export default {
|
|||||||
},
|
},
|
||||||
paddingX: Number,
|
paddingX: Number,
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
loading: Boolean
|
loading: Boolean,
|
||||||
|
disabled: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
||||||
pod 'CapacitorToast', :path => '../../node_modules/@capacitor/toast'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|||||||
@@ -9,18 +9,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
middleware: 'authenticated',
|
middleware: 'authenticated',
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
// watch: {
|
|
||||||
// routeName(newVal, oldVal) {
|
|
||||||
// if (newVal === 'connect' && this.$server.connected) {
|
|
||||||
// this.$router.replace('/')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
computed: {
|
computed: {
|
||||||
streaming() {
|
streaming() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
@@ -46,63 +41,95 @@ export default {
|
|||||||
this.$refs.streamContainer.streamOpen(stream)
|
this.$refs.streamContainer.streamOpen(stream)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
parseSemver(ver) {
|
async clickUpdateToast() {
|
||||||
if (!ver) return null
|
var immediateUpdateAllowed = this.$store.state.appUpdateInfo.immediateUpdateAllowed
|
||||||
var groups = ver.match(/^v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$/)
|
if (immediateUpdateAllowed) {
|
||||||
if (groups && groups.length > 6) {
|
await AppUpdate.performImmediateUpdate()
|
||||||
var total = Number(groups[3]) * 100 + Number(groups[4]) * 10 + Number(groups[5])
|
|
||||||
if (isNaN(total)) {
|
|
||||||
console.warn('Invalid version total', groups[3], groups[4], groups[5])
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
total,
|
|
||||||
version: groups[2],
|
|
||||||
major: Number(groups[3]),
|
|
||||||
minor: Number(groups[4]),
|
|
||||||
patch: Number(groups[5]),
|
|
||||||
preRelease: groups[6] || null
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Invalid semver string', ver)
|
await AppUpdate.openAppStore()
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
},
|
},
|
||||||
checkForUpdate() {
|
showUpdateToast(availableVersion, immediateUpdateAllowed) {
|
||||||
if (!this.$config.version) {
|
var toastText = immediateUpdateAllowed ? `Click here to update` : `Click here to open app store`
|
||||||
return
|
this.$toast.info(`Update is available for v${availableVersion}! ${toastText}`, {
|
||||||
}
|
draggable: false,
|
||||||
var currVerObj = this.parseSemver(this.$config.version)
|
hideProgressBar: false,
|
||||||
if (!currVerObj) {
|
timeout: 10000,
|
||||||
console.error('Invalid version', this.$config.version)
|
closeButton: false,
|
||||||
return
|
onClick: this.clickUpdateToast()
|
||||||
}
|
|
||||||
console.log('Check for update, your version:', currVerObj.version)
|
|
||||||
this.$store.commit('setCurrentVersion', currVerObj)
|
|
||||||
|
|
||||||
var largestVer = null
|
|
||||||
this.$axios.$get(`https://api.github.com/repos/advplyr/audiobookshelf-app/tags`).then((tags) => {
|
|
||||||
if (tags && tags.length) {
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
var verObj = this.parseSemver(tag.name)
|
|
||||||
if (verObj) {
|
|
||||||
if (!largestVer || largestVer.total < verObj.total) {
|
|
||||||
largestVer = verObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
if (!largestVer) {
|
},
|
||||||
console.error('No valid version tags to compare with')
|
async checkForUpdate() {
|
||||||
|
const result = await AppUpdate.getAppUpdateInfo()
|
||||||
|
if (!result) {
|
||||||
|
console.error('Invalid version check')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$store.commit('setLatestVersion', largestVer)
|
this.$store.commit('setAppUpdateInfo', result)
|
||||||
if (largestVer.total > currVerObj.total) {
|
|
||||||
console.log('Has Update!', largestVer.version)
|
if (result.updateAvailability === 2) {
|
||||||
this.$store.commit('setHasUpdate', true)
|
setTimeout(() => {
|
||||||
|
this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||||
|
}, 5000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// parseSemver(ver) {
|
||||||
|
// if (!ver) return null
|
||||||
|
// var groups = ver.match(/^v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$/)
|
||||||
|
// if (groups && groups.length > 6) {
|
||||||
|
// var total = Number(groups[3]) * 100 + Number(groups[4]) * 10 + Number(groups[5])
|
||||||
|
// if (isNaN(total)) {
|
||||||
|
// console.warn('Invalid version total', groups[3], groups[4], groups[5])
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
// return {
|
||||||
|
// total,
|
||||||
|
// version: groups[2],
|
||||||
|
// major: Number(groups[3]),
|
||||||
|
// minor: Number(groups[4]),
|
||||||
|
// patch: Number(groups[5]),
|
||||||
|
// preRelease: groups[6] || null
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// console.warn('Invalid semver string', ver)
|
||||||
|
// }
|
||||||
|
// return null
|
||||||
|
// },
|
||||||
|
// checkForUpdateWebVersion() {
|
||||||
|
// if (!this.$config.version) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// var currVerObj = this.parseSemver(this.$config.version)
|
||||||
|
// if (!currVerObj) {
|
||||||
|
// console.error('Invalid version', this.$config.version)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// console.log('Check for update, your version:', currVerObj.version)
|
||||||
|
// this.$store.commit('setCurrentVersion', currVerObj)
|
||||||
|
|
||||||
|
// var largestVer = null
|
||||||
|
// this.$axios.$get(`https://api.github.com/repos/advplyr/audiobookshelf-app/tags`).then((tags) => {
|
||||||
|
// if (tags && tags.length) {
|
||||||
|
// tags.forEach((tag) => {
|
||||||
|
// var verObj = this.parseSemver(tag.name)
|
||||||
|
// if (verObj) {
|
||||||
|
// if (!largestVer || largestVer.total < verObj.total) {
|
||||||
|
// largestVer = verObj
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// if (!largestVer) {
|
||||||
|
// console.error('No valid version tags to compare with')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// this.$store.commit('setLatestVersion', largestVer)
|
||||||
|
// if (largestVer.total > currVerObj.total) {
|
||||||
|
// console.log('Has Update!', largestVer.version)
|
||||||
|
// this.$store.commit('setHasUpdate', true)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (!this.$server) return console.error('No Server')
|
if (!this.$server) return console.error('No Server')
|
||||||
@@ -114,6 +141,8 @@ export default {
|
|||||||
this.$router.push('/connect')
|
this.$router.push('/connect')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkForUpdate()
|
||||||
|
|
||||||
// var checkForUpdateFlag = localStorage.getItem('checkForUpdate')
|
// var checkForUpdateFlag = localStorage.getItem('checkForUpdate')
|
||||||
// if (!checkForUpdateFlag || checkForUpdateFlag !== '1') {
|
// if (!checkForUpdateFlag || checkForUpdateFlag !== '1') {
|
||||||
// this.checkForUpdate()
|
// this.checkForUpdate()
|
||||||
|
|||||||
@@ -32,13 +32,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
|
'@/assets/app.css'
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
{ src: '~/plugins/server.js', mode: 'client' },
|
'@/plugins/server.js',
|
||||||
'@/plugins/init.client.js',
|
'@/plugins/init.client.js',
|
||||||
'@/plugins/axios.js',
|
'@/plugins/axios.js',
|
||||||
'@/plugins/my-native-audio.js'
|
'@/plugins/my-native-audio.js',
|
||||||
|
'@/plugins/toast.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
components: true,
|
components: true,
|
||||||
|
|||||||
@@ -1061,11 +1061,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.2.2.tgz",
|
||||||
"integrity": "sha512-Eq17Y+UDHFmYGaZcObvxHAcHw0fF9TCBAg1f5f6qdV8ab3cKKEUB9xMvoCSZAueBfxFARrD18TsZJKoxh2YsLA=="
|
"integrity": "sha512-Eq17Y+UDHFmYGaZcObvxHAcHw0fF9TCBAg1f5f6qdV8ab3cKKEUB9xMvoCSZAueBfxFARrD18TsZJKoxh2YsLA=="
|
||||||
},
|
},
|
||||||
"@capacitor/toast": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/toast/-/toast-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-4R4aUP/BeqQ9/CRJpuoE+U+3tO1pmoZE8fNPhmcXZJYNedprX/uZfNzxBDz6kCJ90RETjrF4e/BxtBFXLiXxxQ=="
|
|
||||||
},
|
|
||||||
"@csstools/convert-colors": {
|
"@csstools/convert-colors": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
||||||
@@ -2619,6 +2614,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
|
||||||
"integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA=="
|
"integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA=="
|
||||||
},
|
},
|
||||||
|
"@robingenz/capacitor-app-update": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@robingenz/capacitor-app-update/-/capacitor-app-update-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pK8Yi7VgG/O/R4kJ3JtLpdeQgJzRIDPGM61bJhofTqu/+i26h8GhQdq4MB2OLJWk06Ht8MYDIIW/E0nxatrrnA=="
|
||||||
|
},
|
||||||
"@types/component-emitter": {
|
"@types/component-emitter": {
|
||||||
"version": "1.2.10",
|
"version": "1.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||||
@@ -13354,6 +13354,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw=="
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw=="
|
||||||
},
|
},
|
||||||
|
"vue-toastification": {
|
||||||
|
"version": "1.7.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-1.7.11.tgz",
|
||||||
|
"integrity": "sha512-CT/DYttb/VtWDNdhJG0BskLVfveZq5rGOgO/u3qTX+RPQQzX0WSai8VVxxUuvR8UpxfSGPS+JQleR33bo3Vadg=="
|
||||||
|
},
|
||||||
"vuex": {
|
"vuex": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.1.0-beta",
|
"version": "v0.2.0-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
@@ -15,13 +15,14 @@
|
|||||||
"@capacitor/core": "^3.1.2",
|
"@capacitor/core": "^3.1.2",
|
||||||
"@capacitor/dialog": "^1.0.3",
|
"@capacitor/dialog": "^1.0.3",
|
||||||
"@capacitor/ios": "^3.2.2",
|
"@capacitor/ios": "^3.2.2",
|
||||||
"@capacitor/toast": "^1.0.2",
|
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"hls.js": "^1.0.9",
|
"hls.js": "^1.0.9",
|
||||||
"nuxt": "^2.15.7",
|
"nuxt": "^2.15.7",
|
||||||
"socket.io-client": "^4.1.3"
|
"socket.io-client": "^4.1.3",
|
||||||
|
"vue-toastification": "^1.7.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.13.15",
|
"@babel/core": "7.13.15",
|
||||||
|
|||||||
@@ -17,11 +17,15 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-mono pt-1 pb-4">v{{ $config.version }}</p>
|
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
||||||
|
|
||||||
|
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate"> Version {{ availableVersion }} is available! {{ immediateUpdateAllowed ? 'Update now' : 'Get update from app store' }} </ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -35,9 +39,32 @@ export default {
|
|||||||
},
|
},
|
||||||
serverUrl() {
|
serverUrl() {
|
||||||
return this.$server.url
|
return this.$server.url
|
||||||
|
},
|
||||||
|
appUpdateInfo() {
|
||||||
|
return this.$store.state.appUpdateInfo
|
||||||
|
},
|
||||||
|
availableVersion() {
|
||||||
|
return this.appUpdateInfo ? this.appUpdateInfo.availableVersion : null
|
||||||
|
},
|
||||||
|
immediateUpdateAllowed() {
|
||||||
|
return this.appUpdateInfo ? !!this.appUpdateInfo.immediateUpdateAllowed : false
|
||||||
|
},
|
||||||
|
updateAvailability() {
|
||||||
|
return this.appUpdateInfo ? this.appUpdateInfo.updateAvailability : null
|
||||||
|
},
|
||||||
|
isUpdateAvailable() {
|
||||||
|
return this.updateAvailability === 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async clickUpdate() {
|
||||||
|
if (this.immediateUpdateAllowed) {
|
||||||
|
await AppUpdate.performImmediateUpdate()
|
||||||
|
} else {
|
||||||
|
await AppUpdate.openAppStore()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<cards-book-cover :audiobook="audiobook" :width="128" />
|
<div class="relative">
|
||||||
|
<cards-book-cover :audiobook="audiobook" :width="128" />
|
||||||
|
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||||
|
</div>
|
||||||
|
<!-- <cards-book-cover :audiobook="audiobook" :width="128" /> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-3">
|
<div class="flex-grow px-3">
|
||||||
<h1 class="text-lg">{{ title }}</h1>
|
<h1 class="text-lg">{{ title }}</h1>
|
||||||
@@ -11,9 +15,18 @@
|
|||||||
<p class="text-gray-300 text-sm my-1">
|
<p class="text-gray-300 text-sm my-1">
|
||||||
{{ durationPretty }}<span class="px-4">{{ sizePretty }}</span>
|
{{ durationPretty }}<span class="px-4">{{ sizePretty }}</span>
|
||||||
</p>
|
</p>
|
||||||
<ui-btn color="success" class="flex items-center justify-center w-full mt-2" :padding-x="4" @click="playClick">
|
|
||||||
<span class="material-icons">play_arrow</span>
|
<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' : ''">
|
||||||
<span class="px-1">Play</span>
|
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
|
<p 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-btn color="success" :disabled="streaming" class="flex items-center justify-center w-full mt-4" :padding-x="4" @click="playClick">
|
||||||
|
<span v-show="!streaming" class="material-icons">play_arrow</span>
|
||||||
|
<span class="px-1">{{ streaming ? 'Streaming' : 'Play' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<div class="flex my-4"></div>
|
<div class="flex my-4"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,6 +38,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, redirect, app }) {
|
async asyncData({ params, redirect, app }) {
|
||||||
var audiobookId = params.id
|
var audiobookId = params.id
|
||||||
@@ -41,9 +56,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
resettingProgress: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
audiobookId() {
|
||||||
|
return this.audiobook.id
|
||||||
|
},
|
||||||
book() {
|
book() {
|
||||||
return this.audiobook.book || {}
|
return this.audiobook.book || {}
|
||||||
},
|
},
|
||||||
@@ -54,7 +74,7 @@ export default {
|
|||||||
return this.book.author || 'Unknown'
|
return this.book.author || 'Unknown'
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this.book.description || 'No Description'
|
return this.book.description || ''
|
||||||
},
|
},
|
||||||
series() {
|
series() {
|
||||||
return this.book.series || null
|
return this.book.series || null
|
||||||
@@ -75,6 +95,27 @@ export default {
|
|||||||
},
|
},
|
||||||
sizePretty() {
|
sizePretty() {
|
||||||
return this.audiobook.sizePretty
|
return this.audiobook.sizePretty
|
||||||
|
},
|
||||||
|
userAudiobooks() {
|
||||||
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
},
|
||||||
|
userAudiobook() {
|
||||||
|
return this.userAudiobooks[this.audiobookId] || null
|
||||||
|
},
|
||||||
|
userCurrentTime() {
|
||||||
|
return this.userAudiobook ? this.userAudiobook.currentTime : 0
|
||||||
|
},
|
||||||
|
userTimeRemaining() {
|
||||||
|
return this.duration - this.userCurrentTime
|
||||||
|
},
|
||||||
|
progressPercent() {
|
||||||
|
return this.userAudiobook ? this.userAudiobook.progress : 0
|
||||||
|
},
|
||||||
|
streamAudiobook() {
|
||||||
|
return this.$store.state.streamAudiobook
|
||||||
|
},
|
||||||
|
isStreaming() {
|
||||||
|
return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -82,8 +123,45 @@ export default {
|
|||||||
this.$store.commit('setPlayOnLoad', true)
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||||
this.$server.socket.emit('open_stream', this.audiobook.id)
|
this.$server.socket.emit('open_stream', this.audiobook.id)
|
||||||
|
},
|
||||||
|
async clearProgressClick() {
|
||||||
|
const { value } = await Dialog.confirm({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: 'Are you sure you want to reset your progress?'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.resettingProgress = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/user/audiobook/${this.audiobookId}`)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Progress reset complete')
|
||||||
|
this.$toast.success(`Your progress was reset`)
|
||||||
|
this.resettingProgress = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Progress reset failed', error)
|
||||||
|
this.resettingProgress = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
audiobookUpdated() {
|
||||||
|
console.log('Audiobook Updated - Fetch full audiobook')
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/audiobook/${this.audiobookId}`)
|
||||||
|
.then((audiobook) => {
|
||||||
|
this.audiobook = audiobook
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6">
|
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6">
|
||||||
<h2 class="text-xl leading-7 mb-4">Enter an <span class="font-book font-normal">AudioBookshelf</span><br />server address:</h2>
|
<h2 class="text-xl leading-7 mb-4">Enter an <span class="font-book font-normal">AudioBookshelf</span><br />server address:</h2>
|
||||||
<form v-show="!showAuth" @submit.prevent="submit">
|
<form v-show="!showAuth" @submit.prevent="submit" novalidate>
|
||||||
<ui-text-input v-model="serverUrl" :disabled="processing" placeholder="http://55.55.55.55:13378" type="url" class="w-60 sm:w-72 h-10" />
|
<ui-text-input v-model="serverUrl" :disabled="processing" placeholder="http://55.55.55.55:13378" type="url" class="w-60 sm:w-72 h-10" />
|
||||||
<ui-btn :disabled="processing" type="submit" :padding-x="3" class="h-10">Submit</ui-btn>
|
<ui-btn :disabled="processing" type="submit" :padding-x="3" class="h-10">Submit</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
@@ -79,6 +79,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit() {
|
async submit() {
|
||||||
|
if (!this.serverUrl.startsWith('http')) {
|
||||||
|
this.serverUrl = 'http://' + this.serverUrl
|
||||||
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.error = null
|
this.error = null
|
||||||
var success = await this.$server.check(this.serverUrl)
|
var success = await this.$server.check(this.serverUrl)
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
import { Toast } from '@capacitor/toast'
|
|
||||||
|
|
||||||
Vue.prototype.$toast = (text) => {
|
|
||||||
if (!Toast) {
|
|
||||||
return console.error('No Toast Plugin')
|
|
||||||
}
|
|
||||||
Toast.show({
|
|
||||||
text: text
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
return '0 Bytes'
|
return '0 Bytes'
|
||||||
@@ -23,6 +12,19 @@ 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) => {
|
||||||
|
var minutes = Math.floor(seconds / 60)
|
||||||
|
if (minutes < 70) {
|
||||||
|
return `${minutes} min`
|
||||||
|
}
|
||||||
|
var hours = Math.floor(minutes / 60)
|
||||||
|
minutes -= hours * 60
|
||||||
|
if (!minutes) {
|
||||||
|
return `${hours} hr`
|
||||||
|
}
|
||||||
|
return `${hours} hr ${minutes} min`
|
||||||
|
}
|
||||||
|
|
||||||
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
var _seconds = seconds
|
var _seconds = seconds
|
||||||
var _minutes = Math.floor(seconds / 60)
|
var _minutes = Math.floor(seconds / 60)
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Vue from "vue";
|
||||||
|
import Toast from "vue-toastification";
|
||||||
|
import "vue-toastification/dist/index.css";
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hideProgressBar: true,
|
||||||
|
position: 'bottom-center'
|
||||||
|
};
|
||||||
|
|
||||||
|
Vue.use(Toast, options);
|
||||||
@@ -4,22 +4,14 @@ export const state = () => ({
|
|||||||
playOnLoad: false,
|
playOnLoad: false,
|
||||||
serverUrl: null,
|
serverUrl: null,
|
||||||
user: null,
|
user: null,
|
||||||
currentVersion: null,
|
appUpdateInfo: null
|
||||||
latestVersion: null,
|
|
||||||
hasUpdate: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const actions = {}
|
export const actions = {}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setCurrentVersion(state, verObj) {
|
setAppUpdateInfo(state, info) {
|
||||||
state.currentVersion = verObj
|
state.appUpdateInfo = info
|
||||||
},
|
|
||||||
setLatestVersion(state, verObj) {
|
|
||||||
state.latestVersion = verObj
|
|
||||||
},
|
|
||||||
setHasUpdate(state, val) {
|
|
||||||
state.hasUpdate = val
|
|
||||||
},
|
},
|
||||||
closeStream(state, audiobookId) {
|
closeStream(state, audiobookId) {
|
||||||
if (state.streamAudiobook && state.streamAudiobook.id !== audiobookId) {
|
if (state.streamAudiobook && state.streamAudiobook.id !== audiobookId) {
|
||||||
|
|||||||