Compare commits

..

No commits in common. 'main' and 'mic_level' have entirely different histories.

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Practice Tracker</title> <title>Practice Tracker</title>
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
</head> </head>

1055
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,7 +9,7 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.3.4", "axios": "^1.3.4",
"vue": "^3.4.38" "vue": "^3.2.47"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",

@ -2,76 +2,18 @@ body {
margin:0; margin:0;
} }
@media (prefers-color-scheme: dark) {
body {
background-color: black;
color: white;
}
a {
color:white;
}
tbody tr:nth-child(even) td,
tbody tr:nth-child(even) td a {
color:black;
}
}
td,th { td,th {
border:1px solid black; border:1px solid black;
padding:1em; padding:1em;
} }
td ul {
padding-inline-start:0;
margin:0;
}
td li {
padding-bottom:1em;
}
th { th {
background:lightgrey; background:lightgrey;
color:black;
} }
tbody tr:nth-child(even) td{ tbody tr:nth-child(even) td{
background:#eeeeee; background:#eeeeee;
color:black;
}
nav {
text-align:right;
position:fixed;
width:100%;
pointer-events:none;
}
nav ul {
list-style:none;
} }
nav ul li { @media (min-width: 900px){
padding: 0.5em;
}
nav ul li a {
padding: 0.5em;
background-color:rgba(100,100,100, 1);
pointer-events:visible;
display:inline-block;
color:white;
border:1px solid black;
}
main > section {
overflow:auto;
}
@media (min-width: 1400px){
nav {
display:none;
}
main { main {
height:100vh; height:100vh;
@ -80,6 +22,10 @@ main > section {
grid-template-rows: repeat(2, minmax(10px, 1fr)); grid-template-rows: repeat(2, minmax(10px, 1fr));
} }
section {
overflow:auto;
}
#timer { grid-area: 1 / 1 / 3 / 2; } #timer { grid-area: 1 / 1 / 3 / 2; }
#summary { grid-area: 1 / 2 / 3 / 3; } #summary { grid-area: 1 / 2 / 3 / 3; }
#category { grid-area: 1 / 3 / 2 / 4; } #category { grid-area: 1 / 3 / 2 / 4; }

@ -1,79 +1,22 @@
<script setup> <script setup lang="ts">
import { watch, onMounted, ref } from 'vue'
import Summary from './components/summary.vue' import Summary from './components/summary.vue'
import MinsLeftToPracticeToday from './components/mins_left_to_practice_today.vue'
import ShowCategory from './components/show_category.vue' import ShowCategory from './components/show_category.vue'
import Songs from './components/songs.vue' import Songs from './components/songs.vue'
import Timer from './components/timer.vue' import Timer from './components/timer.vue'
import Status from './components/status.vue'
import {
currentWorkingCategory,
currentWorkingInstrument,
instruments,
categories
} from './libs/state.js'
const timerRunning = ref(false)
const micThresholdExceeded = ref(false)
const summaryRef = ref(null)
const statusRef = ref(null)
const showCategoryRef = ref(null)
const refreshPage = (session) => {
summaryRef.value.loadData()
statusRef.value.loadData()
showCategoryRef.value.loadCategory(session.practice_category_id)
}
const loadData = async () => {
let response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'instruments')
instruments.value = await response.json()
response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'categories')
categories.value = await response.json()
currentWorkingCategory.value = categories.value[0]
currentWorkingInstrument.value = instruments.value.find(instrument => instrument.name === currentWorkingCategory.value.instrument)
}
onMounted(loadData)
</script> </script>
<template> <template>
<nav>
<ul>
<li>
<a href="#timer">Timer</a>
</li>
<li>
<a href="#summary">Summary</a>
</li>
<li>
<a href="#category">Category</a>
</li>
<li>
<a href="#songs">Songs</a>
</li>
</ul>
</nav>
<main> <main>
<section id="timer" :class="{timerRunning:timerRunning, micThresholdExceeded:micThresholdExceeded}"> <section id="timer">
<Status ref="statusRef"/> <MinsLeftToPracticeToday/>
<Timer <Timer/>
v-if="currentWorkingInstrument"
@loggedTime="refreshPage"
:instruments="instruments"
:categories="categories"
v-model:timerRunning="timerRunning"
v-model:micThresholdExceeded="micThresholdExceeded"
/>
</section> </section>
<section id="summary"> <section id="summary">
<Summary ref="summaryRef" /> <Summary/>
</section> </section>
<section id="category"> <section id="category">
<ShowCategory <ShowCategory/>
v-if="currentWorkingInstrument"
ref="showCategoryRef"
/>
</section> </section>
<section id="songs"> <section id="songs">
<Songs/> <Songs/>
@ -82,12 +25,4 @@
</template> </template>
<style scoped> <style scoped>
.timerRunning {
background:lightgreen;
}
.timerRunning.micThresholdExceeded {
background:darkgreen;
color:white;
}
</style> </style>

@ -1,47 +0,0 @@
<script setup>
import { onMounted, defineProps, ref } from 'vue';
import {
currentWorkingCategory,
currentWorkingInstrument,
instruments,
categories
} from '../libs/state.js'
const selectFirstInstrumentOfCategory = () => {
currentWorkingCategory.value = categories.value.find(category => category.instrument === currentWorkingInstrument.value.name)
}
const categoryInput = ref(null)
onMounted(()=>{
window.addEventListener('keydown', (event)=>{
if(event.ctrlKey === true){
if(event.key === 'c') {
categoryInput.value.focus();
}
}
})
})
</script>
<template>
<select ref="categoryInput" @change="selectFirstInstrumentOfCategory" v-model="currentWorkingInstrument">
<option :value="instrument" v-for="instrument in instruments">
{{instrument.name}}
</option>
</select>
<select v-model="currentWorkingCategory">
<option
v-for="category in categories.filter(currentCategory => currentCategory.instrument === currentWorkingInstrument.name)"
:value="category">
{{category.category}}
</option>
</select>
</template>
<style scoped>
select {
padding:1em;
}
</style>

@ -0,0 +1,37 @@
<script lang="ts">
import axios from 'axios';
export default {
data() {
return {
time_left:0,
mins_practiced_today:0
}
},
methods: {
refresh(){
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'mins-left-to-practice-today').then((response)=>{
this.time_left= response.data.time_left;
this.mins_practiced_today = response.data.mins_practiced_today;
})
}
},
mounted() {
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'mins-left-to-practice-today').then((response)=>{
this.time_left= response.data.time_left;
this.mins_practiced_today = response.data.mins_practiced_today;
})
}
}
</script>
<template>
<h2 @click="refresh">Time Summary</h2>
<h3>Mins Left to Practice Today</h3>
{{time_left}}
<h3>Mins Practiced Today</h3>
{{mins_practiced_today}}
</template>
<style scoped>
</style>

