Compare commits

..

105 Commits

Author SHA1 Message Date
Matt Huntington bade40c59e Merge branch 'main' of https://gitea.artisan.al/mahuntington/practice-tracker-frontend
1 month ago
Matt Huntington 87ecf2ed11 adjusting breakpoint in css
1 month ago
Matthew Huntington 0f38a25075 adjusting breakpoint in css
1 month ago
Matthew Huntington e9a7483f5a shorcut to copy comments
3 months ago
Matthew Huntington 19d7a922c0 click to duplicate comments
3 months ago
Matthew Huntington 3e619cb27f links to song, not remove when clicked
3 months ago
Matthew Huntington b693e68b9c oops
3 months ago
Matthew Huntington c8dad66310 lyrics/original links for songs
3 months ago
Matthew Huntington ee95ad89ee force description to be entered 1st entry too
4 months ago
Matthew Huntington 33262aa2dd styling
4 months ago
Matthew Huntington 64cc2f326b styling
4 months ago
Matthew Huntington bc99e16a4c reorder columns in show category
4 months ago
Matthew Huntington 4078377ce5 disable timer form submit while posting. wait until it returns
4 months ago
Matthew Huntington 4de311abb4 ctrl+l loads category's latest session's description as description for current session
5 months ago
Matthew Huntington 7cc7562d2a removing unnecessary copy calls
5 months ago
Matthew Huntington 83f0112c8e link/unlink in songs component
5 months ago
Matthew Huntington 5e4a3b5bdf wording
5 months ago
mahuntington 566bbb0f46 Merge pull request 'practiced-songs' (#1) from practiced-songs into main
6 months ago
Matthew Huntington 4628d0a686 link colors in dark mode
6 months ago
Matthew Huntington ba8a12b081 use song id for links between show category and songs
6 months ago
Matthew Huntington 3bc4f41f48 send songs in request body, not description
6 months ago
Matthew Huntington c628a7ebb5 link from show category to songs
6 months ago
Matthew Huntington 1efb99d2a0 show songs practiced
6 months ago
Matthew Huntington 225c9d5e20 shortcut for focusing on cateogry chooser
7 months ago
Matthew Huntington bfc4e0c29f shortcut to move to description
10 months ago
Matthew Huntington aa10ef8176 better meta tag
10 months ago
Matthew Huntington 6fda78da7e cmd+Enter submits timer
10 months ago
Matthew Huntington 100137b7b1 overflow auto for <section> in mobile too
10 months ago
Matthew Huntington aa4a52166d clicking on category description pastes in description in timer
11 months ago
Matthew Huntington 7a07a455d2 more legible css in dark mode
11 months ago
Matthew Huntington 218cbc9e39 fixing nav clickthrough
11 months ago
Matthew Huntington c7e780f716 moving instruments/categories to global state
11 months ago
Matthew Huntington 7d9f05a758 diff
11 months ago
Matthew Huntington 8abc6c24b1 styling for submit/select
11 months ago
Matthew Huntington bde7379c65 styling for songs buttons
11 months ago
Matthew Huntington 2c1b1fc821 button styling in show category
11 months ago
Matthew Huntington 581e49a341 paste songs into description
11 months ago
Matthew Huntington cc9542b3a9 converting songs to new vue3 syntax. Adding copy/paste buttons
11 months ago
Matthew Huntington 00d43e9bc3 nav backgrounds
12 months ago
Matthew Huntington 5eca98b272 nav backgrounds
12 months ago
Matthew Huntington eb1d210ec5 nav backgrounds
12 months ago
Matthew Huntington b0c16d4abc show nav only in mobile
12 months ago
Matthew Huntington b2361c60ef nav
12 months ago
Matthew Huntington 6a48e789d1 dealing with NaN when starting in incognito
12 months ago
Matthew Huntington ddea612f3a when timer runs, entire #timer section turns green, not just Timer component
1 year ago
Matthew Huntington c61458e64d submit/reset timer clears manual inputs
1 year ago
mahuntington 43db528fa9 Update 'src/components/timer.vue'
1 year ago
mahuntington 0fd0d5be7d Update 'src/components/timer.vue'
1 year ago
mahuntington 5241492be5 Update 'src/components/timer.vue'
1 year ago
mahuntington 819decca07 Update 'src/components/timer.vue'
1 year ago
Matthew Huntington 93c0f304a8 up/down arrow start/stop timer
1 year ago
Matthew Huntington 404cbf12aa fixing issue with v-if and category chooser
1 year ago
Matthew Huntington 88583ddbfb only single click needed on summary now
1 year ago
Matthew Huntington cbcae5b355 cleanup
1 year ago
Matthew Huntington 1b2b28d451 switch to first category of instrument when instrument is changed
1 year ago
Matthew Huntington 8aaa97fbb8 loggin hours works again
1 year ago
Matthew Huntington ab6a9425d0 getting category chooser to work with timer
1 year ago
Matthew Huntington b7690e1265 using v-model for show category
1 year ago
Matthew Huntington ed4a78f373 using v-model on summary
1 year ago
Matthew Huntington 78482dabf9 updating vue
1 year ago
Matthew Huntington c0b0ee390f format seconds for show category
1 year ago
Matthew Huntington aa21ecb6f5 timer category chooser changes correctly
1 year ago
Matthew Huntington 88f5185d4e cleanup
1 year ago
Matthew Huntington 7d275bdc2b category chooser mostly works in timer
1 year ago
Matthew Huntington 654bc1168d category choose might work in timer
1 year ago
Matthew Huntington b51a8f4746 cleanup
1 year ago
Matthew Huntington a8dd2274d1 category chooser might actually be done
1 year ago
Matthew Huntington a0642e86cc category chooser mostly working
1 year ago
Matthew Huntington 03dc8d1bb4 App gets categories via AJAX and sends to component chooser
1 year ago
Matthew Huntington 79a1f6aaf0 App gets instruments via AJAX and sends to component chooser
1 year ago
Matthew Huntington a62c96f665 category chooser changes when category id prop changes
1 year ago
Matthew Huntington d4cd8a768d taking out old category chooser in show category component
1 year ago
Matthew Huntington 37378a67ac category chooser emits id
1 year ago
Matthew Huntington 9cebd803e6 category chooser started
1 year ago
Matthew Huntington f8cea3a543 changing seconds updates manual time entry
1 year ago
Matthew Huntington ec002cc020 show category for practice session just logged
1 year ago
Matthew Huntington 65f029207e converting show category to script setup syntax
1 year ago
Matthew Huntington a2422e40ce refresh status on session logged
1 year ago
Matthew Huntington a0bcfb8c3c logging time updates summary
1 year ago
Matthew Huntington d1cdfba63d switching to emit for clicking on summary category
1 year ago
Matthew Huntington 8121e79095 rewrote status component
1 year ago
Matthew Huntington 4f9235c95d taking out category ids from dropdowns
1 year ago
Matthew Huntington e463f43693 fixed warning about practice_category_id
1 year ago
Matthew Huntington e15e618319 status text
1 year ago
Matthew Huntington bc211c3f2b formatSeconds defaults to true for includeHours param
1 year ago
Matthew Huntington 54c825827f fixing formatSeconds for negative values
1 year ago
Matthew Huntington 66cc5fbdc6 better time formatting in status section
1 year ago
Matthew Huntington bd02cc79c7 setting up fixed decimal values for mins left, get ahead, practiced
1 year ago
Matthew Huntington 25c21f3cca have manual time inputs match timer
1 year ago
Matthew Huntington 553a128468 time left to get ahead
1 year ago
Matthew Huntington d9935f7e1f click categories to set timer and show category currentWorkingCategory
1 year ago
Matthew Huntington c80b043c03 display local storage saved seconds on timer mount
1 year ago
Matthew Huntington ae9367384b innerText for clipboard copy
1 year ago
Matthew Huntington 6a54cea56b dark mode css
1 year ago
Matthew Huntington 96b1b050e5 dark mode css
1 year ago
Matthew Huntington 2fe364b0ba confirm close with unsaved seconds
1 year ago
Matthew Huntington c4365dfe56 click to copy
1 year ago
Matt Huntington 69d202079d timer has dark green background when initially started and no changes to threshold
2 years ago
mahuntington b9f7eba63f Update 'src/components/timer.vue'
2 years ago
Matthew Huntington 3bff27a77d don't request mic access until threshold initially changed
2 years ago
Matthew Huntington 986058ecb1 moving comments below submit button in timer
2 years ago
Matt Huntington 3c07315ea7 starting mic threshold
2 years ago
Matt Huntington 4db261d943 mic threshold input width
2 years ago
Matt Huntington 1def5795ca reset seconds to subtract appropriately
2 years ago
Matt Huntington cd847bb90e reset for subtracted seconds
2 years ago

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

1055
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

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

@ -1,22 +1,79 @@
<script setup lang="ts">
<script setup>
import { watch, onMounted, ref } from '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 Songs from './components/songs.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>
<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>
<section id="timer">
<MinsLeftToPracticeToday/>
<Timer/>
<section id="timer" :class="{timerRunning:timerRunning, micThresholdExceeded:micThresholdExceeded}">
<Status ref="statusRef"/>
<Timer
v-if="currentWorkingInstrument"
@loggedTime="refreshPage"
:instruments="instruments"
:categories="categories"
v-model:timerRunning="timerRunning"
v-model:micThresholdExceeded="micThresholdExceeded"
/>
</section>
<section id="summary">
<Summary/>
<Summary ref="summaryRef" />
</section>
<section id="category">
<ShowCategory/>
<ShowCategory
v-if="currentWorkingInstrument"
ref="showCategoryRef"
/>
</section>
<section id="songs">
<Songs/>
@ -25,4 +82,12 @@
</template>
<style scoped>
.timerRunning {
background:lightgreen;
}
.timerRunning.micThresholdExceeded {
background:darkgreen;
color:white;
}
</style>

@ -0,0 +1,47 @@
<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>

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

@ -1,20 +1,25 @@
<script lang="ts">
import axios from 'axios';
<script setup>
import { onMounted, ref } from 'vue'
import { copiedSongs } from '../libs/state.js'
export default {
data() {
return {
songs:[]
}
},
mounted() {
axios.get(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'songs').then((response)=>{
this.songs = response.data
const songs = ref([])
onMounted(async ()=>{
const response = await fetch(import.meta.env.VITE_PRACTICE_TRACKER_API_URL+'songs')
songs.value = await response.json()
})
const copy = (title) => {
navigator.clipboard.writeText(title);
}
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>
<template>
<h2>Songs</h2>
<table>
@ -23,17 +28,39 @@
<th>id</th>
<th>title</th>
<th>notes</th>
<th>lyrics</th>
<th>original</th>
</tr>
</thead>
<tbody>
<tr v-for="song in songs">
<td>{{song.id}}</td>
<td>{{song.title}}</td>
<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>
<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>
</tbody>
</table>
</template>
<style scoped>
button {
padding: 1em 2em;
margin: 1em;
}
td:nth-child(2){
text-align:center;
}
</style>

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

@ -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>

@ -0,0 +1,8 @@
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)

@ -0,0 +1,33 @@
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