Check app version, fix close stream bug, show audiobook progress, add toasts
|
@ -33,6 +33,7 @@ class Server extends EventEmitter {
|
|||
setUser(user) {
|
||||
this.user = user
|
||||
this.store.commit('user/setUser', user)
|
||||
this.store.commit('user/setSettings', user.settings)
|
||||
if (user) {
|
||||
localStorage.setItem('userToken', user.token)
|
||||
} else {
|
||||
|
@ -149,6 +150,11 @@ class Server extends EventEmitter {
|
|||
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"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 3
|
||||
versionName "0.1.0-beta"
|
||||
versionCode 4
|
||||
versionName "0.2.0-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// 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"
|
||||
dependencies {
|
||||
implementation project(':capacitor-dialog')
|
||||
implementation project(':capacitor-toast')
|
||||
implementation project(':robingenz-capacitor-app-update')
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/toast",
|
||||
"classpath": "com.capacitorjs.plugins.toast.ToastPlugin"
|
||||
"pkg": "@robingenz/capacitor-app-update",
|
||||
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -13,6 +13,7 @@ class Audiobook {
|
|||
var cover:String = ""
|
||||
var playWhenReady:Boolean = false
|
||||
var startTime:Long = 0
|
||||
var playbackSpeed:Float = 1f
|
||||
var duration:Long = 0
|
||||
|
||||
var hasPlayerLoaded:Boolean = false
|
||||
|
@ -30,6 +31,7 @@ class Audiobook {
|
|||
playlistUrl = jsondata.getString("playlistUrl", "").toString()
|
||||
playWhenReady = jsondata.getBoolean("playWhenReady", false) == true
|
||||
startTime = jsondata.getString("startTime", "0")!!.toLong()
|
||||
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
||||
duration = jsondata.getString("duration", "0")!!.toLong()
|
||||
|
||||
playlistUri = Uri.parse(playlistUrl)
|
||||
|
|
|
@ -102,16 +102,27 @@ class MyNativeAudio : Plugin() {
|
|||
}
|
||||
|
||||
@PluginMethod
|
||||
fun seekForward10(call: PluginCall) {
|
||||
fun seekForward(call: PluginCall) {
|
||||
var amount:Long = call.getString("amount", "0")!!.toLong()
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.seekForward10()
|
||||
playerNotificationService.seekForward(amount)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
@PluginMethod
|
||||
fun seekBackward10(call: PluginCall) {
|
||||
fun seekBackward(call: PluginCall) {
|
||||
var amount:Long = call.getString("amount", "0")!!.toLong()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -373,6 +373,7 @@ class PlayerNotificationService : Service() {
|
|||
mPlayer.setMediaSource(mediaSource, true)
|
||||
mPlayer.prepare()
|
||||
mPlayer.playWhenReady = currentAudiobook!!.playWhenReady
|
||||
mPlayer.setPlaybackSpeed(audiobook.playbackSpeed)
|
||||
}
|
||||
|
||||
|
||||
|
@ -396,12 +397,16 @@ class PlayerNotificationService : Service() {
|
|||
mPlayer.seekTo(time)
|
||||
}
|
||||
|
||||
fun seekForward10() {
|
||||
mPlayer.seekTo(mPlayer.currentPosition + 10000)
|
||||
fun seekForward(amount:Long) {
|
||||
mPlayer.seekTo(mPlayer.currentPosition + amount)
|
||||
}
|
||||
|
||||
fun seekBackward10() {
|
||||
mPlayer.seekTo(mPlayer.currentPosition - 10000)
|
||||
fun seekBackward(amount:Long) {
|
||||
mPlayer.seekTo(mPlayer.currentPosition - amount)
|
||||
}
|
||||
|
||||
fun setPlaybackSpeed(speed:Float) {
|
||||
mPlayer.setPlaybackSpeed(speed)
|
||||
}
|
||||
|
||||
fun terminateStream() {
|
||||
|
|
BIN
android/app/src/main/res/drawable-land-hdpi/screen.png
Normal file
After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 26 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/screen.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/screen.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/screen.png
Normal file
After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 70 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/screen.png
Normal file
After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 64 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/screen.png
Normal file
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 26 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/screen.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/screen.png
Normal file
After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/screen.png
Normal file
After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 68 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/screen.png
Normal file
After Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 63 KiB |
BIN
android/app/src/main/res/drawable/screen.png
Normal file
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -17,6 +17,6 @@
|
|||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
<item name="android:background">@drawable/screen</item>
|
||||
</style>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -5,5 +5,5 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||
include ':capacitor-dialog'
|
||||
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
||||
|
||||
include ':capacitor-toast'
|
||||
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
|
||||
include ':robingenz-capacitor-app-update'
|
||||
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
|
||||
|
|
|
@ -6,6 +6,7 @@ ext {
|
|||
androidxAppCompatVersion = '1.2.0'
|
||||
androidxCoordinatorLayoutVersion = '1.1.0'
|
||||
androidxCoreVersion = '1.6.0'
|
||||
androidPlayCore = '1.9.0'
|
||||
androidxFragmentVersion = '1.3.0'
|
||||
junitVersion = '4.13.1'
|
||||
androidxJunitVersion = '1.1.2'
|
||||
|
|
15
assets/app.css
Normal file
|
@ -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">
|
||||
<span class="material-icons text-3xl">forward_10</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-8" @mousedown.prevent @mouseup.prevent>
|
||||
<span class="font-mono text-lg uppercase">1x</span>
|
||||
<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">{{ playbackRate }}x</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -58,8 +58,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
totalDuration: 0,
|
||||
currentPlaybackRate: 1,
|
||||
currentTime: 0,
|
||||
isTerminated: false,
|
||||
isResetting: false,
|
||||
initObject: null,
|
||||
stateName: 'idle',
|
||||
playInterval: null,
|
||||
|
@ -77,17 +78,24 @@ export default {
|
|||
computed: {
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
},
|
||||
playbackRate() {
|
||||
return this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePlaybackRate() {
|
||||
this.currentPlaybackRate = this.playbackRate
|
||||
MyNativeAudio.setPlaybackSpeed({ speed: this.playbackRate })
|
||||
},
|
||||
restart() {
|
||||
this.seek(0)
|
||||
},
|
||||
backward10() {
|
||||
MyNativeAudio.seekBackward10()
|
||||
MyNativeAudio.seekBackward({ amount: '10000' })
|
||||
},
|
||||
forward10() {
|
||||
MyNativeAudio.seekForward10()
|
||||
MyNativeAudio.seekForward({ amount: '10000' })
|
||||
},
|
||||
sendStreamUpdate() {
|
||||
this.$emit('updateTime', this.currentTime)
|
||||
|
@ -191,7 +199,9 @@ export default {
|
|||
}
|
||||
},
|
||||
set(audiobookStreamData) {
|
||||
this.isResetting = false
|
||||
this.initObject = { ...audiobookStreamData }
|
||||
this.currentPlaybackRate = this.initObject.playbackSpeed
|
||||
MyNativeAudio.initPlayer(this.initObject)
|
||||
},
|
||||
setFromObj() {
|
||||
|
@ -199,6 +209,7 @@ export default {
|
|||
console.error('Cannot set from obj')
|
||||
return
|
||||
}
|
||||
this.isResetting = false
|
||||
MyNativeAudio.initPlayer(this.initObject)
|
||||
},
|
||||
play() {
|
||||
|
@ -220,13 +231,17 @@ export default {
|
|||
stopPlayInterval() {
|
||||
clearInterval(this.playInterval)
|
||||
},
|
||||
terminateStream(startTime) {
|
||||
resetStream(startTime) {
|
||||
var _time = String(Math.floor(startTime * 1000))
|
||||
if (!this.initObject) {
|
||||
console.error('Terminate stream when no init object is set...')
|
||||
return
|
||||
}
|
||||
this.isResetting = true
|
||||
this.initObject.currentTime = _time
|
||||
this.terminateStream()
|
||||
},
|
||||
terminateStream() {
|
||||
MyNativeAudio.terminateStream()
|
||||
},
|
||||
init() {
|
||||
|
@ -245,7 +260,7 @@ export default {
|
|||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||
this.stateName = data.stateName
|
||||
|
||||
if (this.stateName === 'ended' && this.isTerminated) {
|
||||
if (this.stateName === 'ended' && this.isResetting) {
|
||||
this.setFromObj()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
<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">
|
||||
<template v-for="audiobook in shelf">
|
||||
<div :key="audiobook.id" class="relative px-4">
|
||||
<nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
||||
<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" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</nuxt-link> -->
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||
</div>
|
||||
|
@ -40,6 +41,9 @@ export default {
|
|||
},
|
||||
hasFilters() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
||||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
<div class="absolute left-2 -top-10">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
|
||||
</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>
|
||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -25,7 +26,9 @@ export default {
|
|||
return {
|
||||
audioPlayerReady: false,
|
||||
stream: null,
|
||||
lastServerUpdateSentSeconds: 0
|
||||
lastServerUpdateSentSeconds: 0,
|
||||
showPlaybackSpeedModal: false,
|
||||
playbackSpeed: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -85,6 +88,10 @@ export default {
|
|||
})
|
||||
if (value) {
|
||||
this.$server.socket.emit('close_stream')
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTime(currentTime) {
|
||||
|
@ -122,7 +129,7 @@ export default {
|
|||
streamReset({ streamId, startTime }) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
if (this.stream && this.stream.id === streamId) {
|
||||
this.$refs.audioPlayerMini.terminateStream(startTime)
|
||||
this.$refs.audioPlayerMini.resetStream(startTime)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -133,10 +140,6 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
if (this.stream && this.stream.id !== stream.id) {
|
||||
console.error('STREAM CHANGED', this.stream.id, stream.id)
|
||||
}
|
||||
|
||||
this.stream = stream
|
||||
|
||||
var playlistUrl = stream.clientPlaylistUri
|
||||
|
@ -149,6 +152,7 @@ export default {
|
|||
author: this.author,
|
||||
playWhenReady: !!playOnLoad,
|
||||
startTime: String(Math.floor(currentTime * 1000)),
|
||||
playbackSpeed: this.playbackSpeed || 1,
|
||||
cover: this.coverForNative,
|
||||
duration: String(Math.floor(this.duration * 1000)),
|
||||
series: this.seriesTxt,
|
||||
|
@ -156,8 +160,6 @@ export default {
|
|||
token: this.$store.getters['user/getToken']
|
||||
}
|
||||
|
||||
console.log('audiobook stream data', audiobookStreamData.token, JSON.stringify(audiobookStreamData))
|
||||
|
||||
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
||||
},
|
||||
audioPlayerMounted() {
|
||||
|
@ -177,11 +179,31 @@ export default {
|
|||
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||
this.$server.socket.on('stream_ready', this.streamReady)
|
||||
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() {
|
||||
console.warn('Stream Container Mounted')
|
||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
|
||||
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>
|
118
components/cards/BookCard.vue
Normal file
|
@ -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>
|
||||
</div>
|
||||
<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 />
|
||||
</div>
|
||||
</div>
|
||||
|
|
62
components/modals/PlaybackSpeedModal.vue
Normal file
|
@ -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>
|
||||
<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 />
|
||||
<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> -->
|
||||
|
@ -23,7 +23,8 @@ export default {
|
|||
},
|
||||
paddingX: Number,
|
||||
small: Boolean,
|
||||
loading: Boolean
|
||||
loading: Boolean,
|
||||
disabled: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
|
|
@ -10,7 +10,6 @@ def capacitor_pods
|
|||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
||||
pod 'CapacitorToast', :path => '../../node_modules/@capacitor/toast'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
|
|
@ -9,18 +9,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||
|
||||
export default {
|
||||
middleware: 'authenticated',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
// watch: {
|
||||
// routeName(newVal, oldVal) {
|
||||
// if (newVal === 'connect' && this.$server.connected) {
|
||||
// this.$router.replace('/')
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
computed: {
|
||||
streaming() {
|
||||
return this.$store.state.streamAudiobook
|
||||
|
@ -46,63 +41,95 @@ export default {
|
|||
this.$refs.streamContainer.streamOpen(stream)
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
async clickUpdateToast() {
|
||||
var immediateUpdateAllowed = this.$store.state.appUpdateInfo.immediateUpdateAllowed
|
||||
if (immediateUpdateAllowed) {
|
||||
await AppUpdate.performImmediateUpdate()
|
||||
} else {
|
||||
console.warn('Invalid semver string', ver)
|
||||
await AppUpdate.openAppStore()
|
||||
}
|
||||
return null
|
||||
},
|
||||
checkForUpdate() {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
showUpdateToast(availableVersion, immediateUpdateAllowed) {
|
||||
var toastText = immediateUpdateAllowed ? `Click here to update` : `Click here to open app store`
|
||||
this.$toast.info(`Update is available for v${availableVersion}! ${toastText}`, {
|
||||
draggable: false,
|
||||
hideProgressBar: false,
|
||||
timeout: 10000,
|
||||
closeButton: false,
|
||||
onClick: this.clickUpdateToast()
|
||||
})
|
||||
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
|
||||
}
|
||||
this.$store.commit('setLatestVersion', largestVer)
|
||||
if (largestVer.total > currVerObj.total) {
|
||||
console.log('Has Update!', largestVer.version)
|
||||
this.$store.commit('setHasUpdate', true)
|
||||
this.$store.commit('setAppUpdateInfo', result)
|
||||
|
||||
if (result.updateAvailability === 2) {
|
||||
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() {
|
||||
if (!this.$server) return console.error('No Server')
|
||||
|
@ -114,6 +141,8 @@ export default {
|
|||
this.$router.push('/connect')
|
||||
}
|
||||
|
||||
this.checkForUpdate()
|
||||
|
||||
// var checkForUpdateFlag = localStorage.getItem('checkForUpdate')
|
||||
// if (!checkForUpdateFlag || checkForUpdateFlag !== '1') {
|
||||
// this.checkForUpdate()
|
||||
|
|
|
@ -32,13 +32,15 @@ export default {
|
|||
},
|
||||
|
||||
css: [
|
||||
'@/assets/app.css'
|
||||
],
|
||||
|
||||
plugins: [
|
||||
{ src: '~/plugins/server.js', mode: 'client' },
|
||||
'@/plugins/server.js',
|
||||
'@/plugins/init.client.js',
|
||||
'@/plugins/axios.js',
|
||||
'@/plugins/my-native-audio.js'
|
||||
'@/plugins/my-native-audio.js',
|
||||
'@/plugins/toast.js'
|
||||
],
|
||||
|
||||
components: true,
|
||||
|
|
15
package-lock.json
generated
|
@ -1061,11 +1061,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.2.2.tgz",
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.2.10",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.1.0-beta",
|
||||
"version": "v0.2.0-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
@ -15,13 +15,14 @@
|
|||
"@capacitor/core": "^3.1.2",
|
||||
"@capacitor/dialog": "^1.0.3",
|
||||
"@capacitor/ios": "^3.2.2",
|
||||
"@capacitor/toast": "^1.0.2",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.15.1",
|
||||
"hls.js": "^1.0.9",
|
||||
"nuxt": "^2.15.7",
|
||||
"socket.io-client": "^4.1.3"
|
||||
"socket.io-client": "^4.1.3",
|
||||
"vue-toastification": "^1.7.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.13.15",
|
||||
|
@ -29,4 +30,4 @@
|
|||
"@nuxtjs/tailwindcss": "^4.2.0",
|
||||
"postcss": "^8.3.5"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,11 +17,15 @@
|
|||
</svg>
|
||||
</a>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
|
@ -35,9 +39,32 @@ export default {
|
|||
},
|
||||
serverUrl() {
|
||||
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() {}
|
||||
}
|
||||
</script>
|
|
@ -2,7 +2,11 @@
|
|||
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
||||
<div class="flex">
|
||||
<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 class="flex-grow px-3">
|
||||
<h1 class="text-lg">{{ title }}</h1>
|
||||
|
@ -11,9 +15,18 @@
|
|||
<p class="text-gray-300 text-sm my-1">
|
||||
{{ durationPretty }}<span class="px-4">{{ sizePretty }}</span>
|
||||
</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>
|
||||
<span class="px-1">Play</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' : ''">
|
||||
<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>
|
||||
<div class="flex my-4"></div>
|
||||
</div>
|
||||
|
@ -25,6 +38,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
|
||||
export default {
|
||||
async asyncData({ params, redirect, app }) {
|
||||
var audiobookId = params.id
|
||||
|
@ -41,9 +56,14 @@ export default {
|
|||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
resettingProgress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
audiobookId() {
|
||||
return this.audiobook.id
|
||||
},
|
||||
book() {
|
||||
return this.audiobook.book || {}
|
||||
},
|
||||
|
@ -54,7 +74,7 @@ export default {
|
|||
return this.book.author || 'Unknown'
|
||||
},
|
||||
description() {
|
||||
return this.book.description || 'No Description'
|
||||
return this.book.description || ''
|
||||
},
|
||||
series() {
|
||||
return this.book.series || null
|
||||
|
@ -75,6 +95,27 @@ export default {
|
|||
},
|
||||
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: {
|
||||
|
@ -82,8 +123,45 @@ export default {
|
|||
this.$store.commit('setPlayOnLoad', true)
|
||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||
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>
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<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>
|
||||
<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-btn :disabled="processing" type="submit" :padding-x="3" class="h-10">Submit</ui-btn>
|
||||
</form>
|
||||
|
@ -79,6 +79,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
if (!this.serverUrl.startsWith('http')) {
|
||||
this.serverUrl = 'http://' + this.serverUrl
|
||||
}
|
||||
this.processing = true
|
||||
this.error = null
|
||||
var success = await this.$server.check(this.serverUrl)
|
||||
|
|
|
@ -1,17 +1,6 @@
|
|||
import Vue from 'vue'
|
||||
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) => {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes'
|
||||
|
@ -23,6 +12,19 @@ Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
|||
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) => {
|
||||
var _seconds = seconds
|
||||
var _minutes = Math.floor(seconds / 60)
|
||||
|
|
10
plugins/toast.js
Normal file
|
@ -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,
|
||||
serverUrl: null,
|
||||
user: null,
|
||||
currentVersion: null,
|
||||
latestVersion: null,
|
||||
hasUpdate: true
|
||||
appUpdateInfo: null
|
||||
})
|
||||
|
||||
export const actions = {}
|
||||
|
||||
export const mutations = {
|
||||
setCurrentVersion(state, verObj) {
|
||||
state.currentVersion = verObj
|
||||
},
|
||||
setLatestVersion(state, verObj) {
|
||||
state.latestVersion = verObj
|
||||
},
|
||||
setHasUpdate(state, val) {
|
||||
state.hasUpdate = val
|
||||
setAppUpdateInfo(state, info) {
|
||||
state.appUpdateInfo = info
|
||||
},
|
||||
closeStream(state, audiobookId) {
|
||||
if (state.streamAudiobook && state.streamAudiobook.id !== audiobookId) {
|
||||
|
|