Cleanup UX

This commit is contained in:
Daniel O'Connor 2025-02-01 05:15:09 +00:00
commit 00823d8417
2 changed files with 70 additions and 19 deletions

View file

@ -1,9 +1,12 @@
<template> <template>
<span class="v-btn__content"> <div v-if="ttsSupported" class="v-btn__content">
<i v-if="!playing" aria-hidden="true" class="v-icon notranslate mdi mdi-play theme--dark" @click.stop="play"></i> <i v-if="canPlay" aria-hidden="true" class="v-icon notranslate mdi mdi-play theme--dark" @click.stop="play"></i>
<i v-if="playing" aria-hidden="true" class="v-icon notranslate mdi mdi-pause theme--dark" @click.stop="pause"></i> <i v-if="canPause" aria-hidden="true" class="v-icon notranslate mdi mdi-pause theme--dark" @click.stop="pause"></i>
<i v-if="playing" aria-hidden="true" class="v-icon notranslate mdi mdi-pause theme--dark" @click.stop="resume"></i> <i v-if="canResume" aria-hidden="true" class="v-icon notranslate mdi mdi-play theme--dark" @click.stop="resume"></i>
</span> <i v-if="canStartAgain" aria-hidden="true" class="v-icon notranslate mdi mdi-restart theme--dark" @click.stop="play"></i>
<small @click.stop="pauseOrResume">{{ currentSentence }}</small>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -20,7 +23,9 @@ export default defineComponent({
}, },
setup() { setup() {
onUnmounted(() => { onUnmounted(() => {
speechSynthesis.stop(); if (speechSynthesis) {
speechSynthesis.cancel();
}
}); });
}, },
data() { data() {
@ -28,19 +33,57 @@ export default defineComponent({
playing: false, playing: false,
played: false, played: false,
paused: false, paused: false,
utterance: null, utterance: SpeechSynthesisUtterance,
currentIndex: 0 currentIndex: 0
} }
}, },
computed: {
ttsSupported() {
return !!speechSynthesis;
},
canPlay() {
return !this.playing && !this.paused;
},
canPause() {
return this.playing && !this.paused;
},
canResume() {
return !this.playing && this.paused;
},
canStartAgain() {
return this.playing || this.paused;
},
nextIndex() {
// TODO: i18n stopwords in sync with the browser's implementation? Assumes . and ! are boundaries, may not be true for all languages.
const match = this.step.text.slice(this.currentIndex).search(/[\.\!]/)
if (match === -1) {
return this.currentIndex;
}
return this.currentIndex + match;
},
currentSentence() {
if (this.playing || this.paused) {
return this.step.text.slice(this.currentIndex, this.nextIndex);
}
return "";
}
},
methods: { methods: {
pauseOrResume() {
if (this.paused) {
this.resume();
} else {
this.pause();
}
},
play() { play() {
const self = this; const self = this;
if (this.utterance && this.playing) { if (this.utterance && this.playing) {
speechSynthesis.cancel(); speechSynthesis.cancel();
} }
this.utterance = new SpeechSynthesisUtterance(this.step.text); const utterance = new SpeechSynthesisUtterance(this.step.text);
this.utterance.onstart = (event) => { utterance.onstart = (event) => {
self.playing = true; self.playing = true;
self.played = false; self.played = false;
self.paused = false; self.paused = false;
@ -48,26 +91,26 @@ export default defineComponent({
console.debug("Now playing: " + this.step.text); console.debug("Now playing: " + this.step.text);
}; };
this.utterance.onend = () => { utterance.onend = () => {
self.playing = false; self.playing = false;
self.played = true; self.played = true;
self.paused = false; self.paused = false;
// self.$emit("playing", false); self.$emit("ttscompleted", true);
console.debug("Playback complete"); console.debug("Playback complete");
}; };
this.utterance.onpause = () => { utterance.onpause = () => {
self.playing = false; self.playing = false;
self.paused = true; self.paused = true;
}; };
this.utterance.onresume = () => { utterance.onresume = () => {
self.playing = true; self.playing = true;
self.paused = false; self.paused = false;
}; };
this.utterance.onboundary = (event) => { utterance.onboundary = (event) => {
// Update the start of the current sentence. // Update the start of the current sentence.
if (event.name === "sentence") { if (event.name === "sentence") {
self.currentIndex = event.charIndex; self.currentIndex = event.charIndex;
@ -78,13 +121,20 @@ export default defineComponent({
// console.log("mark") // console.log("mark")
// console.log(event) // console.log(event)
// }; // };
this.utterance.onerror = (event) => { utterance.onerror = (event) => {
console.error("Error in playback") if (event.error == "interrupted") {
console.debug(event) this.playing = false;
this.played = false;
this.paused = false;
} else {
console.error("Error in playback")
console.debug(event);
}
}; };
speechSynthesis.speak(this.utterance); speechSynthesis.speak(utterance);
return true;
this.utterance = utterance;
}, },
pause() { pause() {
speechSynthesis.pause(); speechSynthesis.pause();

View file

@ -122,6 +122,7 @@
:ripple="false" :ripple="false"
@click="toggleDisabled(index)" @click="toggleDisabled(index)"
> >
<!-- TODO: Should a ttscompleted event mark the step 'done'? Or auto scroll? -->
<v-card-title :class="{ 'pb-0': !isChecked(index) }"> <v-card-title :class="{ 'pb-0': !isChecked(index) }">
<v-text-field <v-text-field
v-if="isEditForm" v-if="isEditForm"