aboutsummaryrefslogtreecommitdiff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/form.ts178
-rw-r--r--src/view/timer.ts87
2 files changed, 197 insertions, 68 deletions
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'
}
}