You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
7.3 KiB
286 lines
7.3 KiB
<script setup>
|
|
import { onMounted, getCurrentInstance, ref } from 'vue'
|
|
|
|
const micThresholdExceeded = defineModel('micThresholdExceeded')
|
|
const timerRunning = defineModel('timerRunning')
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
const descriptionInput = ref(null)
|
|
|
|
onMounted(()=>{
|
|
window.addEventListener('keydown', (event)=>{
|
|
if(event.key === 'ArrowDown'){
|
|
event.preventDefault()
|
|
proxy.start()
|
|
} else if (event.key === 'ArrowUp'){
|
|
event.preventDefault()
|
|
proxy.stop()
|
|
}
|
|
if(event.ctrlKey === true){
|
|
if(event.key === 'Enter'){
|
|
proxy.submit(event)
|
|
} else if(event.key === 'd') {
|
|
descriptionInput.value.focus()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
</script>
|
|
<script>
|
|
import axios from 'axios';
|
|
|
|
import CategoryChooser from './category_chooser.vue'
|
|
import { getHours, getMinutes, createTimeObj, getAccumulatedSeconds, formatSeconds } from '../libs/time.js'
|
|
import {
|
|
copiedSongs,
|
|
currentWorkingCategory,
|
|
currentWorkingInstrument,
|
|
description
|
|
} from '../libs/state.js'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
submitting:false,
|
|
startTime:0,
|
|
savedPreviousSeconds:0,
|
|
totalSeconds:0,
|
|
comments:'',
|
|
manualHours:0,
|
|
manualMinutes:0,
|
|
manualSeconds:0,
|
|
analyser:null,
|
|
secondsToSubtract:0,
|
|
micThreshold:-1,
|
|
micLevel:0,
|
|
practice_category_id:0
|
|
}
|
|
},
|
|
emits: ['loggedTime'],
|
|
components: {
|
|
CategoryChooser
|
|
},
|
|
methods: {
|
|
formatSeconds,
|
|
submit(event){
|
|
event.preventDefault();
|
|
if(this.submitting){
|
|
return
|
|
} else {
|
|
this.submitting = true
|
|
}
|
|
const reqBody = {
|
|
description:description.value,
|
|
seconds: this.totalSeconds,
|
|
practice_category_id: currentWorkingCategory.value.id,
|
|
}
|
|
|
|
if(copiedSongs.value.length > 0){
|
|
reqBody.songs = copiedSongs.value
|
|
}
|
|
|
|
if(this.comments){
|
|
reqBody.comments = this.comments
|
|
}
|
|
|
|
axios.post(
|
|
import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'sessions',
|
|
reqBody
|
|
).then(()=>{
|
|
description.value = null;
|
|
copiedSongs.value = []
|
|
this.totalSeconds = 0;
|
|
this.savedPreviousSeconds = 0;
|
|
this.comments = null;
|
|
this.secondsToSubtract = 0;
|
|
this.manualHours = 0
|
|
this.manualMinutes = 0
|
|
this.manualSeconds = 0
|
|
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
|
|
this.$emit('loggedTime', reqBody)
|
|
this.submitting = false
|
|
},()=>{
|
|
this.submitting = false
|
|
});
|
|
|
|
},
|
|
start(event){
|
|
if(this.timerRunning === false){
|
|
this.startTime = Date.now();
|
|
this.$emit('update:timerRunning', true)
|
|
this.intervalID = setInterval(()=>{
|
|
|
|
if(this.micThreshold > -1){
|
|
|
|
const array = new Uint8Array(this.analyser.frequencyBinCount);
|
|
this.analyser.getByteFrequencyData(array);
|
|
const arraySum = array.reduce((a, value) => a + value, 0);
|
|
this.micLevel = arraySum / array.length;
|
|
|
|
if(this.micLevel < this.micThreshold){
|
|
this.secondsToSubtract++;
|
|
}
|
|
|
|
}
|
|
|
|
if(this.micLevel >= this.micThreshold){
|
|
this.$emit('update:micThresholdExceeded', true)
|
|
} else {
|
|
this.$emit('update:micThresholdExceeded', false)
|
|
}
|
|
|
|
this.totalSeconds = getAccumulatedSeconds(Date.now(), this.startTime) - this.secondsToSubtract + this.savedPreviousSeconds;
|
|
this.setManualTime(this.totalSeconds)
|
|
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
|
|
}, 1000);
|
|
}
|
|
},
|
|
setManualTime(seconds){
|
|
const timeObj = createTimeObj(this.totalSeconds)
|
|
this.manualHours = timeObj.hours
|
|
this.manualMinutes = timeObj.minutes
|
|
this.manualSeconds = timeObj.seconds
|
|
},
|
|
updateSavedPreviousSeconds(event){
|
|
this.savedPreviousSeconds = parseInt(event.target.value);
|
|
this.setManualTime(this.totalSeconds)
|
|
},
|
|
stop(event){
|
|
if(this.timerRunning === true){
|
|
this.$emit('update:timerRunning', false)
|
|
this.savedPreviousSeconds += getAccumulatedSeconds(Date.now(), this.startTime);
|
|
clearInterval(this.intervalID);
|
|
}
|
|
},
|
|
reset(event){
|
|
this.savedPreviousSeconds = 0;
|
|
this.totalSeconds = 0;
|
|
this.secondsToSubtract = 0;
|
|
this.manualHours = 0
|
|
this.manualMinutes = 0
|
|
this.manualSeconds = 0
|
|
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
|
|
},
|
|
setSecondsManually(){
|
|
this.totalSeconds = this.manualHours*60*60 + this.manualMinutes*60 + this.manualSeconds;
|
|
this.savedPreviousSeconds = this.totalSeconds;
|
|
this.secondsToSubtract = 0;
|
|
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
|
|
},
|
|
activateMic(){
|
|
|
|
if(this.micThreshold > -1){
|
|
|
|
navigator.mediaDevices.getUserMedia({
|
|
audio: true
|
|
}).then((stream) => {
|
|
|
|
const audioContext = new AudioContext();
|
|
this.analyser = audioContext.createAnalyser();
|
|
const microphone = audioContext.createMediaStreamSource(stream);
|
|
const scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1);
|
|
|
|
this.analyser.smoothingTimeConstant = 0.8;
|
|
this.analyser.fftSize = 1024;
|
|
|
|
microphone.connect(this.analyser);
|
|
this.analyser.connect(scriptProcessor);
|
|
scriptProcessor.connect(audioContext.destination);
|
|
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
confirmClose(event){
|
|
if(this.totalSeconds > 0){
|
|
event.returnValue = "Clear or save your time"
|
|
return false
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
this.practice_category_id = currentWorkingCategory.value.id
|
|
if(window.localStorage.getItem('lastTotalSeconds')){
|
|
this.totalSeconds = parseInt(window.localStorage.getItem('lastTotalSeconds'))
|
|
} else {
|
|
this.totalSeconds = 0
|
|
}
|
|
this.setManualTime(this.totalSeconds)
|
|
this.savedPreviousSeconds = this.totalSeconds
|
|
window.onbeforeunload = this.confirmClose
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<h2>Timer</h2>
|
|
<div>
|
|
<em>
|
|
{{formatSeconds(totalSeconds)}}
|
|
</em>
|
|
Mic Threshold (current level: {{Math.round(micLevel)}}):
|
|
<input type="number" @change="activateMic" v-model="micThreshold"/>
|
|
<button :disabled="timerRunning" @click="start">Start</button>
|
|
<button :disabled="!timerRunning" @click="stop">Stop</button>
|
|
<button :disabled="timerRunning || totalSeconds === 0" @click="reset">Reset</button>
|
|
</div>
|
|
<form @submit="submit">
|
|
<label>Description</label>
|
|
<input v-model="description" ref="descriptionInput" type="text" maxlength="128"/>
|
|
|
|
<label>Songs</label>
|
|
<ul v-if="copiedSongs.length > 0">
|
|
<li v-for="song in copiedSongs">
|
|
<a v-bind:href="'#/songs/' + song.id">
|
|
{{song.title}}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<label>Seconds</label>
|
|
<input @change="updateSavedPreviousSeconds" v-model="totalSeconds" type="number"/>
|
|
|
|
<label>Practice Category</label>
|
|
<CategoryChooser />
|
|
<input :disabled="submitting" type="submit"/>
|
|
|
|
<label>Comments</label>
|
|
<textarea v-model="comments"/>
|
|
|
|
<label>Hours:</label>
|
|
<input type="number" @change="setSecondsManually" v-model="manualHours"/>
|
|
<label>Minutes:</label>
|
|
<input type="number" @change="setSecondsManually" v-model="manualMinutes"/>
|
|
<label>Seconds:</label>
|
|
<input type="number" @change="setSecondsManually" v-model="manualSeconds"/>
|
|
</form>
|
|
</template>
|
|
|
|
<style scoped>
|
|
label, [type="submit"] {
|
|
display:block;
|
|
}
|
|
|
|
input[type="number"] {
|
|
width: 5em;
|
|
}
|
|
|
|
button, em, [type="submit"]{
|
|
margin: 1em 0;
|
|
display:block;
|
|
font-size:3em;
|
|
font-style:normal;
|
|
font-weight:bold;
|
|
width:100%;
|
|
}
|
|
select {
|
|
width: 100%;
|
|
}
|
|
|
|
</style>
|