@ -1,76 +1,64 @@
<script setup> <script lang="ts">
import { formatSeconds } from '../libs/time.js' import axios from 'axios';
import CategoryChooser from './category_chooser.vue' export default {
import { onMounted, ref, watch, defineProps } from 'vue' data() {
import { return {
description, categories:[],
comments, categorySessions:[]
currentWorkingCategory,
currentWorkingInstrument
} from '../libs/state.js'
const categorySessions = ref([])
const copy = (event) => {
description.value = event.target.innerText
}
const copyComments = (event) => {
comments.value = event.target.innerText
}
onMounted(()=>{
loadCategory()
window.addEventListener('keydown', (event)=>{
if(event.ctrlKey === true){
if(event.key === 'l') {
description.value = categorySessions.value[0].description
} else if (event.key === 'm') {
comments.value = categorySessions.value[0].comments
}
} }
}) },
}) methods: {
change(event){
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'show-category/'+event.target.value).then((response)=>{
const loadCategory = async () => { this.categorySessions = response.data;
const response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'show-category/'+currentWorkingCategory.value.id) })
categorySessions.value = await response.json() }
},
mounted() {
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'categories').then((response)=>{
this.categories = response.data
})
}
} }
watch(currentWorkingCategory, loadCategory)
defineExpose({loadCategory})
</script> </script>
<template> <template>
<h2>Show Category</h2> <h2>Show Category</h2>
<CategoryChooser /> <select @change="change">
<option v-for="category in categories" v-bind:value="category.id">
{{category.id}}.
{{category.instrument}}
:
{{category.category}}
</option>
</select>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>description</th> <th>id</th>
<th>comments</th> <th>description</th>
<th>songs</th> <th>seconds</th>
<th>duration</th> <th>comments</th>
<th>created at</th> <th>created at</th>
</tr> <th>category id</th>
<th>category</th>
<th>instrument</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="session in categorySessions"> <tr v-for="session in categorySessions">
<td @click="copy">{{session.description}}</td> <td>{{session.id}}</td>
<td @click="copyComments">{{session.comments}}</td> <td>{{session.description}}</td>
<td> <td>{{session.seconds}}</td>
<ul> <td>{{session.comments}}</td>
<li v-for="song in session.songs"> <td>{{session.created_at}}</td>
<a v-bind:href="'#/songs/' + song.id"> <td>{{session.category_id}}</td>
{{song.title}} <td>{{session.category}}</td>
</a> <td>{{session.instrument}}</td>
</li>
</ul>
</td>
<td>{{formatSeconds(session.seconds)}}</td>
<td>{{new Date(session.created_at).toLocaleString("en-US")}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</template> </template>
<style scoped>
</style>

@ -1,25 +1,20 @@
<script setup> <script lang="ts">
import { onMounted, ref } from 'vue' import axios from 'axios';
import { copiedSongs } from '../libs/state.js'
const songs = ref([]) export default {
data() {
onMounted(async ()=>{ return {
const response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'songs') songs:[]
songs.value = await response.json() }
}) },
const copy = (title) => { mounted() {
navigator.clipboard.writeText(title); axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'songs').then((response)=>{
} this.songs = response.data
const link = (song) => { })
if(copiedSongs.value.findIndex(copiedSong=>copiedSong.id === song.id) === -1){
copiedSongs.value.push(song)
} }
} }
const unlink = (song) => {
copiedSongs.value = copiedSongs.value.filter(currSong => currSong.id !== song.id)
}
</script> </script>
<template> <template>
<h2>Songs</h2> <h2>Songs</h2>
<table> <table>
@ -28,39 +23,17 @@
<th>id</th> <th>id</th>
<th>title</th> <th>title</th>
<th>notes</th> <th>notes</th>
<th>lyrics</th>
<th>original</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="song in songs"> <tr v-for="song in songs">
<td>{{song.id}}</td> <td>{{song.id}}</td>
<td> <td>{{song.title}}</td>
<a v-bind:name="'/songs/' + song.id">
{{song.title}}
</a>
<br/>
<button @click="link(song)" v-if="copiedSongs.findIndex(currSong => currSong.id === song.id) === -1">Link</button>
<button @click="unlink(song)" v-if="copiedSongs.findIndex(currSong => currSong.id === song.id) !== -1">X</button>
</td>
<td>{{song.notes}}</td> <td>{{song.notes}}</td>
<td>
<a v-if="song.lyrics_link" target="lyrics" v-bind:href="song.lyrics_link">lyrics</a>
</td>
<td>
<a v-if="song.original_link" target="original" v-bind:href="song.original_link">original</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</template> </template>
<style scoped> <style scoped>
button {
padding: 1em 2em;
margin: 1em;
}
td:nth-child(2){
text-align:center;
}
</style> </style>

