Skip to content

Commit

Permalink
Play audio on Inbox page
Browse files Browse the repository at this point in the history
Fixes #172
  • Loading branch information
akdasa committed Feb 26, 2023
1 parent 669eae5 commit b5e8124
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 73 deletions.
5 changes: 4 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<template>
<ion-app>
<ion-router-outlet />

<!-- Global audio player -->
<AudioPlayer />
</ion-app>
</template>

<script lang="ts" setup>
import { IonApp, IonRouterOutlet } from '@ionic/vue'
import { AudioPlayer } from '@/app/shared'
// Notch!
try {
Expand All @@ -20,5 +24,4 @@ try {
style.appendChild(document.createTextNode(css))
}
} catch(e) { console.log(e) }
</script>
13 changes: 12 additions & 1 deletion src/app/home/components/HomePage.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<template>
<ion-page>
<ion-tabs>
<ion-tabs
@ion-tabs-did-change="onTabChanged"
>
<ion-router-outlet />

<!-- Tabs -->
<ion-tab-bar
slot="bottom"
Expand Down Expand Up @@ -72,7 +75,15 @@ import {
import { storeToRefs } from 'pinia'
import { useInboxDeckStore } from '@/app/decks/inbox'
import { useReviewDeckStore } from '@/app/decks/review'
import { useAudioPlayerStore } from '@/app/shared'
const { count: inboxDeckCount } = storeToRefs(useInboxDeckStore())
const { count: reviewDeckCount } = storeToRefs(useReviewDeckStore())
const audioPlayer = useAudioPlayerStore()
/* -------------------------------------------------------------------------- */
/* Handlers */
/* -------------------------------------------------------------------------- */
function onTabChanged() { audioPlayer.stop() }
</script>
1 change: 1 addition & 0 deletions src/app/library/components/AddVerseDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ function onAddClicked() {
.player {
margin-bottom: 10px;
border-radius: 5px;
background-color: var(--ion-color-light);
border: 1px solid var(--ion-color-light-shade);
color: var(--ion-color-medium-contrast);
Expand Down
3 changes: 3 additions & 0 deletions src/app/library/components/LibraryPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { Application } from '@akdasa-studios/shlokas-core'
import { EventEmitter2 } from 'eventemitter2'
import { testId } from '@/app/TestId'
import { AddVerseDialog, useAddVerse, useLibrary } from '@/app/library'
import { useAudioPlayerStore } from '@/app/shared'
/* -------------------------------------------------------------------------- */
Expand All @@ -90,13 +91,15 @@ const app = inject('app') as Application
const emitter = inject('emitter') as EventEmitter2
const { toastVerseAdded, dialogAddVerse, addVerseToInbox, revert } = useAddVerse(app)
const { searchQuery, filteredVerses } = useLibrary(app, emitter)
const { close } = useAudioPlayerStore()
/* -------------------------------------------------------------------------- */
/* Handlers */
/* -------------------------------------------------------------------------- */
async function onVerseDialogDismiss(action: string) {
dialogAddVerse.close()
close()
if (action === 'confirm') {
await addVerseToInbox(dialogAddVerse.data.value)
}
Expand Down
67 changes: 67 additions & 0 deletions src/app/shared/components/AudioPlayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<audio
ref="audioRef"
:src="store.uri.value"
:loop="store.loop.value"
/>
</template>


<script lang="ts" setup>
import { useMediaControls, syncRef } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'
import { MediaSession } from '@jofr/capacitor-media-session'
import { useAudioPlayerStore } from '@/app/shared'
const audioRef = ref()
const store = storeToRefs(useAudioPlayerStore())
const player = useMediaControls(audioRef, { src: store.uri })
// If the audio file changes, stop the player
watch(store.uri, () => { player.playing.value = false })
// If the title or artist changes, update the media session
// TODO: artwork is not working on iOS for some reason
// https://github.com/akdasa-studios/shlokas/issues/174
watch([store.title, store.artist], () => {
MediaSession.setMetadata({
title: store.title.value,
artist: store.artist.value,
// artwork: [
// { src: 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/png' },
// ]
})
})
// Sync the player state with the store and vice versa
syncRef(store.playing, player.playing)
syncRef(store.duration, player.duration, { direction: 'rtl' })
syncRef(store.time, player.currentTime, { direction: 'rtl' })
</script>


<doc lang="md">
# AudioPlayer
Global audio player component. This component is used to play audio files in the
app. It is a wrapper around the `<audio>` tag. It controlled by `audioPlayer`
store.

## Usage
1. Put the component somewhere in the app. For example, in `App.vue`:
```html
<template>
<ion-app>
<ion-router-outlet></ion-router-outlet>
<AudioPlayer />
</ion-app>
</template>
```

2. Use the `useAudioPlayerStore` store to control the player:
```ts
const player = useAudioPlayerStore()
player.open(uri, title, artist)
```
</doc>

102 changes: 34 additions & 68 deletions src/app/shared/components/VersePlayer.vue
Original file line number Diff line number Diff line change
@@ -1,111 +1,81 @@
<template>
<div
class="root"
@click.stop="play"
>
<div class="root">
<ion-icon
v-if="!playing"
:icon="playFilled"
:icon="playCircle"
size="large"
color="dark"
@click.stop="play"
@click.stop="audioPlayer.play"
/>
<ion-icon
v-else
:icon="stopFilled"
:icon="stopCircle"
color="dark"
size="large"
@click.stop="stop"
@click.stop="audioPlayer.stop"
/>
<ion-icon
v-if="props.showRepeatButton"
:icon="repeatIcon"
:icon="reloadCircle"
size="large"
:color="isLooped ? 'primary' : 'medium'"
@click.stop="changeMode"
:color="loop ? 'primary' : 'medium'"
@click.stop="audioPlayer.toggleLoop"
/>
<ion-progress-bar
v-if="props.showProgressBar"
:value="progressValue"
:value="progress"
:type="progressType"
color="dark"
class="progressBar"
/>
<audio
ref="audio"
:src="audioUri"
:loop="isLooped"
/>
</div>
</template>

<script lang="ts" setup>
import { computed, nextTick, ref, defineProps, watch } from 'vue'
import { useMediaControls } from '@vueuse/core'
import { playCircle as playFilled, stopCircle as stopFilled, reloadCircle as repeatIcon } from 'ionicons/icons'
import { computed, defineProps, watch, toRefs } from 'vue'
import { playCircle, stopCircle, reloadCircle } from 'ionicons/icons'
import { IonProgressBar , IonIcon } from '@ionic/vue'
import { MediaSession } from '@jofr/capacitor-media-session'
import { DownloadService } from '@/services/DownloadService'
import { storeToRefs } from 'pinia'
import { useAudioPlayerStore, useDownloadService } from '@/app/shared'
/* -------------------------------------------------------------------------- */
/* Interface */
/* -------------------------------------------------------------------------- */
const props = defineProps<{
uri: string,
title: string,
artist: string,
title?: string,
artist?: string,
showProgressBar?: boolean,
showRepeatButton?: boolean
}>()
watch(() => props.uri, async () => {
stop()
currentTime.value = 0
})
/* -------------------------------------------------------------------------- */
/* State */
/* -------------------------------------------------------------------------- */
const service = new DownloadService()
const downloadService = useDownloadService()
const audioPlayer = useAudioPlayerStore()
const { uri } = toRefs(props)
const { playing, loop, progress } = storeToRefs(audioPlayer)
const progressType = computed(() => downloadService.isDownloading.value ? 'indeterminate' : 'determinate')
const audio = ref()
const audioUri = ref('')
const isDownloading = ref(false)
const isLooped = ref(false)
const progressValue = computed(() => currentTime.value / duration.value || 0)
const progressType = computed(() => isDownloading.value ? 'indeterminate' : 'determinate')
const {
playing, currentTime, duration
} = useMediaControls(audio, { src: audioUri })
/* -------------------------------------------------------------------------- */
/* Handlers */
/* -------------------------------------------------------------------------- */
async function play() {
// Download the audio file if it's not already downloaded
// and get the local file URI
isDownloading.value = true
audioUri.value = await service.download(props.uri)
isDownloading.value = false
watch(uri, async (value) => onUriChanged(value), { immediate: true })
// Play the audio
nextTick(() => playing.value = true)
// Update the media session
MediaSession.setMetadata({
title: props.title,
artist: props.artist,
// artwork is not working on iOS for some reason :(
// artwork: [
// { src: 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/png' },
// ]
})
}
function stop() { playing.value = false }
function changeMode() {
isLooped.value = !isLooped.value
async function onUriChanged(uri: string) {
const localUri = await downloadService.download(uri)
audioPlayer.open(localUri, props.title, props.artist)
}
</script>


<style lang="scss" scoped>
.root {
padding: .5rem;
border-radius: 5px;
display: flex;
align-items: center;
justify-items: baseline;
Expand All @@ -115,8 +85,4 @@ function changeMode() {
margin-left: .5rem;
margin-right: .5rem;
}
.off {
opacity: .5;
}
</style>
5 changes: 4 additions & 1 deletion src/app/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { default as DarkImage } from './DarkImage.vue'
export { default as AudioPlayer } from './components/AudioPlayer.vue'
export * from './tasks/RefreshTokenTask'
export * from './tasks/SyncTask'
export { default as VersePlayer } from './components/VersePlayer.vue'
export { default as VersePlayer } from './components/VersePlayer.vue'
export * from './stores/audioPlayerStore'
export * from './services/downloadService'
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Capacitor } from '@capacitor/core'
import { Directory, Filesystem } from '@capacitor/filesystem'
import write_blob from 'capacitor-blob-writer'
import { ref } from 'vue'


export function useDownloadService() {
const isDownloading = ref(false)

export class DownloadService {
/**
* Download a file from a URL and return device-specific URI
* what can be used to play the file with <audio> tag.
* @param url Url of the file to download.
* @returns The URI of the downloaded file.
*/
async download(url: string): Promise<string> {
async function download(url: string): Promise<string> {
// We don't need to download the file if we're on the web
// because we can just use the URL directly to play the audio
// file.
Expand All @@ -31,6 +35,7 @@ export class DownloadService {
// We need to download the file if we're on a mobile device
// because users should be able to play the audio file even
// if they're offline.
isDownloading.value = true
const res = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'audio/mpeg' },
Expand All @@ -51,6 +56,11 @@ export class DownloadService {
path: filePath,
directory: Directory.Data
})
isDownloading.value = false
return Capacitor.convertFileSrc(uri.uri)
}

return {
download, isDownloading
}
}
Loading

0 comments on commit b5e8124

Please sign in to comment.