Check app version, fix close stream bug, show audiobook progress, add toasts

This commit is contained in:
advplyr 2021-09-04 12:31:00 -05:00
parent 98076927ff
commit 619b6f3686
51 changed files with 551 additions and 141 deletions

View file

@ -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)
}
})
}
}

View file

@ -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.

View file

@ -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')
}

View file

@ -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"
}
]

View file

@ -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)

View file

@ -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()
}
}

View file

@ -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() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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>

View file

@ -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')

View file

@ -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
View 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;
}

View file

@ -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()
}

View file

@ -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: {

View file

@ -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>

View 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>

View file

@ -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>

View 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>

View file

@ -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 {}

View file

@ -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

View file

@ -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()

View file

@ -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
View file

@ -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",

View file

@ -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"
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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
View 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);

View file

@ -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) {