|
|
|
|
@ -1,120 +1,177 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
<script setup>
|
|
|
|
|
import { onMounted, getCurrentInstance, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
const padDigits = (value) => {
|
|
|
|
|
if(value < 10){
|
|
|
|
|
return '0'+value;
|
|
|
|
|
} else {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const micThresholdExceeded = defineModel('micThresholdExceeded')
|
|
|
|
|
const timerRunning = defineModel('timerRunning')
|
|
|
|
|
|
|
|
|
|
const getMinutes = (seconds) => {
|
|
|
|
|
return Math.floor( seconds / 60 );
|
|
|
|
|
}
|
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
|
|
|
|
|
const getAccumulatedSeconds = (newerTime, olderTime) => {
|
|
|
|
|
return Math.floor((newerTime-olderTime)/1000);
|
|
|
|
|
}
|
|
|
|
|
const descriptionInput = ref(null)
|
|
|
|
|
|
|
|
|
|
const formatSeconds = (total) => {
|
|
|
|
|
const minutes = getMinutes(total);
|
|
|
|
|
const seconds = total - minutes*60;
|
|
|
|
|
return `${minutes}:${padDigits(seconds)}`;
|
|
|
|
|
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,
|
|
|
|
|
comments
|
|
|
|
|
} from '../libs/state.js'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
categories:[],
|
|
|
|
|
running: false,
|
|
|
|
|
submitting:false,
|
|
|
|
|
startTime:0,
|
|
|
|
|
savedPreviousSeconds:0,
|
|
|
|
|
totalSeconds:0,
|
|
|
|
|
description:'',
|
|
|
|
|
practice_category_id:0,
|
|
|
|
|
comments:'',
|
|
|
|
|
manualHours:0,
|
|
|
|
|
manualMinutes:0,
|
|
|
|
|
manualSeconds:0,
|
|
|
|
|
analyser:null,
|
|
|
|
|
secondsToSubtract:0,
|
|
|
|
|
micThreshold:0,
|
|
|
|
|
micThreshold:-1,
|
|
|
|
|
micLevel:0,
|
|
|
|
|
micThresholdExceeded:false
|
|
|
|
|
practice_category_id:0
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
formatTime(seconds){
|
|
|
|
|
return formatSeconds(seconds)
|
|
|
|
|
emits: ['loggedTime'],
|
|
|
|
|
components: {
|
|
|
|
|
CategoryChooser
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
formatSeconds,
|
|
|
|
|
submit(event){
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if(this.submitting){
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
this.submitting = true
|
|
|
|
|
}
|
|
|
|
|
const reqBody = {
|
|
|
|
|
description:this.description,
|
|
|
|
|
description:description.value,
|
|
|
|
|
seconds: this.totalSeconds,
|
|
|
|
|
practice_category_id: this.practice_category_id
|
|
|
|
|
practice_category_id: currentWorkingCategory.value.id,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(this.comments){
|
|
|
|
|
reqBody.comments = this.comments
|
|
|
|
|
if(copiedSongs.value.length > 0){
|
|
|
|
|
reqBody.songs = copiedSongs.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(comments.value){
|
|
|
|
|
reqBody.comments = comments.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
axios.post(
|
|
|
|
|
import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'sessions',
|
|
|
|
|
reqBody
|
|
|
|
|
).then(()=>{
|
|
|
|
|
this.description = null;
|
|
|
|
|
description.value = null;
|
|
|
|
|
copiedSongs.value = []
|
|
|
|
|
this.totalSeconds = 0;
|
|
|
|
|
this.savedPreviousSeconds = 0;
|
|
|
|
|
this.comments = null;
|
|
|
|
|
comments.value = 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.running = true;
|
|
|
|
|
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.micThresholdExceeded = true;
|
|
|
|
|
} else {
|
|
|
|
|
if(this.micLevel < this.micThreshold){
|
|
|
|
|
this.secondsToSubtract++;
|
|
|
|
|
this.micThresholdExceeded = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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){
|
|
|
|
|
this.running = false;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'categories').then((response)=>{
|
|
|
|
|
this.categories = response.data
|
|
|
|
|
})
|
|
|
|
|
activateMic(){
|
|
|
|
|
|
|
|
|
|
if(this.micThreshold > -1){
|
|
|
|
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia({
|
|
|
|
|
audio: true
|
|
|
|
|
@ -138,41 +195,63 @@
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
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 :class="{running:running, micThresholdExceeded:micThresholdExceeded}">
|
|
|
|
|
<div>
|
|
|
|
|
<em>
|
|
|
|
|
{{formatTime(totalSeconds)}}
|
|
|
|
|
{{formatSeconds(totalSeconds)}}
|
|
|
|
|
</em>
|
|
|
|
|
Mic Threshold (current level: {{Math.round(micLevel)}}): <input type="number" v-model="micThreshold"/>
|
|
|
|
|
<button :disabled="running" @click="start">Start</button>
|
|
|
|
|
<button :disabled="!running" @click="stop">Stop</button>
|
|
|
|
|
<button :disabled="running || totalSeconds === 0" @click="reset">Reset</button>
|
|
|
|
|
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" type="text" maxlength="128"/>
|
|
|
|
|
<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>Comments</label>
|
|
|
|
|
<textarea v-model="comments"/>
|
|
|
|
|
|
|
|
|
|
<label>Practice Category</label>
|
|
|
|
|
<select v-model="practice_category_id">
|
|
|
|
|
<option v-for="category in categories" v-bind:value="category.id">
|
|
|
|
|
{{category.id}}.
|
|
|
|
|
{{category.instrument}}
|
|
|
|
|
:
|
|
|
|
|
{{category.category}}
|
|
|
|
|
</option>
|
|
|
|
|
</select>
|
|
|
|
|
<CategoryChooser />
|
|
|
|
|
<input :disabled="submitting" type="submit"/>
|
|
|
|
|
|
|
|
|
|
<input type="Submit"/>
|
|
|
|
|
<label>Comments</label>
|
|
|
|
|
<textarea v-model="comments"/>
|
|
|
|
|
|
|
|
|
|
<label>Hours:</label>
|
|
|
|
|
<input type="number" @change="setSecondsManually" v-model="manualHours"/>
|
|
|
|
|
@ -188,15 +267,11 @@ label, [type="submit"] {
|
|
|
|
|
display:block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.running {
|
|
|
|
|
background:lightgreen;
|
|
|
|
|
input[type="number"] {
|
|
|
|
|
width: 5em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.running.micThresholdExceeded {
|
|
|
|
|
background:green;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button, em {
|
|
|
|
|
button, em, [type="submit"]{
|
|
|
|
|
margin: 1em 0;
|
|
|
|
|
display:block;
|
|
|
|
|
font-size:3em;
|
|
|
|
|
@ -207,4 +282,5 @@ button, em {
|
|
|
|
|
select {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|