From f9e7e819a0a673befb11b24404efeb9d6644bceb Mon Sep 17 00:00:00 2001 From: Joris Date: Thu, 20 May 2021 09:43:02 +0200 Subject: Provide named exercices --- src/view/form.ts | 178 +++++++++++++++++++++++++++++++++++++++++++++--------- src/view/timer.ts | 87 +++++++++++++------------- 2 files changed, 197 insertions(+), 68 deletions(-) (limited to 'src/view') diff --git a/src/view/form.ts b/src/view/form.ts index b0d5827..a5e7253 100644 --- a/src/view/form.ts +++ b/src/view/form.ts @@ -3,25 +3,6 @@ import h from 'h' import * as Router from 'router' import * as Duration from 'duration' -function labelledInput( - labelValue: string, - min: number, - value: number, - update: (n: number) => void -): Element { - return h('label', - { className: 'g-Form__Label', - oninput: (e: Event) => { - if (e.target !== null) { - const target = e.target as HTMLInputElement - update(parseInt(target.value) || 0) - } - } - }, - labelValue, - h('input', { className: 'g-Form__Input', type: 'number', min, value })) -} - export function view(config: Config.Config, showPage: (route: Router.Route) => void) { const duration = document.createTextNode(Duration.prettyPrint(Config.getDuration(config))) const wd = () => duration.textContent = Duration.prettyPrint(Config.getDuration(config)) @@ -36,13 +17,156 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v history.pushState({}, '', Router.toString(timerRoute)) showPage(timerRoute) }}, - labelledInput('Warm Up', 0, config.warmup, n => { config.warmup = n; wd()}), - labelledInput('Tabatas', 1, config.tabatas, n => { config.tabatas = n; wd()}), - labelledInput('Prepare', 0, config.prepare, n => { config.prepare = n; wd()}), - labelledInput('Cycles', 1, config.cycles, n => { config.cycles = n; wd()}), - labelledInput('Work', 5, config.work, n => { config.work = n; wd()}), - labelledInput('Rest', 5, config.rest, n => { config.rest = n; wd()}), - h('div', { className: 'g-Form__Duration' }, 'Duration:', h('div', {}, duration)), - h('button', { className: 'g-Form__Start' }, 'start')) + h('section', + { className: 'g-Form__Section' }, + numberInput('Warm Up', 0, config.warmup, n => { config.warmup = n; wd()}) + ), + h('section', + { className: 'g-Form__Section' }, + h('h1', { className: 'g-Form__Title' }, 'Tabatas'), + h('div', + { className: 'g-Form__Line' }, + numberInput('Preparation', 0, config.prepare, n => { config.prepare = n; wd()}), + operator('+'), + numberInput('Cycles', 1, config.cycles, n => { config.cycles = n; wd()}), + operator('× ('), + numberInput('Work', 5, config.work, n => { config.work = n; wd()}), + operator('+'), + numberInput('Rest', 5, config.rest, n => { config.rest = n; wd()}), + operator(')') + ), + tabatasSection( + config.tabatas, + (str: string) => { config.tabatas.push(str); wd() }, + (index: number, str: string) => { config.tabatas[index] = str; wd() }, + (index: number) => { config.tabatas.splice(index, 1); wd() } + ) + ), + h('section', + { className: 'g-Form__Section' }, + h('h1', { className: 'g-Title' }, 'Duration'), + h('div', { className: 'g-Form__Duration' }, duration) + ), + h('button', { className: 'g-Button' }, 'Start') + ) + ) +} + +function operator(str: string): Element { + return h('span', { className: 'g-Form__Operator' }, str) +} + +function tabatasSection( + init: string[], + onAdd: (str: string) => void, + onUpdate: (index: number, str: string) => void, + onRemove: (inedx: number) => void +) { + const tabatas = h('ol', { className: 'g-List' }) + let counter = init.length + let removedIndexes: number[] = [] + let adjustIndex = (index: number) => index - removedIndexes.filter(i => i < index).length + + init.forEach((initStr, index) => { + const { tabata } = tabataInput( + initStr, + (str: string) => onUpdate(adjustIndex(index), str), + () => { + onRemove(adjustIndex(index)) + tabatas.removeChild(tabata) + removedIndexes.push(index) + } + ) + tabatas.appendChild(tabata) + }) + + return h('div', + { className: 'g-Form__Tabatas' }, + tabatas, + h('input', + { type: 'button', + value: 'Add', + className: 'g-Button', + onclick: (e: Event) => { + let index = counter++ + const txt = `Exercise ${adjustIndex(index) + 1}` + onAdd(txt) + let { tabata, textInput } = tabataInput( + txt, + (str: string) => onUpdate(adjustIndex(index), str), + () => { + onRemove(adjustIndex(index)) + tabatas.removeChild(tabata) + removedIndexes.push(index) + } + ) + tabatas.appendChild(tabata) + textInput.select() + } + } + ) + ) +} + +interface TabataInput { + tabata: Element, + textInput: HTMLInputElement, +} + +function tabataInput( + init: string, + onUpdate: (str: string) => void, + onRemove: () => void +): TabataInput { + const textInput = h('input', + { value: init, + className: 'g-Input', + required: 'required', + oninput: (e: Event) => { + if (e.target !== null) { + const target = e.target as HTMLInputElement + onUpdate(target.value.trim()) + } + } + } + ) as HTMLInputElement + + const tabata = h('li', + { className: 'g-Form__Tabata'}, + textInput, + h('input', + { type: 'button', + value: 'Remove', + className: 'g-Button', + onclick: (e: Event) => onRemove() + } + ) ) + + return { tabata, textInput } +} + +function numberInput( + labelValue: string, + min: number, + value: number, + update: (n: number) => void +): Element { + return h('label', + { className: 'g-Form__Label', + oninput: (e: Event) => { + if (e.target !== null) { + const target = e.target as HTMLInputElement + update(parseInt(target.value) || 0) + } + } + }, + labelValue, + h('input', + { className: 'g-Input', + type: 'number', + required: 'required', + min, + value + })) } diff --git a/src/view/timer.ts b/src/view/timer.ts index 07b5db3..3cdf0d7 100644 --- a/src/view/timer.ts +++ b/src/view/timer.ts @@ -1,5 +1,5 @@ import * as Config from 'config' -import * as State from 'state' +import * as Step from 'step' import * as Arc from 'arc' import * as Router from 'router' import * as Audio from 'audio' @@ -16,29 +16,35 @@ export function clearInterval() { } } +interface State { + isPlaying: boolean, + elapsed: number +} + export function view(config: Config.Config, showPage: (route: Router.Route) => void) { const formUrl = `${Router.toString({ name: 'form', config })}` const duration = Config.getDuration(config) // State - let isPlaying = true - let elapsed = 0 - let state = State.getAt(config, elapsed) + let state = { + isPlaying: true, + elapsed: 0 + } // Elements - const section = h('section', { className: timerClass(state.step) }) - const stepElt = document.createTextNode(State.prettyPrintStep(state.step)) - const stepInfoElt = document.createTextNode(state.info) + const initStep = Step.getAt(config, state.elapsed) + const section = h('section', { className: timerClass(initStep) }) + const stepElt = document.createTextNode(Step.prettyPrint(initStep)) const arcPathElt = h('path', { class: 'g-Timer__ArcProgress' }) const updateDom = () => { - const angle = Math.min(359.999, elapsed / duration * 360) + const step = Step.getAt(config, state.elapsed) + const angle = Math.min(359.999, state.elapsed / duration * 360) arcPathElt.setAttribute("d", Arc.describe(0, 0, 90, 0, angle)) - section.className = timerClass(state.step) - stepElt.textContent = State.prettyPrintStep(state.step) - stepInfoElt.textContent = state.info - Audio.playFromStep(config, state) + section.className = timerClass(step) + stepElt.textContent = Step.prettyPrint(step) + Audio.playFromStep(config, step, state.elapsed) } const quit = () => { @@ -49,10 +55,9 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v } const update = () => { - if (isPlaying) { - elapsed = elapsed + 1 - state = State.getAt(config, elapsed) - elapsed >= duration + endDuration + if (state.isPlaying) { + state.elapsed = state.elapsed + 1 + state.elapsed >= duration + endDuration ? quit() : updateDom() } @@ -66,7 +71,7 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v interval = window.setInterval(update, 1000) // Play initial audio - Audio.playFromStep(config, state) + Audio.playFromStep(config, initStep, state.elapsed) section.append( h('div', @@ -83,31 +88,30 @@ export function view(config: Config.Config, showPage: (route: Router.Route) => v ...arcPaths(config), arcPathElt ), - h('div', { className: 'g-Timer__Step' }, stepElt), - h('div', {}, stepInfoElt) + stepElt ), h('div', { className: 'g-Timer__Buttons' }, h('button', - { className: 'g-Timer__Button', + { className: 'g-Button', onclick: (e: MouseEvent) => { - isPlaying = !isPlaying + state.isPlaying = !state.isPlaying const elt = e.target as HTMLElement - elt.textContent = isPlaying + elt.textContent = state.isPlaying ? 'pause' : 'resume' - elt.className = isPlaying - ? 'g-Timer__Button' - : 'g-Timer__Button g-Timer__Button--Active' + elt.className = state.isPlaying + ? 'g-Button' + : 'g-Button g-Button--Active' } }, - 'pause' + 'Pause' ), h('a', - { className: 'g-Timer__Button', + { className: 'g-Button', href: formUrl }, - 'quit' + 'Quit' ) ) ) @@ -123,7 +127,7 @@ function arcPaths(config: Config.Config): Element[] { let arc = (kind: string, duration: number): Element => { const startAngle = 360 * t / totalDuration - const endAngle = 360 * (t + duration) / totalDuration + const endAngle = Math.min(360 * (t + duration) / totalDuration, 359.999) t += duration @@ -138,7 +142,7 @@ function arcPaths(config: Config.Config): Element[] { paths.push(arc('WarmUp', config.warmup)) } - for (let tabata = 0; tabata < config.tabatas; tabata++) { + for (let tabata = 0; tabata < config.tabatas.length; tabata++) { paths.push(arc('Prepare', config.prepare)) for (let cycle = 0; cycle < config.cycles; cycle++) { paths.push(arc('Work', config.work)) @@ -149,16 +153,17 @@ function arcPaths(config: Config.Config): Element[] { return paths } -function timerClass(step: State.Step): string { - if (step === State.Step.WarmUp) { - return 'g-Layout__Page g-Timer g-Timer--WarmUp' - } else if (step === State.Step.Work) { - return 'g-Layout__Page g-Timer g-Timer--Work' - } else if (step === State.Step.Rest) { - return 'g-Layout__Page g-Timer g-Timer--Rest' - } else if (step === State.Step.Prepare) { - return 'g-Layout__Page g-Timer g-Timer--Prepare' - } else { - return 'g-Layout__Page g-Timer' +function timerClass(step: Step.Step): string { + switch (step.name) { + case 'warmup': + return 'g-Layout__Page g-Timer g-Timer--WarmUp' + case 'prepare': + return 'g-Layout__Page g-Timer g-Timer--Prepare' + case 'work': + return 'g-Layout__Page g-Timer g-Timer--Work' + case 'rest': + return 'g-Layout__Page g-Timer g-Timer--Rest' + case 'end': + return 'g-Layout__Page g-Timer' } } -- cgit v1.2.3