@ -1,23 +0,0 @@
<script setup>
import { formatSeconds } from '../libs/time.js'
import { onMounted, ref, defineExpose } from 'vue'
const practiceStatus = ref({})
const loadData = async () => {
const response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'status')
practiceStatus.value = await response.json()
}
onMounted(loadData)
defineExpose({loadData})
</script>
<template>
<h2 @click="loadData">Practice Status</h2>
<h3>Time Left to Practice Today</h3>
{{formatSeconds(practiceStatus.seconds_left_to_practice_today)}}
<h3>Extra Time to Accumulate</h3>
{{formatSeconds(practiceStatus.seconds_left_to_get_ahead)}}
<h3>Time Practiced Today</h3>
{{formatSeconds(practiceStatus.seconds_practiced)}}
</template>

@ -1,32 +1,29 @@
<script setup> <script lang="ts">
import { onMounted, ref, defineExpose } from 'vue' import axios from 'axios';
import {
currentWorkingCategory,
currentWorkingInstrument,
instruments,
categories
} from '../libs/state.js'
const summary = ref([]) export default {
data() {
const loadData = async () => { return {
const response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'summary') categories:[]
summary.value = await response.json() }
} },
methods: {
onMounted(loadData) refresh(){
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'summary').then((response)=>{
const setWorkingCategory = (choice) => { this.categories = response.data
const chosenCategory = categories.value.find(category => category.id === choice.category_id) })
currentWorkingCategory.value = chosenCategory }
currentWorkingInstrument.value = instruments.value.find(instrument => instrument.name === chosenCategory.instrument) },
mounted() {
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'summary').then((response)=>{
this.categories = response.data
})
}
} }
defineExpose({loadData})
</script> </script>
<template> <template>
<h2 @click="loadData">Summary</h2> <h2 @click="refresh">Summary</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -38,7 +35,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="category in summary" @click="setWorkingCategory(category)"> <tr v-for="category in categories">
<td>{{category.category_id}}</td> <td>{{category.category_id}}</td>
<td>{{category.category}}</td> <td>{{category.category}}</td>
<td>{{category.chunks_practiced}}</td> <td>{{category.chunks_practiced}}</td>
@ -48,3 +45,6 @@
</tbody> </tbody>
</table> </table>
</template> </template>
<style scoped>
</style>

