Skip to content
This repository has been archived by the owner on May 24, 2021. It is now read-only.

Commit

Permalink
feat(progress-track): Add ghost mode to progress slider
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-heimbuch committed Jun 21, 2017
1 parent 2d5e061 commit 401c032
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 30 deletions.
87 changes: 72 additions & 15 deletions src/components/player/progress-bar/Progress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
type="range"
min="0" :max="interpolate(duration)" step="0.1"
:value="interpolate(playtime)"
v-on:change="onChange"
v-on:input="onInput"
@change="onChange"
@input="onInput"
@mousemove="onMouseMove"
@mouseout="onMouseOut"
/>
<span class="progress-range"></span>
<span class="progress-buffer" :style="bufferStyle(theme, buffer, duration)"></span>
<span v-for="quantile in quantiles" class="progress-track" :style="trackStyle(theme, duration, quantile)"></span>
<ChaptersIndicator />
<span class="progress-thumb" :style="thumbStyle(theme, thumbPosition)"></span>
<span class="ghost-thumb" :style="thumbStyle(theme, ghostPosition, ghost.active)"></span>
<span class="progress-thumb" :class="{ active: thumbActive }" :style="thumbStyle(theme, thumbPosition)"></span>
</div>
</template>

Expand All @@ -31,7 +34,8 @@
'background-color': color(theme.player.progress.bar).fade(0.5)
})
const thumbStyle = (theme, position) => ({
const thumbStyle = (theme, position, active = true) => ({
display: active ? 'block' : 'none',
left: position,
'background-color': theme.player.progress.thumb,
'border-color': theme.player.progress.border
Expand All @@ -47,15 +51,21 @@
data () {
let playtime = this.$select('playtime')
let duration = this.$select('duration')
let theme = this.$select('theme')
return {
playtime,
duration,
theme,
buffer: this.$select('buffer'),
playstate: this.$select('playstate'),
theme: this.$select('theme'),
thumbPosition: relativePosition(playtime, duration),
quantiles: this.$select('quantiles')
quantiles: this.$select('quantiles'),
ghost: this.$select('ghost'),
ghostPosition: 0,
thumbActive: false
}
},
watch: {
Expand All @@ -71,6 +81,23 @@
this.thumbPosition = relativePosition(interpolate(event.target.value), this.duration)
store.dispatch(store.actions.updatePlaytime(event.target.value))
},
onMouseMove (event) {
if (event.offsetY < 13 && event.offsetY > 31) {
this.thumbActive = false
store.dispatch(store.actions.disableGhostMode())
return
}
this.thumbActive = true
this.ghostPosition = relativePosition(event.offsetX, event.target.clientWidth)
store.dispatch(store.actions.simulatePlaytime(this.duration * event.offsetX / event.target.clientWidth))
store.dispatch(store.actions.enableGhostMode())
},
onMouseOut (event) {
this.thumbActive = false
store.dispatch(store.actions.disableGhostMode())
store.dispatch(store.actions.simulatePlaytime(this.playtime))
},
interpolate,
bufferStyle,
thumbStyle,
Expand All @@ -88,20 +115,28 @@
$progress-height: 44px;
$progress-track-height: 2px;
$progress-thumb-height: 14px;
$progress-thumb-width: 6px;
$progress-thumb-active-height: 18px;
$progress-thumb-active-width: 8px;
.progress {
width: 100%;
position: relative;
height: $progress-height;
transition: opacity ($animation-duration / 2), height $animation-duration;
cursor: pointer;
}
.progress-range {
display: block;
position: absolute;
width: 100%;
left: 0;
top: calc(50% - 1px);
height: 2px;
top: calc(50% - #{$progress-track-height / 2});
height: $progress-track-height;
background-color: rgba($accent-color, 0.25);
pointer-events: none;
}
Expand All @@ -110,26 +145,48 @@
display: block;
position: absolute;
left: 0;
top: calc(50% - 1px);
height: 2px;
top: calc(50% - #{$progress-track-height / 2});
height: $progress-track-height;
pointer-events: none;
}
.progress-thumb {
position: absolute;
border: 1px solid;
height: 14px;
top: calc(50% - 7px);
width: 6px;
// border offset
margin-left: -2px;
height: $progress-thumb-height;
top: calc(50% - #{$progress-thumb-height / 2});
width: $progress-thumb-width;
pointer-events: none;
transition: all $animation-duration / 2;
&.active {
width: $progress-thumb-active-width;
height: $progress-thumb-active-height;
top: calc(50% - #{$progress-thumb-active-height / 2});
}
}
.ghost-thumb {
display: none;
position: absolute;
border: 1px solid transparent;
opacity: 0.8;
margin-left: -2px;
height: $progress-thumb-height;
top: calc(50% - #{$progress-thumb-height / 2});
width: $progress-thumb-width;
pointer-events: none;
}
.progress-buffer {
display: block;
opacity: 1;
position: absolute;
height: 2px;
top: calc(50% - 1px);
height: $progress-track-height;
top: calc(50% - #{$progress-track-height / 2});
left: 0;
pointer-events: none;
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/player/progress-bar/Timer.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<div class="timer-progress" :class="playstate" :style="timerStyle(theme)">
<span class="current">{{secondsToTime(playtime)}}</span>
<span class="current">{{ secondsToTime(ghost.active ? ghost.time : playtime) }}</span>
<CurrentChapter class="chapter" />
<span class="time">{{secondsToTime(duration - playtime)}}</span>
<span class="time">{{ secondsToTime(duration - (ghost.active ? ghost.time : playtime)) }}</span>
</div>
</template>

Expand All @@ -20,11 +20,11 @@ export default {
data () {
return {
playtime: this.$select('playtime'),
ghost: this.$select('ghost'),
duration: this.$select('duration'),
playstate: this.$select('playstate'),
theme: this.$select('theme'),
chapters: this.$select('chapters'),
timerMode: this.$select('timerMode')
chapters: this.$select('chapters')
}
},
methods: {
Expand Down
4 changes: 2 additions & 2 deletions src/components/shared/Slider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
:max="maxValue"
:value="value"
:step="sliderSteps"
v-on:input="onSliderInput"
v-on:change="onSliderChange"
@input="onSliderInput"
@change="onSliderChange"
/>
<span class="slider--track"></span>
<span class="slider--thumb" :style="thumbStyle(thumbPosition, thumbColor, thumbBorder)"></span>
Expand Down
6 changes: 3 additions & 3 deletions src/components/tabs/chapters/ChapterEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<div class="chapters--entry" :style="chapterStyle(theme, chapter)" @click="onChapterClick(index)">
<span class="index">{{index + 1}}</span>
<span class="title truncate">{{chapter.title}}</span>
<span class="timer">{{remainingTime(chapter, playtime)}}</span>
<span class="timer">{{remainingTime(chapter, ghost.active ? ghost.time : playtime)}}</span>

<span class="progress" :style="progressStyle(theme, chapter, playtime)"></span>
<span class="progress" :style="progressStyle(theme, chapter, ghost.active ? ghost.time : playtime)"></span>
</div>
</template>

Expand Down Expand Up @@ -55,7 +55,7 @@
theme: this.$select('theme'),
chapters: this.$select('chapters'),
playtime: this.$select('playtime'),
timerMode: this.$select('timerMode')
ghost: this.$select('ghost')
}
},
methods: {
Expand Down
18 changes: 18 additions & 0 deletions src/store/actions/ghost.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const simulatePlaytime = playtime => ({
type: 'SIMULATE_PLAYTIME',
payload: playtime
})

const enableGhostMode = () => ({
type: 'ENABLE_GHOST_MODE'
})

const disableGhostMode = () => ({
type: 'DISABLE_GHOST_MODE'
})

export {
simulatePlaytime,
enableGhostMode,
disableGhostMode
}
21 changes: 21 additions & 0 deletions src/store/actions/ghost.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import test from 'ava'
import { simulatePlaytime, enableGhostMode, disableGhostMode } from './ghost'

test(`simulatePlaytime: creates the SIMULATE_PLAYTIME action`, t => {
t.deepEqual(simulatePlaytime(100), {
type: 'SIMULATE_PLAYTIME',
payload: 100
})
})

test(`enableGhostMode: creates the ENABLE_GHOST_MODE action`, t => {
t.deepEqual(enableGhostMode(), {
type: 'ENABLE_GHOST_MODE'
})
})

test(`disableGhostMode: creates the DISABLE_GHOST_MODE action`, t => {
t.deepEqual(disableGhostMode(), {
type: 'DISABLE_GHOST_MODE'
})
})
3 changes: 2 additions & 1 deletion src/store/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import * as quantiles from './quantiles'
import * as l10n from './l10n'
import * as error from './error'
import * as chapters from './chapters'
import * as ghost from './ghost'

export default Object.assign({}, init, player, playtime, components, tabs, share, theme, quantiles, l10n, error, chapters)
export default Object.assign({}, init, player, playtime, components, tabs, share, theme, quantiles, l10n, error, chapters, ghost)
1 change: 1 addition & 0 deletions src/store/reducers/chapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const chapters = (state = [], action) => {
return chapters
.reduce(parseChapters(action.payload.duration), [])
.map(setActiveByPlaytime(action.payload.playtime || 0))
case 'SIMULATE_PLAYTIME':
case 'SET_PLAYTIME':
case 'UPDATE_PLAYTIME':
const nextChapters = state.map(setActiveByPlaytime(action.payload))
Expand Down
30 changes: 30 additions & 0 deletions src/store/reducers/ghost.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const INITIAL = {
time: 0,
active: false
}

const ghost = (state = INITIAL, action) => {
switch (action.type) {
case 'SIMULATE_PLAYTIME':
return {
...state,
time: parseFloat(action.payload)
}
case 'ENABLE_GHOST_MODE':
return {
...state,
active: true
}
case 'DISABLE_GHOST_MODE':
return {
...state,
active: false
}
default:
return state
}
}

export {
ghost
}
73 changes: 73 additions & 0 deletions src/store/reducers/ghost.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import test from 'ava'
import { ghost } from './ghost'

test(`ghost: it exports a reducer function`, t => {
t.truthy(typeof ghost === 'function')
})

test(`ghost: it returns the initial state on default`, t => {
const result = ghost(undefined, {
type: 'FOO'
})

t.deepEqual(result, {
time: 0,
active: false
})
})

test(`ghost: it sets the ghost time on SIMULATE_PLAYTIME`, t => {
const result = ghost(undefined, {
type: 'SIMULATE_PLAYTIME',
payload: 100
})

t.deepEqual(result, {
time: 100,
active: false
})
})

test(`ghost: it activates the ghost mode on ENABLE_GHOST_MODE`, t => {
const result = ghost(undefined, {
type: 'ENABLE_GHOST_MODE'
})

t.deepEqual(result, {
time: 0,
active: true
})
})

test(`ghost: it disables the ghost mode on DISABLE_GHOST_MODE`, t => {
const result = ghost(undefined, {
type: 'DISABLE_GHOST_MODE'
})

t.deepEqual(result, {
time: 0,
active: false
})
})

// test(`error: it sets the message and title on ERROR_LOAD`, t => {
// const result = error(undefined, {
// type: 'ERROR_LOAD'
// })

// t.deepEqual(result, {
// title: 'ERROR.LOADING.TITLE',
// message: 'ERROR.LOADING.MESSAGE'
// })
// })

// test(`error: it sets the message and title on ERROR_MISSING_AUDIO_FILES`, t => {
// const result = error(undefined, {
// type: 'ERROR_MISSING_AUDIO_FILES'
// })

// t.deepEqual(result, {
// title: 'ERROR.MISSING_FILES.TITLE',
// message: 'ERROR.MISSING_FILES.MESSAGE'
// })
// })
7 changes: 4 additions & 3 deletions src/store/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import * as share from './share'
import * as quantiles from './quantiles'
import * as runtime from './runtime'
import * as error from './error'
import * as ghost from './ghost'

export default combineReducers(
Object.assign({}, init, components, player, playtime, chapters, tabs, theme, share, quantiles, runtime, error)
)
export default combineReducers(Object.assign({},
init, components, player, playtime, chapters, tabs, theme, share, quantiles, runtime, error, ghost
))
Loading

0 comments on commit 401c032

Please sign in to comment.