@ -1,258 +1,179 @@
<script setup> <script lang="ts">
import { onMounted, getCurrentInstance, ref } from 'vue' import axios from 'axios';
const micThresholdExceeded = defineModel('micThresholdExceeded')
const timerRunning = defineModel('timerRunning')
const { proxy } = getCurrentInstance(); const padDigits = (value) => {
if(value < 10){
return '0'+value;
} else {
return value;
}
}
const descriptionInput = ref(null) const getMinutes = (seconds) => {
return Math.floor( seconds / 60 );
}
onMounted(()=>{ const getAccumulatedSeconds = (newerTime, olderTime) => {
window.addEventListener('keydown', (event)=>{ return Math.floor((newerTime-olderTime)/1000);
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' const formatSeconds = (total) => {
import { getHours, getMinutes, createTimeObj, getAccumulatedSeconds, formatSeconds } from '../libs/time.js' const minutes = getMinutes(total);
import { const seconds = total - minutes*60;
copiedSongs, return `${minutes}:${padDigits(seconds)}`;
currentWorkingCategory, }
currentWorkingInstrument,
description,
comments
} from '../libs/state.js'
export default { export default {
data() { data() {
return { return {
submitting:false, categories:[],
running: false,
startTime:0, startTime:0,
savedPreviousSeconds:0, savedPreviousSeconds:0,
totalSeconds:0, totalSeconds:0,
description:'',
practice_category_id:0,
comments:'', comments:'',
manualHours:0, manualHours:0,
manualMinutes:0, manualMinutes:0,
manualSeconds:0, manualSeconds:0,
analyser:null, analyser:null,
secondsToSubtract:0, secondsToSubtract:0,
micThreshold:-1, micThreshold:0,
micLevel:0, micLevel:0,
practice_category_id:0 micThresholdExceeded:false
} }
}, },
emits: ['loggedTime'],
components: {
CategoryChooser
},
methods: { methods: {
formatSeconds, formatTime(seconds){
return formatSeconds(seconds)
},
submit(event){ submit(event){
event.preventDefault(); event.preventDefault();
if(this.submitting){
return
} else {
this.submitting = true
}
const reqBody = { const reqBody = {
description:description.value, description:this.description,
seconds: this.totalSeconds, seconds: this.totalSeconds,
practice_category_id: currentWorkingCategory.value.id, practice_category_id: this.practice_category_id
}
if(copiedSongs.value.length > 0){
reqBody.songs = copiedSongs.value
} }
if(comments.value){ if(this.comments){
reqBody.comments = comments.value reqBody.comments = this.comments
} }
axios.post( axios.post(
import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'sessions', import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'sessions',
reqBody reqBody
).then(()=>{ ).then(()=>{
description.value = null; this.description = null;
copiedSongs.value = []
this.totalSeconds = 0; this.totalSeconds = 0;
this.savedPreviousSeconds = 0; this.savedPreviousSeconds = 0;
comments.value = null; 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){ start(event){
if(this.timerRunning === false){ this.startTime = Date.now();
this.startTime = Date.now(); this.running = true;
this.$emit('update:timerRunning', true) this.intervalID = setInterval(()=>{
this.intervalID = setInterval(()=>{
const array = new Uint8Array(this.analyser.frequencyBinCount);
if(this.micThreshold > -1){ this.analyser.getByteFrequencyData(array);
const arraySum = array.reduce((a, value) => a + value, 0);
const array = new Uint8Array(this.analyser.frequencyBinCount); this.micLevel = arraySum / array.length;
this.analyser.getByteFrequencyData(array);
const arraySum = array.reduce((a, value) => a + value, 0); if(this.micLevel > this.micThreshold){
this.micLevel = arraySum / array.length; this.micThresholdExceeded = true;
} else {
if(this.micLevel < this.micThreshold){ this.secondsToSubtract++;
this.secondsToSubtract++; this.micThresholdExceeded = false;
} }
} this.totalSeconds = getAccumulatedSeconds(Date.now(), this.startTime) - this.secondsToSubtract + this.savedPreviousSeconds;
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
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; }, 1000);
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){ updateSavedPreviousSeconds(event){
this.savedPreviousSeconds = parseInt(event.target.value); this.savedPreviousSeconds = parseInt(event.target.value);
this.setManualTime(this.totalSeconds)
}, },
stop(event){ stop(event){
if(this.timerRunning === true){ this.running = false;
this.$emit('update:timerRunning', false) this.savedPreviousSeconds += getAccumulatedSeconds(Date.now(), this.startTime);
this.savedPreviousSeconds += getAccumulatedSeconds(Date.now(), this.startTime); clearInterval(this.intervalID);
clearInterval(this.intervalID);
}
}, },
reset(event){ reset(event){
this.savedPreviousSeconds = 0; this.savedPreviousSeconds = 0;
this.totalSeconds = 0; this.totalSeconds = 0;
this.secondsToSubtract = 0;
this.manualHours = 0
this.manualMinutes = 0
this.manualSeconds = 0
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
}, },
setSecondsManually(){ setSecondsManually(){
this.totalSeconds = this.manualHours*60*60 + this.manualMinutes*60 + this.manualSeconds; this.totalSeconds = this.manualHours*60*60 + this.manualMinutes*60 + this.manualSeconds;
this.savedPreviousSeconds = this.totalSeconds; this.savedPreviousSeconds = this.totalSeconds;
this.secondsToSubtract = 0;
window.localStorage.setItem('lastTotalSeconds', this.totalSeconds); window.localStorage.setItem('lastTotalSeconds', this.totalSeconds);
}, }
activateMic(){ },
mounted() {
if(this.micThreshold > -1){ axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'categories').then((response)=>{
this.categories = response.data
navigator.mediaDevices.getUserMedia({ })
audio: true
}).then((stream) => {
const audioContext = new AudioContext(); navigator.mediaDevices.getUserMedia({
this.analyser = audioContext.createAnalyser(); audio: true
const microphone = audioContext.createMediaStreamSource(stream); }).then((stream) => {
const scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1);
this.analyser.smoothingTimeConstant = 0.8; const audioContext = new AudioContext();
this.analyser.fftSize = 1024; this.analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1);
microphone.connect(this.analyser); this.analyser.smoothingTimeConstant = 0.8;
this.analyser.connect(scriptProcessor); this.analyser.fftSize = 1024;
scriptProcessor.connect(audioContext.destination);
}) microphone.connect(this.analyser);
.catch((err) => { this.analyser.connect(scriptProcessor);
console.error(err); 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> </script>
<template> <template>
<h2>Timer</h2> <h2>Timer</h2>
<div> <div :class="{running:running, micThresholdExceeded:micThresholdExceeded}">
<em> <em>
{{formatSeconds(totalSeconds)}} {{formatTime(totalSeconds)}}
</em> </em>
Mic Threshold (current level: {{Math.round(micLevel)}}): Mic Threshold (current level: {{Math.round(micLevel)}}): <input type="number" v-model="micThreshold"/>
<input type="number" @change="activateMic" v-model="micThreshold"/> <button :disabled="running" @click="start">Start</button>
<button :disabled="timerRunning" @click="start">Start</button> <button :disabled="!running" @click="stop">Stop</button>
<button :disabled="!timerRunning" @click="stop">Stop</button> <button :disabled="running || totalSeconds === 0" @click="reset">Reset</button>
<button :disabled="timerRunning || totalSeconds === 0" @click="reset">Reset</button>
</div> </div>
<form @submit="submit"> <form @submit="submit">
<label>Description</label> <label>Description</label>
<input v-model="description" ref="descriptionInput" type="text" maxlength="128"/> <input v-model="description" 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> <label>Seconds</label>
<input @change="updateSavedPreviousSeconds" v-model="totalSeconds" type="number"/> <input @change="updateSavedPreviousSeconds" v-model="totalSeconds" type="number"/>
<label>Practice Category</label>
<CategoryChooser />
<input :disabled="submitting" type="submit"/>
<label>Comments</label> <label>Comments</label>
<textarea v-model="comments"/> <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>
<input type="Submit"/>
<label>Hours:</label> <label>Hours:</label>
<input type="number" @change="setSecondsManually" v-model="manualHours"/> <input type="number" @change="setSecondsManually" v-model="manualHours"/>
<label>Minutes:</label> <label>Minutes:</label>
@ -267,11 +188,15 @@ label, [type="submit"] {
display:block; display:block;
} }
input[type="number"] { .running {
width: 5em; background:lightgreen;
} }
button, em, [type="submit"]{ .running.micThresholdExceeded {
background:green;
}
button, em {
margin: 1em 0; margin: 1em 0;
display:block; display:block;
font-size:3em; font-size:3em;
@ -282,5 +207,4 @@ button, em, [type="submit"]{
select { select {
width: 100%; width: 100%;
} }
</style> </style>

@ -1,8 +0,0 @@
import { ref } from 'vue'
export const copiedSongs = ref([])
export const currentWorkingCategory = ref(null)
export const currentWorkingInstrument = ref(null)
export const instruments = ref([])
export const categories = ref([])
export const description = ref(null)
export const comments = ref(null)

@ -1,33 +0,0 @@
const getHours = (seconds) => {
return Math.floor( seconds / 60 / 60 );
}
const getMinutes = (seconds) => {
return Math.floor( seconds / 60 );
}
const createTimeObj = (seconds) => {
const hours = getHours(seconds)
let remainingSeconds = seconds - hours * 60 * 60
const minutes = getMinutes(remainingSeconds)
remainingSeconds -= minutes * 60
return { hours, minutes, seconds: remainingSeconds }
}
const getAccumulatedSeconds = (newerTime, olderTime) => {
return Math.floor((newerTime-olderTime)/1000);
}
const formatSeconds = (total, includeHours = true) => {
const timeObj = createTimeObj(Math.abs(total))
let result = (total < 0) ? '-' : ''
if(includeHours) {
result += `${timeObj.hours}:${timeObj.minutes.toString().padStart(2,'0')}`
} else {
result += `${timeObj.hours*60 + timeObj.minutes}`
}
result += `:${timeObj.seconds.toString().padStart(2,'0')}`
return result
}
export { getHours, getMinutes, createTimeObj, getAccumulatedSeconds, formatSeconds }
Loading…
Cancel
Save