├── .babelrc
├── .circleci
└── config.yml
├── .editorconfig
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── assetsources
├── TabIcons.afdesign
├── design.sketch
└── svg
│ ├── Audio.svg
│ ├── Chapters.svg
│ ├── Download.svg
│ ├── Info.svg
│ ├── Share.svg
│ ├── Transcripts.svg
│ └── optimize.sh
├── build
├── blocks
│ ├── dev-server.js
│ ├── dir.js
│ ├── entry.js
│ ├── index.js
│ ├── optimization.js
│ ├── output.js
│ ├── plugins.js
│ ├── resolve.js
│ └── rules.js
├── webpack.config.cdn.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── cypress.json
├── cypress
├── fixtures
│ ├── audio.json
│ ├── chapters.json
│ ├── contributors.json
│ ├── episode.json
│ ├── reference.json
│ ├── runtime.json
│ └── show.json
├── helpers
│ └── state.js
├── integration
│ ├── controllbar.spec.js
│ ├── header.spec.js
│ ├── progressbar.spec.js
│ ├── tab-audio.spec.js
│ ├── tab-chapters.spec.js
│ ├── tab-files.spec.js
│ ├── tab-info.spec.js
│ ├── tab-share.spec.js
│ ├── url-parameters.spec.js
│ └── visible-components.spec.js
├── plugins
│ └── index.js
├── selectors
│ ├── controllbar.js
│ ├── header.js
│ ├── index.js
│ ├── progressbar.js
│ └── tabs
│ │ ├── audio.js
│ │ ├── chapters.js
│ │ ├── files.js
│ │ ├── info.js
│ │ └── share.js
├── support
│ ├── commands.js
│ └── index.js
└── testbed
│ ├── assets
│ ├── bender.png
│ ├── episode.png
│ ├── fry.png
│ ├── podlove.m4a
│ ├── podlove.mp3
│ ├── professor.png
│ └── show.png
│ ├── embed.html
│ └── test.html
├── docs
├── .vuepress
│ ├── components
│ │ ├── ColorPicker.vue
│ │ ├── JsonEditor.vue
│ │ ├── Playground.vue
│ │ ├── PodloveWebPlayer.vue
│ │ ├── StoreDispatch.vue
│ │ └── StoreSubscribe.vue
│ ├── config.js
│ └── public
│ │ ├── favicon.png
│ │ └── fixtures
│ │ ├── chapters
│ │ └── fg55.json
│ │ ├── example.json
│ │ ├── fg45.json
│ │ ├── fs207.json
│ │ ├── fs220.json
│ │ └── transcripts
│ │ ├── cs309.json
│ │ ├── fg45.json
│ │ ├── fg55.json
│ │ └── fs207.json
├── config.md
├── embedding.md
├── extensions.md
├── index.md
├── installation.md
├── player-dispatches.md
├── player-subscriptions.md
├── playground.md
└── theme.md
├── now.json
├── package-lock.json
├── package.json
├── readme.md
├── screenshot.png
├── scripts
├── deploy-ghpages.sh
└── dev.js
├── src
├── app.js
├── components
│ ├── App.vue
│ ├── header
│ │ ├── Error.vue
│ │ ├── Header.vue
│ │ ├── Info.vue
│ │ └── Poster.vue
│ ├── icons
│ │ ├── AudioIcon.vue
│ │ ├── CalendarIcon.vue
│ │ ├── ChapterBackIcon.vue
│ │ ├── ChapterNextIcon.vue
│ │ ├── ChaptersIcon.vue
│ │ ├── ClockIcon.vue
│ │ ├── CloseIcon.vue
│ │ ├── CopyIcon.vue
│ │ ├── DownloadIcon.vue
│ │ ├── EmbedIcon.vue
│ │ ├── ErrorIcon.vue
│ │ ├── FacebookIcon.vue
│ │ ├── FilesAudioIcon.vue
│ │ ├── FollowIcon.vue
│ │ ├── InfoIcon.vue
│ │ ├── LinkIcon.vue
│ │ ├── LinkedinIcon.vue
│ │ ├── MailIcon.vue
│ │ ├── MinusIcon.vue
│ │ ├── NextSearchIcon.vue
│ │ ├── PauseIcon.vue
│ │ ├── PinterestIcon.vue
│ │ ├── PlayIcon.vue
│ │ ├── PlusIcon.vue
│ │ ├── PodlovePlayerIcon.vue
│ │ ├── PreviousSearchIcon.vue
│ │ ├── RedditIcon.vue
│ │ ├── ReloadIcon.vue
│ │ ├── SearchDeleteIcon.vue
│ │ ├── SettingsIcon.vue
│ │ ├── ShareChapterIcon.vue
│ │ ├── ShareEpisodeIcon.vue
│ │ ├── ShareIcon.vue
│ │ ├── SharePlaytimeIcon.vue
│ │ ├── ShareShowIcon.vue
│ │ ├── SpeakerIcon.vue
│ │ ├── StepBackIcon.vue
│ │ ├── StepForwardIcon.vue
│ │ ├── TranscriptsIcon.vue
│ │ ├── TwitterIcon.vue
│ │ └── icon-container.js
│ ├── player
│ │ ├── Player.vue
│ │ ├── control-bar
│ │ │ ├── ChapterBackButton.vue
│ │ │ ├── ChapterNextButton.vue
│ │ │ ├── ControlBar.vue
│ │ │ ├── LoadingIndicator.vue
│ │ │ ├── PlayButton.vue
│ │ │ ├── StepBackButton.vue
│ │ │ └── StepForwardButton.vue
│ │ └── progress-bar
│ │ │ ├── ChapterIndicator.vue
│ │ │ ├── CurrentChapter.vue
│ │ │ ├── Progress.vue
│ │ │ ├── ProgressBar.vue
│ │ │ └── Timer.vue
│ ├── shared
│ │ ├── Button.vue
│ │ ├── ButtonGroup.vue
│ │ ├── CopyTooltip.vue
│ │ ├── Footer.vue
│ │ ├── InputGroup.vue
│ │ ├── InputSelect.vue
│ │ ├── InputSlider.vue
│ │ ├── InputText.vue
│ │ ├── Overlay.vue
│ │ ├── Slider.vue
│ │ ├── TabBody.vue
│ │ ├── TabHeader.vue
│ │ └── TabHeaderItem.vue
│ └── tabs
│ │ ├── Tabs.vue
│ │ ├── audio
│ │ ├── Audio.vue
│ │ ├── AudioRate.vue
│ │ └── AudioVolume.vue
│ │ ├── chapters
│ │ ├── ChapterEntry.vue
│ │ └── Chapters.vue
│ │ ├── files
│ │ ├── File.vue
│ │ └── Files.vue
│ │ ├── info
│ │ └── Info.vue
│ │ ├── share
│ │ ├── Share.vue
│ │ ├── ShareChannels.vue
│ │ ├── ShareContent.vue
│ │ ├── ShareEmbed.vue
│ │ ├── ShareLink.vue
│ │ └── channels
│ │ │ ├── ChannelEmbed.vue
│ │ │ ├── ChannelFacebook.vue
│ │ │ ├── ChannelLinkedin.vue
│ │ │ ├── ChannelMail.vue
│ │ │ ├── ChannelPinterest.vue
│ │ │ ├── ChannelReddit.vue
│ │ │ └── ChannelTwitter.vue
│ │ └── transcripts
│ │ ├── Entry.vue
│ │ ├── Follow.vue
│ │ ├── Prerender.vue
│ │ ├── Render.vue
│ │ ├── Search.vue
│ │ └── Transcripts.vue
├── core
│ ├── directives
│ │ ├── index.js
│ │ ├── marquee.js
│ │ └── resize.js
│ ├── index.js
│ └── lang
│ │ └── index.js
├── effects
│ ├── chapters.js
│ ├── chapters.test.js
│ ├── components
│ │ ├── episode.js
│ │ ├── episode.test.js
│ │ ├── index.js
│ │ ├── live.js
│ │ └── live.test.js
│ ├── index.js
│ ├── keyboard.js
│ ├── keyboard.test.js
│ ├── playback.js
│ ├── playback.test.js
│ ├── player.js
│ ├── player.test.js
│ ├── quantiles.js
│ ├── quantiles.test.js
│ ├── runtime.js
│ ├── storage
│ │ ├── episode.js
│ │ ├── episode.test.js
│ │ ├── index.js
│ │ ├── live.js
│ │ └── live.test.js
│ ├── transcripts
│ │ ├── active.js
│ │ ├── active.test.js
│ │ ├── fetch.js
│ │ ├── fetch.test.js
│ │ ├── fixtures.js
│ │ ├── index.js
│ │ ├── search.js
│ │ └── search.test.js
│ ├── volume.js
│ └── volume.test.js
├── embed
│ ├── embed.js
│ ├── loader.js
│ ├── share.js
│ └── window.js
├── extensions
│ └── external-events.js
├── lang
│ ├── de.json
│ ├── en.json
│ ├── eo.json
│ └── ru.json
├── media
│ └── index.js
├── statics
│ ├── example
│ │ ├── episode.js
│ │ ├── episode.json
│ │ ├── example.js
│ │ ├── files
│ │ │ └── poster.jpg
│ │ ├── standalone.html
│ │ └── transcripts.json
│ └── share.html
├── store
│ ├── actions.js
│ ├── buffer
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── chapters
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ └── selectors.js
│ ├── components
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── effects.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── display
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── duration
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── episode
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── error
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── files
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ └── selectors.js
│ ├── ghost
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── index.js
│ ├── last-action
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── mode
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── muted
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── playback
│ │ ├── actions.js
│ │ ├── index.js
│ │ └── reducer.js
│ ├── player
│ │ ├── actions.js
│ │ └── actions.test.js
│ ├── playstate
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── playtime
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── quantiles
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── rate
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── reducers.js
│ ├── reference
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── runtime
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── selectors.js
│ ├── share
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ └── selectors.js
│ ├── show
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── speakers
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── tabs
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── theme
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── transcripts
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ ├── types.js
│ ├── visible-components
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
│ └── volume
│ │ ├── actions.js
│ │ ├── actions.test.js
│ │ ├── index.js
│ │ ├── reducer.js
│ │ └── reducer.test.js
├── styles
│ ├── _animations.scss
│ ├── _embed.scss
│ ├── _font.scss
│ ├── _global.scss
│ ├── _inputs.scss
│ ├── _loader.scss
│ ├── _marquee.scss
│ ├── _share.scss
│ ├── _text.scss
│ ├── _tooltip.scss
│ ├── _transitions.scss
│ ├── _utils.scss
│ ├── _variables.scss
│ ├── fonts
│ │ ├── FiraMono-Regular.eot
│ │ ├── FiraMono-Regular.woff
│ │ ├── FiraMono-Regular.woff2
│ │ ├── FiraSans-Bold.eot
│ │ ├── FiraSans-Bold.woff
│ │ ├── FiraSans-Bold.woff2
│ │ ├── FiraSans-Light.eot
│ │ ├── FiraSans-Light.woff
│ │ ├── FiraSans-Light.woff2
│ │ ├── FiraSans-Regular.eot
│ │ ├── FiraSans-Regular.woff
│ │ └── FiraSans-Regular.woff2
│ └── resets
│ │ ├── _button.scss
│ │ ├── _input.scss
│ │ ├── _lists.scss
│ │ ├── _range.scss
│ │ └── _resets.scss
└── utils
│ ├── binary-search.js
│ ├── binary-search.test.js
│ ├── chapters.js
│ ├── chapters.test.js
│ ├── dom.js
│ ├── dom.test.js
│ ├── effects.js
│ ├── effects.test.js
│ ├── helper.js
│ ├── helper.test.js
│ ├── keyboard.js
│ ├── math.js
│ ├── math.test.js
│ ├── predicates.js
│ ├── request.js
│ ├── request.test.js
│ ├── runtime.js
│ ├── sandbox.js
│ ├── storage.js
│ ├── storage.test.js
│ ├── text-search.js
│ ├── text-search.test.js
│ ├── time.js
│ ├── time.test.js
│ └── url.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [[
3 | "env",
4 | {
5 | "targets": {
6 | "browsers": [
7 | "last 2 versions",
8 | "safari >= 7"
9 | ]
10 | }
11 | }
12 | ]],
13 | "plugins": [
14 | "lodash",
15 | "transform-object-rest-spread",
16 | "es6-promise",
17 | "syntax-dynamic-import"
18 | ],
19 | "env": {
20 | "AVA": {
21 | "plugins": [
22 | [
23 | "babel-plugin-webpack-alias",
24 | {
25 | "config": "build/webpack.config.prod.js"
26 | }
27 | ]
28 | ]
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 | max_line_length = 120
14 |
15 | # We recommend you to keep these unchanged
16 | end_of_line = lf
17 | charset = utf-8
18 | trim_trailing_whitespace = true
19 | insert_final_newline = true
20 |
21 | [*.md]
22 | trim_trailing_whitespace = false
23 |
24 | [*.html]
25 | indent_size = 4
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dotfiles
2 | .*
3 | !.gitignore
4 | !.jshintrc
5 | !.eslintrc
6 | !.editorconfig
7 | !.travis.yml
8 | !.babelrc
9 | !.circleci
10 | !.vuepress
11 |
12 | coverage
13 |
14 | # external dependencies
15 | node_modules
16 | bower_components
17 |
18 | # OS and editor artefacts
19 | *Thumbs.db
20 | atlassian-ide-plugin.xml
21 |
22 | # build artefacts
23 | _site
24 | dist
25 | coverage
26 | reports
27 | docs/media
28 | gh-pages-branch
29 |
30 | # logs
31 | yarn-error.log
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The 2-Clause BSD License
2 | SPDX short identifier: BSD-2-Clause
3 |
4 | Further resources on the 2-clause BSD license
5 | Note: This license has also been called the "Simplified BSD License" and the "FreeBSD License". See also the 3-clause BSD License.
6 |
7 | Copyright 2017 Podlove.org
8 |
9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
10 |
11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
12 |
13 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16 |
--------------------------------------------------------------------------------
/assetsources/TabIcons.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/assetsources/TabIcons.afdesign
--------------------------------------------------------------------------------
/assetsources/design.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/assetsources/design.sketch
--------------------------------------------------------------------------------
/assetsources/svg/Audio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assetsources/svg/Chapters.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assetsources/svg/Download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assetsources/svg/Info.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assetsources/svg/Share.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assetsources/svg/Transcripts.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assetsources/svg/optimize.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # requires svgo: brew install svgo
3 | for f in *.svg; do
4 | echo $f":";
5 | svgo --enable={sortAttrs} --multipass --pretty $f -o - | sed 's/svg /svg width="25" height="25" /g' | sed 's/fill="#fff"/:fill="color || '"'"'currentColor'"'"'"/g' | sed 's/stroke="#fff"/:stroke="color || '"'"'currentColor'"'"'"/g'
6 | echo;
7 | done
--------------------------------------------------------------------------------
/build/blocks/dev-server.js:
--------------------------------------------------------------------------------
1 | const { distDir } = require('./dir')
2 |
3 | module.exports = (port = 8080) => ({
4 | historyApiFallback: true,
5 | noInfo: true,
6 | overlay: true,
7 | inline: true,
8 | hot: true,
9 | disableHostCheck: true,
10 | host: '0.0.0.0',
11 | contentBase: distDir,
12 | port
13 | })
14 |
--------------------------------------------------------------------------------
/build/blocks/dir.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const sourceDir = path.resolve(__dirname, '..', '..', 'src')
4 | const distDir = path.resolve(__dirname, '..', '..', 'dist')
5 | const prepend = (input, prefix) => prefix ? `${prefix}/${input}` : input
6 |
7 | module.exports = { sourceDir, distDir, prepend }
8 |
--------------------------------------------------------------------------------
/build/blocks/entry.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { sourceDir, prepend } = require('./dir')
3 |
4 | const prod = prefix => ({
5 | embed: path.resolve(sourceDir, 'embed', 'embed.js'),
6 | 'extensions/external-events': path.resolve(sourceDir, 'extensions', 'external-events.js'),
7 | [prepend('window', prefix)]: path.resolve(sourceDir, 'embed', 'window.js'),
8 | [prepend('share', prefix)]: path.resolve(sourceDir, 'embed', 'share.js')
9 | })
10 |
11 | const dev = () => ({
12 | embed: path.resolve(sourceDir, 'embed', 'embed.js'),
13 | 'extensions/external-events': path.resolve(sourceDir, 'extensions', 'external-events.js'),
14 | window: path.resolve(sourceDir, 'embed', 'window.js'),
15 | share: path.resolve(sourceDir, 'embed', 'share.js'),
16 | example: path.resolve(sourceDir, 'statics', 'example', 'example.js')
17 | })
18 |
19 | module.exports = { prod, dev }
20 |
--------------------------------------------------------------------------------
/build/blocks/index.js:
--------------------------------------------------------------------------------
1 | const entry = require('./entry')
2 | const output = require('./output')
3 | const resolve = require('./resolve')
4 | const optimization = require('./optimization')
5 | const rules = require('./rules')
6 | const plugins = require('./plugins')
7 | const devServer = require('./dev-server')
8 |
9 | module.exports = {
10 | entry, output, optimization, rules, resolve, plugins, devServer
11 | }
12 |
--------------------------------------------------------------------------------
/build/blocks/optimization.js:
--------------------------------------------------------------------------------
1 | const { prepend } = require('./dir')
2 | const ignoredChunks = ['embed', 'extensions/external-events']
3 |
4 | module.exports = (prefix = '') => ({
5 | splitChunks: {
6 | cacheGroups: {
7 | default: false,
8 | vendors: false,
9 | vendor: {
10 | name: 'vendor',
11 | chunks: chunk => ~[prepend('window', prefix), prepend('share', prefix)].indexOf(chunk.name),
12 | test: /node_modules/
13 | },
14 | styles: {
15 | name: 'style',
16 | test: /\.(s?css|vue)$/,
17 | enforce: true,
18 | chunks: chunk => chunk.name && !~ignoredChunks.indexOf(chunk.name),
19 | minChunks: 1
20 | }
21 | }
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/build/blocks/output.js:
--------------------------------------------------------------------------------
1 | const { distDir, prepend } = require('./dir')
2 |
3 | module.exports = (publicPath, prefix = '') => ({
4 | path: distDir,
5 | filename: '[name].js',
6 | chunkFilename: prepend('[name].js', prefix),
7 | publicPath
8 | })
9 |
--------------------------------------------------------------------------------
/build/blocks/resolve.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { sourceDir } = require('./dir')
3 |
4 | module.exports = () => ({
5 | extensions: ['*', '.js', '.vue', '.json'],
6 | alias: {
7 | store: path.resolve(sourceDir, 'store'),
8 | utils: path.resolve(sourceDir, 'utils'),
9 | shared: path.resolve(sourceDir, 'components', 'shared'),
10 | icons: path.resolve(sourceDir, 'components', 'icons'),
11 | components: path.resolve(sourceDir, 'components'),
12 | lang: path.resolve(sourceDir, 'lang'),
13 | core: path.resolve(sourceDir, 'core'),
14 | styles: path.resolve(sourceDir, 'styles')
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/build/webpack.config.cdn.js:
--------------------------------------------------------------------------------
1 | const { version } = require('../package')
2 | const BASE = `//cdn.podlove.org/web-player/4.x/`
3 |
4 | const { entry, output, resolve, optimization, rules, plugins } = require('./blocks')
5 |
6 | module.exports = {
7 | mode: 'production',
8 | entry: entry.prod(version),
9 | output: output(BASE, version),
10 |
11 | optimization: optimization(version),
12 |
13 | resolve: resolve(),
14 |
15 | module: {
16 | rules: [
17 | rules.vue(),
18 | rules.javascript(),
19 | rules.images(),
20 | rules.styles('prod'),
21 | rules.fonts(version)
22 | ]
23 | },
24 |
25 | plugins: [
26 | plugins.vue(),
27 | plugins.css(version),
28 | plugins.minifyCss(),
29 | plugins.version(),
30 | plugins.base(`${BASE}${version}`),
31 | plugins.shareHtml(version),
32 | plugins.env('production')
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/build/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const { entry, output, resolve, devServer, rules, plugins, optimization } = require('./blocks')
2 |
3 | module.exports = {
4 | mode: 'development',
5 |
6 | entry: entry.dev(),
7 | output: output(),
8 | resolve: resolve(),
9 |
10 | optimization: optimization(),
11 |
12 | devtool: 'inline-source-map',
13 | devServer: devServer(9002),
14 |
15 | module: {
16 | rules: [
17 | rules.vue(),
18 | rules.javascript(),
19 | rules.images(),
20 | rules.styles('dev'),
21 | rules.fonts(),
22 | rules.examples()
23 | ]
24 | },
25 |
26 | plugins: [
27 | plugins.vue(),
28 | plugins.base('.'),
29 | plugins.jarvis(1337),
30 | plugins.bundleAnalyzer(),
31 | plugins.hmr(),
32 | plugins.shareHtml(),
33 | ...plugins.devHtml('standalone.html'),
34 | plugins.env('development')
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/build/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const { entry, output, resolve, optimization, rules, plugins } = require('./blocks')
2 |
3 | module.exports = {
4 | mode: 'production',
5 | entry: entry.prod(),
6 | output: output(),
7 |
8 | optimization: optimization(),
9 |
10 | resolve: resolve(),
11 |
12 | module: {
13 | rules: [
14 | rules.vue(),
15 | rules.javascript(),
16 | rules.images(),
17 | rules.styles('prod'),
18 | rules.fonts()
19 | ]
20 | },
21 |
22 | plugins: [
23 | plugins.vue(),
24 | plugins.css(),
25 | plugins.minifyCss(),
26 | plugins.version(),
27 | plugins.base('.'),
28 | plugins.shareHtml(),
29 | plugins.env('production')
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectId": "mvgejp",
3 | "baseUrl": "http://localhost:8080"
4 | }
5 |
--------------------------------------------------------------------------------
/cypress/fixtures/audio.json:
--------------------------------------------------------------------------------
1 | {
2 | "audio": [{
3 | "url": "assets/podlove.mp3",
4 | "size": "486839",
5 | "title": "MP3 Audio (mp3)",
6 | "mimeType": "audio\/mpeg"
7 | }, {
8 | "url": "assets/podlove.m4a",
9 | "size": "1931981",
10 | "title": "MP4 Audio (m4a)",
11 | "mimeType": "audio\/mp4"
12 | }]
13 | }
14 |
--------------------------------------------------------------------------------
/cypress/fixtures/chapters.json:
--------------------------------------------------------------------------------
1 | {
2 | "chapters": [{
3 | "start": "00:00:0.000",
4 | "title": "I'm a thing",
5 | "href": "",
6 | "image": ""
7 | }, {
8 | "start": "00:00:08.000",
9 | "title": "Yes! In your face, Gandhi",
10 | "href": "",
11 | "image": ""
12 | }, {
13 | "start": "00:00:10.000",
14 | "title": "Oh, you're a dollar naughtier than most",
15 | "href": "",
16 | "image": ""
17 | }]
18 | }
19 |
--------------------------------------------------------------------------------
/cypress/fixtures/contributors.json:
--------------------------------------------------------------------------------
1 | {
2 | "contributors": [
3 | {
4 | "avatar": "assets/fry.jpg",
5 | "name": "Philip J. Fry",
6 | "role": {
7 | "id": "9",
8 | "slug": "team",
9 | "title": "Team"
10 | },
11 | "group": {
12 | "id": "1",
13 | "slug": "onair",
14 | "title": "On Air"
15 | },
16 | "comment": null
17 | },
18 | {
19 | "avatar": "assets/farnsworth.png",
20 | "name": "Professor Farnsworth",
21 | "role": {
22 | "id": "9",
23 | "slug": "team",
24 | "title": "Team"
25 | },
26 | "group": {
27 | "id": "1",
28 | "slug": "onair",
29 | "title": "On Air"
30 | },
31 | "comment": null
32 | },
33 | {
34 | "avatar": "assets/leela.jpg",
35 | "name": "Turanga Leela",
36 | "role": {
37 | "id": "9",
38 | "slug": "team",
39 | "title": "Team"
40 | },
41 | "group": {
42 | "id": "1",
43 | "slug": "onair",
44 | "title": "On Air"
45 | },
46 | "comment": null
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/cypress/fixtures/episode.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "And until then, I can never die?",
3 | "subtitle": "We'll need to have a look inside you with this camera. Robot 1-X, save my friends! And Zoidberg! Michelle, I don't regret this, but I both rue and lament it. Look, last night was a mistake. So, how 'bout them Knicks?",
4 | "summary": "I'm a thing. Hello Morbo, how's the family? When I was first asked to make a film about my nephew, Hubert Farnsworth, I thought \"Why should I?\" Then later, Leela made the film. But if I did make it, you can bet there would have been more topless women on motorcycles. Roll film!\nI can explain. It's very valuable. You're going to do his laundry? Ah, the 'Breakfast Club' soundtrack! I can't wait til I'm old enough to feel ways about stuff! So, how 'bout them Knicks?",
5 | "publicationDate": "2999-11-02T20:31:30+00:00",
6 | "poster": "assets/episode.png",
7 | "duration": "00:00:12",
8 | "link": "http://link/to/episode"
9 | }
10 |
--------------------------------------------------------------------------------
/cypress/fixtures/reference.json:
--------------------------------------------------------------------------------
1 | {
2 | "reference": {
3 | "config": "//localhost:8080/fixtures/example.json",
4 | "share": "//localhost:8080/share",
5 | "origin": "//localhost:8080/standalone.html"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/cypress/fixtures/runtime.json:
--------------------------------------------------------------------------------
1 | {
2 | "runtime": {
3 | "locale": "en"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/fixtures/show.json:
--------------------------------------------------------------------------------
1 | {
2 | "show": {
3 | "title": "Belligerent and numerous.",
4 | "subtitle": "I saw you with those two \"ladies of the evening\" at Elzars. Explain that. Who am I making this out to? Tell them I hate them. Hello Morbo, how's the family? Guards! Bring me the forms I need to fill out to have her taken away!",
5 | "summary": "Kids don't turn rotten just from watching TV. You'll have all the Slurm you can drink when you're partying with Slurms McKenzie! It's just like the story of the grasshopper and the octopus. All year long, the grasshopper kept burying acorns for winter, while the octopus mooched off his girlfriend and watched TV. But then the winter came, and the grasshopper died, and the octopus ate all his acorns. Also he got a race car. Is any of this getting through to you?",
6 | "poster": "assets/show.png",
7 | "link": "http://link/to/show"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/cypress/helpers/state.js:
--------------------------------------------------------------------------------
1 | const setState = (...fragments) => ({ PODLOVE_STORE }) => {
2 | const state = fragments.reduce((result, item) => Object.assign({}, result, item), {})
3 | PODLOVE_STORE.dispatch({ type: 'INIT', payload: state })
4 | }
5 |
6 | module.exports = { setState }
7 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/cypress/selectors/controllbar.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | controls: {
3 | playButton: {
4 | button: () => cy.get('#control-bar--play-button'),
5 | duration: () => cy.get('#control-bar--play-button--duration'),
6 | pause: () => cy.get('#control-bar--play-button--pause'),
7 | play: () => cy.get('#control-bar--play-button--play'),
8 | replay: () => cy.get('#control-bar--play-button--replay')
9 | },
10 | steppers: {
11 | forward: () => cy.get('#control-bar--step-forward-button'),
12 | back: () => cy.get('#control-bar--step-back-button')
13 | },
14 | chapters: {
15 | next: () => cy.get('#control-bar--chapter-next-button'),
16 | back: () => cy.get('#control-bar--chapter-back-button')
17 | }
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/cypress/selectors/header.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | show: {
3 | title: () => cy.get('#header-showtitle')
4 | },
5 | episode: {
6 | title: () => cy.get('#header-title'),
7 | subtitle: () => cy.get('#header-subtitle')
8 | },
9 | headerPosterContainer: () => cy.get('#header-poster'),
10 | headerPoster: () => cy.get('#header-poster img')
11 | })
12 |
--------------------------------------------------------------------------------
/cypress/selectors/index.js:
--------------------------------------------------------------------------------
1 | const controllbar = require('./controllbar')
2 | const progressbar = require('./progressbar')
3 | const header = require('./header')
4 |
5 | // Tabs
6 | const audio = require('./tabs/audio')
7 | const chapters = require('./tabs/chapters')
8 | const files = require('./tabs/files')
9 | const info = require('./tabs/info')
10 | const share = require('./tabs/share')
11 |
12 | module.exports = cy => Object.assign({}, controllbar(cy), progressbar(cy), header(cy), {
13 | tabs: {
14 | audio: audio(cy),
15 | chapters: chapters(cy),
16 | files: files(cy),
17 | info: info(cy),
18 | share: share(cy)
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/cypress/selectors/progressbar.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | progressbar: () => cy.get('#progress-bar'),
3 | timers: {
4 | left: () => cy.get('#progress-bar--timer-left'),
5 | current: () => cy.get('#progress-bar--timer-current')
6 | },
7 | chapter: {
8 | current: () => cy.get('#progress-bar--current-chapter'),
9 | title: () => cy.get('#progress-bar--current-chapter .chapter-title')
10 | },
11 | progress: {
12 | bar: () => cy.get('#progress-bar--progress'),
13 | range: () => cy.get('#progress-bar--progress .progress-range'),
14 | chapters: () => cy.get('#progress-bar--progress .chapters-progress .indicator')
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/cypress/selectors/tabs/audio.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | header: () => cy.get(`#tabs [rel="audio"]`),
3 | container: () => cy.get('#tab-audio'),
4 | volume: {
5 | current: () => cy.get('#tab-audio--volume--value'),
6 | input: () => cy.get('#tab-audio--volume--input input'),
7 | mute: () => cy.get('#tab-audio--volume--mute')
8 | },
9 | rate: {
10 | current: () => cy.get('#tab-audio--rate--value'),
11 | input: () => cy.get('#tab-audio--rate--input input')
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/cypress/selectors/tabs/chapters.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | header: () => cy.get(`#tabs [rel="chapters"]`),
3 | container: () => cy.get('#tab-chapters'),
4 | entries: () => cy.get('#tab-chapters .chapters--entry'),
5 | indices: () => cy.get('#tab-chapters .chapters--entry .index'),
6 | titles: () => cy.get('#tab-chapters .chapters--entry .title'),
7 | activeTimer: () => cy.get('#tab-chapters .chapters--entry.active .timer'),
8 | timers: () => cy.get('#tab-chapters .chapters--entry .timer')
9 | })
10 |
--------------------------------------------------------------------------------
/cypress/selectors/tabs/files.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | header: () => cy.get(`#tabs [rel="files"]`),
3 | container: () => cy.get('#tab-files'),
4 | audio: () => cy.get('#tab-files--audio')
5 | })
6 |
--------------------------------------------------------------------------------
/cypress/selectors/tabs/info.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | header: () => cy.get(`#tabs [rel="info"]`),
3 | container: () => cy.get('#tab-info'),
4 | episode: {
5 | title: () => cy.get('#tab-info--episode-title'),
6 | meta: () => cy.get('#tab-info--episode-meta'),
7 | subtitle: () => cy.get('#tab-info--episode-subtitle'),
8 | summary: () => cy.get('#tab-info--episode-summary'),
9 | link: () => cy.get('#tab-info--episode-link')
10 | },
11 | show: {
12 | title: () => cy.get('#tab-info--show-title'),
13 | poster: () => cy.get('#tab-info--show-poster'),
14 | summary: () => cy.get('#tab-info--show-summary'),
15 | link: () => cy.get('#tab-info--show-link')
16 | },
17 | speakers: () => cy.get('#tab-info--speakers')
18 | })
19 |
--------------------------------------------------------------------------------
/cypress/selectors/tabs/share.js:
--------------------------------------------------------------------------------
1 | module.exports = cy => ({
2 | header: () => cy.get(`#tabs [rel="share"]`),
3 | container: () => cy.get('#tab-share'),
4 | content: {
5 | show: () => cy.get('#tab-share--content--show'),
6 | episode: () => cy.get('#tab-share--content--episode'),
7 | chapter: () => cy.get('#tab-share--content--chapter'),
8 | time: () => cy.get('#tab-share--content--time')
9 | },
10 | channels: {
11 | twitter: () => cy.get(`#tab-share--channels--twitter a`),
12 | reddit: () => cy.get(`#tab-share--channels--reddit a`),
13 | mail: () => cy.get(`#tab-share--channels--mail a`),
14 | pinterest: () => cy.get(`#tab-share--channels--pinterest a`),
15 | linkedin: () => cy.get(`#tab-share--channels--linkedin a`)
16 | },
17 | embed: {
18 | container: () => cy.get('#tab-share--embed--link'),
19 | input: () => cy.get('#tab-share--share-embed--input'),
20 | size: () => cy.get('#tab-share--share-embed--size')
21 | },
22 | link: {
23 | container: () => cy.get('#tab-share--share-link'),
24 | input: () => cy.get('#tab-share--share-link--input')
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | /* global Cypress, cy */
2 | const domSelectors = require('../selectors')
3 |
4 | Cypress.Commands.add('bootstrap', () => {
5 | cy.visit('/test.html')
6 | cy.fixture('episode').as('episode')
7 | cy.fixture('show').as('show')
8 | cy.fixture('audio').as('audio')
9 | cy.fixture('chapters').as('chapters')
10 | cy.fixture('contributors').as('contributors')
11 | cy.fixture('reference').as('reference')
12 | cy.fixture('runtime').as('runtime')
13 | })
14 |
15 | Cypress.Commands.add('embed', (params = {}) => {
16 | const query = Object.keys(params).reduce((result, key) => [...result, `${key}=${params[key]}`], []).join('&')
17 | cy.visit(`/embed.html${query ? '?' + query : ''}`)
18 | })
19 |
20 | Cypress.Commands.add('iframe', {
21 | prevSubject: 'element'
22 | }, $iframe => {
23 | return new Cypress.Promise(resolve => {
24 | resolve($iframe.contents().find('body'))
25 | })
26 | })
27 |
28 | Cypress.Commands.add('play', () => {
29 | const selectors = domSelectors(cy)
30 | selectors.controls.playButton.button().then(btn => {
31 | btn.click()
32 | selectors.controls.playButton.pause()
33 | })
34 | })
35 |
36 | Cypress.Commands.add('pause', () => {
37 | const selectors = domSelectors(cy)
38 |
39 | selectors.controls.playButton.pause()
40 | selectors.controls.playButton.button().then(btn => {
41 | btn.click()
42 | })
43 | })
44 |
45 | Cypress.Commands.add('tab', tab => {
46 | const selectors = domSelectors(cy)
47 | selectors.tabs[tab].header().click()
48 | selectors.tabs[tab].container()
49 | })
50 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | import './commands'
2 |
--------------------------------------------------------------------------------
/cypress/testbed/assets/bender.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/bender.png
--------------------------------------------------------------------------------
/cypress/testbed/assets/episode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/episode.png
--------------------------------------------------------------------------------
/cypress/testbed/assets/fry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/fry.png
--------------------------------------------------------------------------------
/cypress/testbed/assets/podlove.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/podlove.m4a
--------------------------------------------------------------------------------
/cypress/testbed/assets/podlove.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/podlove.mp3
--------------------------------------------------------------------------------
/cypress/testbed/assets/professor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/professor.png
--------------------------------------------------------------------------------
/cypress/testbed/assets/show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/cypress/testbed/assets/show.png
--------------------------------------------------------------------------------
/cypress/testbed/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/JsonEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
36 |
37 |
54 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/Playground.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Config
6 |
7 |
8 |
9 |
10 |
35 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/PodloveWebPlayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/StoreSubscribe.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
37 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | const base = process.env.BASE || ''
2 |
3 | module.exports = {
4 | title: 'Podlove Web Player 4.0',
5 | base,
6 | description: 'The fast, flexible and responsive podcast player powered by podlove meta data.',
7 | theme: 'millidocs',
8 | head: [
9 | ['link', { rel: 'icon', href: '/favicon.png' }],
10 | ['script', { type: 'text/javascript', src: `embed.js` }],
11 | ['script', { type: 'text/javascript', src: `extensions/external-events.js` }]
12 | ],
13 | markdown: {
14 | anchor: {
15 | permalink: false,
16 | permalinkBefore: false
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/docs/.vuepress/public/favicon.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overview
3 | navigation: 1
4 | ---
5 |
6 | # Podlove Web Player 4.0
7 | ### The fast, flexible and responsive podcast player powered by podlove meta data.
8 |
9 |
10 |
11 | ## Features
12 |
13 | - Standalone and Wordpress Plugin
14 | - Responsive
15 | - Theming
16 | - Sharing
17 | - State Persistance
18 | - Deep Linking
19 |
20 | ## Previous Versions
21 |
22 | - [Podlove Web Player v3 Docs](docs.podlove.org/podlove-web-player-v3/)
23 | - [Podlove Web Player v2 Docs](docs.podlove.org/podlove-web-player-v3/versions/v2.html)
24 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | navigation: 2
3 | ---
4 |
5 | # Installation
6 |
7 | Podlove Webplayer can be integrated in different ways. We provide the always latest version via CDN or all versions as an npm package.
8 |
9 | ## CDN
10 |
11 | The easiest way to integrate the player is to simply integrate this script in your page:
12 |
13 | For https context:
14 | ```html
15 |
16 | ```
17 |
18 | For http context:
19 | ```html
20 |
21 | ```
22 |
23 | Afterwards `podlovePlayer` should be available on the window object:
24 |
25 | ```html
26 |
29 | ```
30 |
31 | Please be aware to __not__ set `reference.base` because this will break the binding to the cdn.
32 |
33 |
34 | ## NPM
35 |
36 | If you want to serve a special player version you can find the player as the npm package [@podlove/podlove-web-player](https://www.npmjs.com/package/@podlove/podlove-web-player).
37 |
38 | To integrate the player you first have to install tha package:
39 |
40 | ```javascript
41 | npm install @podlove/podlove-web-player --save
42 | ```
43 |
44 | Afterwards move the player assets to a public folder of some webserver. By default the player will try to load further chunks from the webserver base. If the player files are located in a subpath you have to adapt the `reference.base` accordingly (see [config]({{ $withBase('config.html') }})
45 |
--------------------------------------------------------------------------------
/docs/player-dispatches.md:
--------------------------------------------------------------------------------
1 | ---
2 | navigation: 8
3 | ---
4 |
5 | # Player Dispatches
6 |
7 | ```javascript
8 | podlovePlayer(selector, config).then(function (store) {
9 | store.dispatch({
10 | type: TYPE,
11 | payload: PAYLOAD
12 | })
13 | });
14 | ```
15 |
16 | Every player interaction can be triggered via the [redux store](http://redux.js.org/docs/api/Store.html).
17 | Accessing the players store enables the full control of the player while running. The [color picker]() is a good example.
18 |
19 | You can find a complete list of available types in the [types definition](https://github.com/podlove/podlove-web-player/blob/development/src/store/types.js).
20 |
21 | ## Dispatching to the Player
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/player-subscriptions.md:
--------------------------------------------------------------------------------
1 | ---
2 | navigation: 9
3 | ---
4 |
5 | # Player Subscriptions
6 |
7 | ```javascript
8 | podlovePlayer(selector, config).then(function (store) {
9 | store.subscribe(() => {
10 | const { lastAction } = store.getState()
11 | // Do something with the last action
12 | console.log({ type: lastAction.type, payload: lastAction.payload })
13 | })
14 | });
15 | ```
16 |
17 | Every player interaction is reflected in the [redux store](http://redux.js.org/docs/api/Store.html).
18 | You can subscribe to every player event by attatching to the latest action.
19 |
20 | You can find a complete list of available types in the [types definition](https://github.com/podlove/podlove-web-player/blob/development/src/store/types.js).
21 |
22 | ## Subscribing to the Player
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/playground.md:
--------------------------------------------------------------------------------
1 | ---
2 | navigation: 5
3 | ---
4 |
5 | # Playground
6 |
7 |
8 |
--------------------------------------------------------------------------------
/docs/theme.md:
--------------------------------------------------------------------------------
1 | ---
2 | navigation: 4
3 | ---
4 |
5 | # Theming
6 |
7 |
8 |
9 | ## Color Calculation
10 |
11 | The player requires at least one main color. If not provided the default color will be used.
12 | Without setting a highlight color the essential control elements are colored black or white depending on a calculated [WCGA contrast ratio](https://www.w3.org/TR/WCAG20/#contrast-ratiodef).
13 | With a highlight color in place these control elements will be colored accordingly.
14 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "alias": "podlove-web-player",
3 | "type": "npm",
4 | "engines": {
5 | "node": "10.0.0"
6 | },
7 | "files": [
8 | "src",
9 | "build",
10 | "docs",
11 | ".babelrc"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/screenshot.png
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'core'
2 |
3 | // Store
4 | import store from 'store'
5 | import actions from 'store/actions'
6 |
7 | // UI Components
8 | import App from './components/App'
9 |
10 | export default config => {
11 | // Initialize meta for store
12 | store.dispatch(actions.init(config))
13 |
14 | window.PODLOVE_STORE = store
15 |
16 | return createApp('PodlovePlayer', App)
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/header/Error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t(error.title) }}
4 |
{{ $t(error.message) }}
5 |
6 |
7 |
8 |
28 |
29 |
62 |
--------------------------------------------------------------------------------
/src/components/header/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
30 |
31 |
39 |
--------------------------------------------------------------------------------
/src/components/icons/AudioIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
--------------------------------------------------------------------------------
/src/components/icons/CalendarIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/ChapterBackIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/ChapterNextIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/ChaptersIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/ClockIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/CloseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/CopyIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/DownloadIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/EmbedIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/ErrorIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/FacebookIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/FilesAudioIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/FollowIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ text }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
30 |
--------------------------------------------------------------------------------
/src/components/icons/InfoIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/icons/LinkIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/LinkedinIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/MailIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/MinusIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/icons/NextSearchIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
25 |
--------------------------------------------------------------------------------
/src/components/icons/PauseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
--------------------------------------------------------------------------------
/src/components/icons/PinterestIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/PlayIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/src/components/icons/PlusIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/icons/PreviousSearchIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
25 |
--------------------------------------------------------------------------------
/src/components/icons/ReloadIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/SearchDeleteIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
25 |
--------------------------------------------------------------------------------
/src/components/icons/ShareChapterIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/src/components/icons/ShareEpisodeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/src/components/icons/ShareIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/SharePlaytimeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/ShareShowIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/StepBackIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
--------------------------------------------------------------------------------
/src/components/icons/StepForwardIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
--------------------------------------------------------------------------------
/src/components/icons/TranscriptsIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
21 |
--------------------------------------------------------------------------------
/src/components/icons/TwitterIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/icons/icon-container.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { mapState } from 'redux-vuex'
3 |
4 | export default iconComponent => Vue.component('icon', {
5 | props: ['width', 'height'],
6 |
7 | render: function (h) {
8 | return h(iconComponent, {
9 | props: {
10 | color: this.theme.icon.color,
11 | background: this.theme.icon.background,
12 | width: this.width,
13 | height: this.height
14 | }
15 | })
16 | },
17 |
18 | data: mapState('theme')
19 | })
20 |
--------------------------------------------------------------------------------
/src/components/player/Player.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
31 |
32 |
45 |
--------------------------------------------------------------------------------
/src/components/player/control-bar/ChapterNextButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ a11y }}
5 |
6 |
7 |
8 |
43 |
--------------------------------------------------------------------------------
/src/components/player/control-bar/LoadingIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
23 |
24 |
58 |
--------------------------------------------------------------------------------
/src/components/player/control-bar/StepBackButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.PLAYER_STEPPER_BACK', { seconds: 15 }) }}
5 |
6 |
7 |
8 |
32 |
--------------------------------------------------------------------------------
/src/components/player/control-bar/StepForwardButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.PLAYER_STEPPER_FORWARD', { seconds: 30 }) }}
5 |
6 |
7 |
8 |
30 |
--------------------------------------------------------------------------------
/src/components/player/progress-bar/ChapterIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
34 |
35 |
51 |
--------------------------------------------------------------------------------
/src/components/player/progress-bar/CurrentChapter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | {{ title }}
7 |
8 |
9 |
10 |
11 |
46 |
47 |
58 |
--------------------------------------------------------------------------------
/src/components/player/progress-bar/ProgressBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
19 |
20 |
25 |
--------------------------------------------------------------------------------
/src/components/shared/ButtonGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
34 |
--------------------------------------------------------------------------------
/src/components/shared/CopyTooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ $t('MESSAGES.COPIED') }}
8 |
9 |
10 |
11 |
12 |
51 |
52 |
57 |
--------------------------------------------------------------------------------
/src/components/shared/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
22 |
23 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/components/shared/InputGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
21 |
22 |
51 |
--------------------------------------------------------------------------------
/src/components/shared/InputSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ option }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
34 |
35 |
51 |
--------------------------------------------------------------------------------
/src/components/shared/InputText.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
43 |
--------------------------------------------------------------------------------
/src/components/shared/TabBody.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
22 |
23 |
40 |
--------------------------------------------------------------------------------
/src/components/tabs/audio/Audio.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
30 |
31 |
39 |
--------------------------------------------------------------------------------
/src/components/tabs/chapters/Chapters.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
27 |
28 |
36 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelEmbed.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_EMBED') }}
5 |
6 |
7 |
8 |
28 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelFacebook.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_CHANNEL', { channel: 'Facebook' }) }}
5 |
6 |
7 |
8 |
26 |
27 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelLinkedin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_CHANNEL', { channel: 'Linkedin' }) }}
5 |
6 |
7 |
8 |
26 |
27 |
34 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelMail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_CHANNEL', { channel: 'Mail' }) }}
5 |
6 |
7 |
8 |
26 |
27 |
34 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelPinterest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_CHANNEL', { channel: 'Pinterest' }) }}
5 |
6 |
7 |
8 |
26 |
27 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelReddit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_CHANNEL', { channel: 'Reddit' }) }}
5 |
6 |
7 |
8 |
26 |
27 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/tabs/share/channels/ChannelTwitter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('A11Y.SHARE_CHANNEL', { channel: 'Twitter' }) }}
5 |
6 |
7 |
8 |
26 |
27 |
36 |
--------------------------------------------------------------------------------
/src/components/tabs/transcripts/Follow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
44 |
--------------------------------------------------------------------------------
/src/components/tabs/transcripts/Prerender.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/src/core/directives/index.js:
--------------------------------------------------------------------------------
1 | import resize from './resize'
2 | import marquee from './marquee'
3 |
4 | const registerDirectives = context => {
5 | context.Renderer.directive('resize', resize)
6 | context.Renderer.directive('marquee', marquee)
7 |
8 | return context
9 | }
10 |
11 | export {
12 | registerDirectives
13 | }
14 |
--------------------------------------------------------------------------------
/src/core/directives/marquee.js:
--------------------------------------------------------------------------------
1 | import { setStyles, addClasses, removeClasses } from 'utils/dom'
2 |
3 | const marquee = el => {
4 | const scroller = el.firstChild
5 |
6 | const animationDuration = scroller.scrollWidth / 50
7 |
8 | setStyles({
9 | 'white-space': 'nowrap',
10 | 'overflow-x': 'hidden'
11 | })(scroller)
12 |
13 | setStyles({
14 | height: `${el.offsetHeight}px`
15 | })(el)
16 |
17 | setStyles({
18 | height: `${scroller.offsetHeight}px`,
19 | width: 'auto'
20 | })(scroller)
21 |
22 | setStyles({
23 | 'overflow-x': 'visible'
24 | })(scroller)
25 |
26 | setTimeout(() => {
27 | if (scroller.scrollWidth > el.offsetWidth) {
28 | addClasses('marquee-container')(el)
29 | addClasses('marquee')(scroller)
30 | setStyles({
31 | 'animation-duration': `${animationDuration > 10 ? animationDuration : 10}s`, // min 10s
32 | width: `${scroller.scrollWidth}px`
33 | })(scroller)
34 | } else {
35 | removeClasses('marquee-container')(el)
36 | removeClasses('marquee')(scroller)
37 | }
38 | }, 0)
39 | }
40 |
41 | export default {
42 | bind (el) {
43 | window.addEventListener('resize', () => marquee(el))
44 | },
45 | inserted: marquee,
46 | update: marquee
47 | }
48 |
--------------------------------------------------------------------------------
/src/core/directives/resize.js:
--------------------------------------------------------------------------------
1 | /* global MutationObserver */
2 |
3 | export default {
4 | bind (el, { value }) {
5 | const observer = new MutationObserver(value)
6 |
7 | observer.observe(el, { childList: true })
8 | window.addEventListener('resize', value)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/core/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { compose } from 'lodash/fp'
3 | import { head } from 'lodash'
4 | import { registerDirectives } from './directives'
5 | import { registerLang } from './lang'
6 |
7 | const registerApp = context => {
8 | return new context.Renderer({
9 | i18n: context.i18n,
10 | el: head(document.getElementsByTagName(context.selector)),
11 | render: h => h(context.App)
12 | })
13 | }
14 |
15 | const boot = compose(registerApp, registerLang, registerDirectives)
16 |
17 | export const createApp = (selector, App) => {
18 | return boot({
19 | Renderer: Vue,
20 | App,
21 | selector
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/src/core/lang/index.js:
--------------------------------------------------------------------------------
1 | import VueI18n from 'vue-i18n'
2 |
3 | const messages = {
4 | en: require('../../lang/en.json'),
5 | de: require('../../lang/de.json'),
6 | eo: require('../../lang/eo.json'),
7 | ru: require('../../lang/ru.json')
8 | }
9 |
10 | export const registerLang = context => {
11 | context.Renderer.use(VueI18n)
12 |
13 | return {
14 | ...context,
15 | i18n: new VueI18n({
16 | locale: 'en',
17 | fallbackLocale: 'en',
18 | messages
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/effects/components/index.js:
--------------------------------------------------------------------------------
1 | import episodeComponents from './episode'
2 | import liveComponents from './live'
3 |
4 | export default (store, action) => {
5 | const state = store.getState()
6 |
7 | if (state.mode === 'live') {
8 | liveComponents(store, action)
9 | } else {
10 | episodeComponents(store, action)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/effects/playback.js:
--------------------------------------------------------------------------------
1 | import actions from 'store/actions'
2 | import { SET_PLAYBACK_PARAMS, SET_PLAYTIME } from 'store/types'
3 |
4 | import { handleActions } from 'utils/effects'
5 |
6 | let paused = false
7 |
8 | export default handleActions({
9 | [SET_PLAYBACK_PARAMS]: ({ dispatch }, { payload }) => {
10 | if (payload.starttime) {
11 | dispatch(actions.setPlaytime(payload.starttime))
12 | dispatch(actions.idle())
13 | }
14 |
15 | if (payload.autoplay) {
16 | dispatch(actions.play())
17 | }
18 | },
19 |
20 | [SET_PLAYTIME]: ({ dispatch }, { payload }, state) => {
21 | if (!state.playback.stoptime) {
22 | return
23 | }
24 |
25 | if (state.playback.stoptime <= payload && paused === false) {
26 | dispatch(actions.pause())
27 | paused = true
28 | }
29 | }
30 | })
31 |
--------------------------------------------------------------------------------
/src/effects/quantiles.js:
--------------------------------------------------------------------------------
1 | import actions from 'store/actions'
2 | import { SET_PLAYTIME, NEXT_CHAPTER, PREVIOUS_CHAPTER, UPDATE_PLAYTIME } from 'store/types'
3 |
4 | import { handleActions } from 'utils/effects'
5 |
6 | let startTime = null
7 |
8 | const resetStarttime = () => {
9 | startTime = null
10 | }
11 |
12 | export default handleActions({
13 | [SET_PLAYTIME]: ({ dispatch }, { payload }) => {
14 | if (!startTime) {
15 | startTime = payload
16 | }
17 |
18 | dispatch(actions.setQuantile(startTime, payload))
19 | },
20 |
21 | [NEXT_CHAPTER]: resetStarttime,
22 | [PREVIOUS_CHAPTER]: resetStarttime,
23 | [UPDATE_PLAYTIME]: resetStarttime
24 | })
25 |
--------------------------------------------------------------------------------
/src/effects/runtime.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 |
3 | import { INIT } from 'store/types'
4 | import actions from 'store/actions'
5 | import runtime from 'utils/runtime'
6 |
7 | import { handleActions } from 'utils/effects'
8 |
9 | export default handleActions({
10 | [INIT]: ({ dispatch }, { payload }) => {
11 | const config = get(payload, 'runtime', {})
12 |
13 | dispatch(actions.setRuntime({
14 | ...runtime,
15 | ...config
16 | }))
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/src/effects/storage/index.js:
--------------------------------------------------------------------------------
1 | import episodeStorage from './episode'
2 | import liveStorage from './live'
3 |
4 | export default storage => (store, action) => {
5 | const state = store.getState()
6 |
7 | if (state.mode === 'live') {
8 | liveStorage(storage)(store, action)
9 | } else {
10 | episodeStorage(storage)(store, action)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/effects/storage/live.js:
--------------------------------------------------------------------------------
1 | import { get, noop } from 'lodash'
2 | import { hashCode } from 'hashcode'
3 |
4 | import { handleActions } from 'utils/effects'
5 |
6 | import actions from 'store/actions'
7 | import { INIT, SET_VOLUME, TOGGLE_TAB } from 'store/types'
8 |
9 | let storage = {
10 | get: noop,
11 | set: noop
12 | }
13 |
14 | const metaHash = config =>
15 | hashCode().value({
16 | ...config,
17 | playtime: 0
18 | })
19 |
20 | export default storageFactory => handleActions({
21 | [INIT]: ({ dispatch }, { payload }) => {
22 | storage = storageFactory(metaHash(payload))
23 |
24 | const storedTabs = storage.get('tabs')
25 | const storedVolume = storage.get('volume')
26 |
27 | if (storedTabs) {
28 | dispatch(actions.setTabs(storedTabs))
29 | }
30 |
31 | if (storedVolume) {
32 | dispatch(actions.setVolume(storedVolume))
33 | }
34 | },
35 |
36 | [SET_VOLUME]: (store, action, state) => {
37 | const volume = get(state, 'volume', 1)
38 | storage.set('volume', volume)
39 | },
40 |
41 | [TOGGLE_TAB]: (store, action, state) => {
42 | const tabs = get(state, 'tabs', {})
43 | storage.set('tabs', tabs)
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/effects/transcripts/active.js:
--------------------------------------------------------------------------------
1 | import { binarySearch } from 'utils/binary-search'
2 |
3 | import { noop, debounce } from 'lodash'
4 | import { compose } from 'lodash/fp'
5 |
6 | import { prohibitiveDispatch, handleActions } from 'utils/effects'
7 | import { inAnimationFrame } from 'utils/helper'
8 |
9 | import actions from 'store/actions'
10 | import { SET_TRANSCRIPTS_TIMELINE, SET_TRANSCRIPTS_CHAPTERS, SET_PLAYTIME, UPDATE_PLAYTIME, DISABLE_GHOST_MODE, SIMULATE_PLAYTIME } from 'store/types'
11 |
12 | let update = noop
13 | let debouncedUpdate = noop
14 |
15 | const createIndex = ({ dispatch }, { payload = [] }, { playtime }) => {
16 | const searchIndex = payload.map(({ start }) => start)
17 |
18 | update = inAnimationFrame(
19 | compose(
20 | prohibitiveDispatch(dispatch, actions.updateTranscripts),
21 | binarySearch(searchIndex)
22 | )
23 | )
24 |
25 | debouncedUpdate = debounce(update, 200)
26 | update(playtime)
27 | }
28 |
29 | export default handleActions({
30 | [SET_TRANSCRIPTS_TIMELINE]: createIndex,
31 | [SET_TRANSCRIPTS_CHAPTERS]: createIndex,
32 |
33 | [SET_PLAYTIME]: (_, { payload }) => update(payload),
34 | [UPDATE_PLAYTIME]: (_, { payload }) => update(payload),
35 |
36 | [DISABLE_GHOST_MODE]: (_, action, { playtime }) => debouncedUpdate(playtime),
37 | [SIMULATE_PLAYTIME]: (_, { payload }) => debouncedUpdate(payload)
38 | })
39 |
--------------------------------------------------------------------------------
/src/effects/transcripts/index.js:
--------------------------------------------------------------------------------
1 | // Fetches data from given transcript, transforms fetched data and dispatches transformed result to store
2 | import fetchEffects from './fetch'
3 | import activeEffects from './active'
4 | import searchEffects from './search'
5 |
6 | import { callWith } from 'utils/helper'
7 |
8 | const effects = [fetchEffects, activeEffects, searchEffects]
9 |
10 | export default (store, action) => effects.map(callWith(store, action))
11 |
--------------------------------------------------------------------------------
/src/effects/transcripts/search.js:
--------------------------------------------------------------------------------
1 | import { textSearch } from 'utils/text-search'
2 |
3 | import { noop } from 'lodash'
4 | import { compose } from 'lodash/fp'
5 |
6 | import { prohibitiveDispatch, handleActions } from 'utils/effects'
7 | import { inAnimationFrame } from 'utils/helper'
8 |
9 | import actions from 'store/actions'
10 | import { SET_TRANSCRIPTS_TIMELINE, SEARCH_TRANSCRIPTS } from 'store/types'
11 |
12 | let search = noop
13 |
14 | export default handleActions({
15 | [SET_TRANSCRIPTS_TIMELINE]: ({ dispatch }, { payload = [] }) => {
16 | const searchIndex = payload
17 | .map(({ texts = [] }) => texts.map(({ text }) => text).join(' '))
18 |
19 | search = inAnimationFrame(
20 | compose(
21 | prohibitiveDispatch(dispatch, actions.setTranscriptsSearchResults),
22 | textSearch(searchIndex)
23 | )
24 | )
25 | },
26 |
27 | [SEARCH_TRANSCRIPTS]: (store, { payload }) => search(payload)
28 | })
29 |
--------------------------------------------------------------------------------
/src/effects/transcripts/search.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import sinon from 'sinon'
3 | import browserEnv from 'browser-env'
4 |
5 | import searchEffect from './search'
6 | import { timeline, state } from './fixtures'
7 |
8 | browserEnv(['window'])
9 |
10 | let store
11 |
12 | test.beforeEach(t => {
13 | window.requestAnimationFrame = cb => cb()
14 |
15 | store = {
16 | dispatch: sinon.stub(),
17 | getState: () => state
18 | }
19 | })
20 |
21 | test(`transcripts - search: exports a function`, t => {
22 | t.is(typeof searchEffect, 'function')
23 | })
24 |
25 | test(`transcripts - search: creates an search index on SET_TRANSCRIPTS_TIMELINE`, t => {
26 | searchEffect(store, {
27 | type: 'SET_TRANSCRIPTS_TIMELINE',
28 | payload: timeline
29 | })
30 |
31 | searchEffect(store, {
32 | type: 'SEARCH_TRANSCRIPTS',
33 | payload: 'fooo'
34 | })
35 |
36 | t.deepEqual(store.dispatch.getCall(0).args[0], {
37 | type: 'SET_SEARCH_TRANSCRIPTS_RESULTS', payload: [ 1 ]
38 | })
39 | })
40 |
41 | test(`transcripts - doesn't dispatch if no results are found`, t => {
42 | searchEffect(store, {
43 | type: 'SET_TRANSCRIPTS_TIMELINE',
44 | payload: timeline
45 | })
46 |
47 | searchEffect(store, {
48 | type: 'SEARCH_TRANSCRIPTS',
49 | payload: 'xyz'
50 | })
51 |
52 | t.deepEqual(store.dispatch.getCall(0).args[0], {
53 | type: 'SET_SEARCH_TRANSCRIPTS_RESULTS', payload: []
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/src/effects/volume.js:
--------------------------------------------------------------------------------
1 | import actions from 'store/actions'
2 | import { SET_VOLUME } from 'store/types'
3 |
4 | import { handleActions } from 'utils/effects'
5 |
6 | export default handleActions({
7 | [SET_VOLUME]: ({ dispatch }, { payload }) =>
8 | payload <= 0 ? dispatch(actions.mute()) : dispatch(actions.unmute())
9 | })
10 |
--------------------------------------------------------------------------------
/src/effects/volume.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import sinon from 'sinon'
3 |
4 | import volumeEffects from './volume'
5 |
6 | let store
7 |
8 | test.beforeEach(t => {
9 | store = {
10 | dispatch: sinon.stub(),
11 | getState: () => {}
12 | }
13 | })
14 |
15 | test(`volumeEffects: it dispatches MUTE on SET_VOLUME if volume equals 0`, t => {
16 | volumeEffects(store, {
17 | type: 'SET_VOLUME',
18 | payload: 0
19 | })
20 |
21 | t.deepEqual(store.dispatch.getCall(0).args[0], {
22 | type: 'MUTE'
23 | })
24 | })
25 |
26 | test(`volumeEffects: it dispatches UNMUTE on SET_VOLUME if volume is greate than 0`, t => {
27 | volumeEffects(store, {
28 | type: 'SET_VOLUME',
29 | payload: 0.2
30 | })
31 |
32 | t.deepEqual(store.dispatch.getCall(0).args[0], {
33 | type: 'UNMUTE'
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/src/embed/loader.js:
--------------------------------------------------------------------------------
1 | import color from 'color'
2 | import { get } from 'lodash'
3 |
4 | import { tag } from 'utils/dom'
5 | // eslint-disable-next-line
6 | import css from '!css-loader!sass-loader!../styles/_loader.scss'
7 |
8 | const style = tag('style', css.toString())
9 |
10 | const dom = ({ theme }) => {
11 | const light = '#fff'
12 | const dark = '#000'
13 |
14 | const main = get(theme, 'main', '#2B8AC6')
15 | const luminosity = color(main).luminosity()
16 |
17 | const highlight = get(theme, 'highlight', luminosity < 0.25 ? light : dark)
18 |
19 | return ``
24 | }
25 |
26 | const script = tag('script', `
27 | var loader = document.getElementById('loader')
28 |
29 | window.addEventListener('load', function() {
30 | loader.className += ' done'
31 |
32 | setTimeout(function () {
33 | loader.parentNode.removeChild(loader)
34 | }, 300)
35 | })
36 | `)
37 |
38 | export default config => ([style, dom(config), script].join(''))
39 |
--------------------------------------------------------------------------------
/src/embed/share.js:
--------------------------------------------------------------------------------
1 | import { urlParameters } from 'utils/url'
2 | import remoteConfig from 'utils/request'
3 |
4 | import { SET_PLAYBACK_PARAMS } from 'store/types'
5 |
6 | import app from '../app'
7 |
8 | remoteConfig(urlParameters.episode)
9 | .then(config => ({ ...config, display: 'embed' }))
10 | .then(app)
11 | .then(() => window.PODLOVE_STORE)
12 | .then(store => {
13 | store.dispatch({
14 | type: SET_PLAYBACK_PARAMS,
15 | payload: urlParameters
16 | })
17 | return store
18 | })
19 | .catch(err => {
20 | console.group(`Can't load Podlove Webplayer`)
21 | console.error('config', urlParameters.episode)
22 | console.error(err)
23 | console.groupEnd()
24 | })
25 |
--------------------------------------------------------------------------------
/src/embed/window.js:
--------------------------------------------------------------------------------
1 | import app from '../app'
2 |
3 | app(window.PODLOVE)
4 |
--------------------------------------------------------------------------------
/src/extensions/external-events.js:
--------------------------------------------------------------------------------
1 | import { compose, get } from 'lodash/fp'
2 | import { toPlayerTime } from 'utils/time'
3 | import actions from 'store/actions'
4 |
5 | const isPlayerInstance = id => (data = {}) => data.ref === id ? data : {}
6 |
7 | const handleAction = store => (data = {}) => {
8 | const action = actions[data.action]
9 | action && store.dispatch(action())
10 | return data
11 | }
12 |
13 | const handleTab = store => (data = {}) => {
14 | data.tab && store.dispatch(actions.toggleTab(data.tab))
15 | return data
16 | }
17 |
18 | const handleTime = store => (data = {}) => {
19 | data.time && store.dispatch(actions.setPlaytime(toPlayerTime(data.time)))
20 | return data
21 | }
22 |
23 | const eventHandler = (id, store) =>
24 | compose(
25 | handleTime(store),
26 | handleTab(store),
27 | handleAction(store),
28 | isPlayerInstance(id),
29 | get('target.dataset')
30 | )
31 |
32 | /**
33 | * External Events registry
34 | *
35 | * rel="podlove-web-player"
36 | * data-ref="web-player-id"
37 | * data-action="play|pause"
38 | * data-tab="info"
39 | * data-time="00:10:12"
40 | */
41 | window.registerExternalEvents = id => store => {
42 | const references = [...document.querySelectorAll('[rel="podlove-web-player"]')]
43 | references.forEach(ref => ref.addEventListener('click', eventHandler(id, store)))
44 |
45 | return store
46 | }
47 |
--------------------------------------------------------------------------------
/src/media/index.js:
--------------------------------------------------------------------------------
1 | import { audio, events as audioEvents, actions as audioActions } from '@podlove/html5-audio-driver'
2 | import { attatchStream } from '@podlove/html5-audio-driver/hls'
3 |
4 | export default (audioFiles) => {
5 | const audioElement = attatchStream(audio(audioFiles))
6 |
7 | return {
8 | events: audioEvents(audioElement),
9 | actions: audioActions(audioElement)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/statics/example/files/poster.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/statics/example/files/poster.jpg
--------------------------------------------------------------------------------
/src/statics/example/standalone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/statics/share.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import * as buffer from './buffer/actions'
2 | import * as chapters from './chapters/actions'
3 | import * as components from './components/actions'
4 | import * as duration from './duration/actions'
5 | import * as error from './error/actions'
6 | import * as ghost from './ghost/actions'
7 | import * as muted from './muted/actions'
8 | import * as player from './player/actions'
9 | import * as playstate from './playstate/actions'
10 | import * as playtime from './playtime/actions'
11 | import * as quantiles from './quantiles/actions'
12 | import * as rate from './rate/actions'
13 | import * as runtime from './runtime/actions'
14 | import * as share from './share/actions'
15 | import * as tabs from './tabs/actions'
16 | import * as theme from './theme/actions'
17 | import * as transcripts from './transcripts/actions'
18 | import * as volume from './volume/actions'
19 | import * as playback from './playback/actions'
20 |
21 | export default {
22 | ...buffer,
23 | ...chapters,
24 | ...components,
25 | ...duration,
26 | ...error,
27 | ...ghost,
28 | ...muted,
29 | ...player,
30 | ...playstate,
31 | ...playtime,
32 | ...quantiles,
33 | ...rate,
34 | ...runtime,
35 | ...share,
36 | ...tabs,
37 | ...theme,
38 | ...transcripts,
39 | ...volume,
40 | ...playback
41 | }
42 |
--------------------------------------------------------------------------------
/src/store/buffer/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_BUFFER } from '../types'
4 |
5 | export const setBuffer = createAction(SET_BUFFER)
6 |
--------------------------------------------------------------------------------
/src/store/buffer/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { setBuffer } from './actions'
3 |
4 | test(`setBufferAction: creates the SET_BUFFER action`, t => {
5 | t.deepEqual(setBuffer(10), {
6 | type: 'SET_BUFFER',
7 | payload: 10
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/store/buffer/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/buffer/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { SET_BUFFER } from '../types'
4 |
5 | export const INITIAL_STATE = 0
6 |
7 | export const reducer = handleActions({
8 | [SET_BUFFER]: (state, { payload }) => payload
9 | }, INITIAL_STATE)
10 |
--------------------------------------------------------------------------------
/src/store/buffer/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as buffer } from './reducer'
3 |
4 | // BUFFER TESTS
5 | test(`buffer: is a reducer function`, t => {
6 | t.is(typeof buffer, 'function')
7 | })
8 |
9 | test(`buffer: parses the buffer on SET_BUFFER`, t => {
10 | let result = buffer(undefined, {
11 | type: 'SET_BUFFER',
12 | payload: 60
13 | })
14 |
15 | t.is(result, 60)
16 | })
17 |
18 | test(`buffer: it does nothing if a unknown action is dispatched`, t => {
19 | const result = buffer(10, {
20 | type: 'NOT_A_REAL_TYPE'
21 | })
22 | t.is(result, 10)
23 | })
24 |
--------------------------------------------------------------------------------
/src/store/chapters/actions.js:
--------------------------------------------------------------------------------
1 | import { NEXT_CHAPTER, SET_NEXT_CHAPTER, PREVIOUS_CHAPTER, SET_PREVIOUS_CHAPTER, SET_CHAPTER, UPDATE_CHAPTER, INIT_CHAPTERS } from '../types'
2 |
3 | import { createAction } from 'redux-actions'
4 |
5 | export const nextChapter = createAction(NEXT_CHAPTER)
6 | export const setNextChapter = createAction(SET_NEXT_CHAPTER)
7 | export const previousChapter = createAction(PREVIOUS_CHAPTER)
8 | export const setPreviousChapter = createAction(SET_PREVIOUS_CHAPTER)
9 | export const setChapter = createAction(SET_CHAPTER)
10 | export const updateChapter = createAction(UPDATE_CHAPTER)
11 | export const initChapters = createAction(INIT_CHAPTERS)
12 |
--------------------------------------------------------------------------------
/src/store/chapters/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { setChapter, nextChapter, previousChapter, updateChapter } from './actions'
3 |
4 | test(`nextChapter: creates the NEXT_CHAPTER action`, t => {
5 | t.deepEqual(nextChapter(), {
6 | type: 'NEXT_CHAPTER'
7 | })
8 | })
9 |
10 | test(`previousChapter: creates the PREVIOUS_CHAPTER action`, t => {
11 | t.deepEqual(previousChapter(), {
12 | type: 'PREVIOUS_CHAPTER'
13 | })
14 | })
15 |
16 | test(`setChapter: creates the SET_CHAPTER action`, t => {
17 | t.deepEqual(setChapter(1), {
18 | type: 'SET_CHAPTER',
19 | payload: 1
20 | })
21 | })
22 |
23 | test(`setChapter: creates the UPDATE_CHAPTER action`, t => {
24 | t.deepEqual(updateChapter(1), {
25 | type: 'UPDATE_CHAPTER',
26 | payload: 1
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/store/chapters/index.js:
--------------------------------------------------------------------------------
1 | import * as selectors from './selectors'
2 |
3 | export * from './actions'
4 | export * from './reducer'
5 |
6 | export {
7 | selectors
8 | }
9 |
--------------------------------------------------------------------------------
/src/store/chapters/selectors.js:
--------------------------------------------------------------------------------
1 | import { get, compose } from 'lodash/fp'
2 |
3 | export const selectChapters = get('list')
4 | export const selectNextChapters = get('next')
5 | export const selectPreviousChapter = get('previous')
6 | export const selectCurrentChapter = get('current')
7 | export const selectCurrentChapterTitle = compose(get('title'), selectCurrentChapter)
8 | export const selectCurrentChapterImage = compose(get('image'), selectCurrentChapter)
9 |
--------------------------------------------------------------------------------
/src/store/components/effects.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/store/components/effects.js
--------------------------------------------------------------------------------
/src/store/components/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/display/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/display/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { get } from 'lodash'
3 |
4 | import { INIT } from '../types'
5 |
6 | export const INITIAL_STATE = 'native'
7 |
8 | export const reducer = handleActions({
9 | [INIT]: (state, { payload }) => get(payload, 'display', INITIAL_STATE)
10 | }, INITIAL_STATE)
11 |
--------------------------------------------------------------------------------
/src/store/display/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as display } from './reducer'
3 |
4 | let testAction
5 |
6 | test.beforeEach(t => {
7 | testAction = {
8 | type: 'INIT',
9 | payload: {}
10 | }
11 | })
12 |
13 | // display TESTS
14 | test(`display: it is a reducer function`, t => {
15 | t.is(typeof display, 'function')
16 | })
17 |
18 | test(`display: it extracts the display`, t => {
19 | testAction.payload.display = 'embed'
20 | const result = display('', testAction)
21 | t.is(result, 'embed')
22 | })
23 |
24 | test(`display: it returns native if a display is not available`, t => {
25 | let result = display(undefined, testAction)
26 | t.is(result, 'native')
27 | })
28 |
29 | test(`display: it does nothing if not the init action is dispatched`, t => {
30 | const result = display('foobar', {
31 | type: 'NOT_A_REAL_TYPE'
32 | })
33 | t.is(result, 'foobar')
34 | })
35 |
--------------------------------------------------------------------------------
/src/store/duration/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_DURATION } from '../types'
4 |
5 | export const setDuration = createAction(SET_DURATION)
6 |
--------------------------------------------------------------------------------
/src/store/duration/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { setDuration } from './actions'
3 |
4 | test(`setDurationAction: creates the SET_DURATION action`, t => {
5 | t.deepEqual(setDuration(10), {
6 | type: 'SET_DURATION',
7 | payload: 10
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/store/duration/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/duration/reducer.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { handleActions } from 'redux-actions'
3 |
4 | import { toPlayerTime } from 'utils/time'
5 | import { toInt } from 'utils/helper'
6 |
7 | import { INIT, SET_DURATION } from '../types'
8 |
9 | export const INITIAL_STATE = 0
10 |
11 | export const reducer = handleActions({
12 | [INIT]: (state, { payload }) => toPlayerTime(get(payload, 'duration', state)),
13 | [SET_DURATION]: (state, { payload }) => toInt(payload || state)
14 | }, INITIAL_STATE)
15 |
--------------------------------------------------------------------------------
/src/store/duration/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as duration } from './reducer'
3 |
4 | test(`duration: is a reducer function`, t => {
5 | t.is(typeof duration, 'function')
6 | })
7 |
8 | test(`duration: parses the duration on INIT`, t => {
9 | let result = duration(undefined, {
10 | type: 'INIT',
11 | payload: {
12 | duration: '01:00'
13 | }
14 | })
15 |
16 | t.is(result, 60000)
17 |
18 | result = duration(10, {
19 | type: 'INIT',
20 | payload: {}
21 | })
22 |
23 | t.is(result, 10)
24 | })
25 |
26 | test(`duration: parses duration on SET_DURATION`, t => {
27 | let result = duration(undefined, {
28 | type: 'SET_DURATION',
29 | payload: 60
30 | })
31 |
32 | t.is(result, 60)
33 | })
34 |
35 | test(`duration: sets state if duration is undefined on SET_DURATION`, t => {
36 | let result = duration(30, {
37 | type: 'SET_DURATION'
38 | })
39 |
40 | t.is(result, 30)
41 | })
42 |
43 | test(`duration: it does nothing if a unknown action is dispatched`, t => {
44 | const result = duration(10, {
45 | type: 'NOT_A_REAL_TYPE'
46 | })
47 | t.is(result, 10)
48 | })
49 |
--------------------------------------------------------------------------------
/src/store/episode/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/episode/reducer.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { handleActions } from 'redux-actions'
3 |
4 | import { parseDate } from 'utils/time'
5 | import { sanitize } from 'utils/dom'
6 |
7 | import { INIT } from '../types'
8 |
9 | export const INIT_STATE = {
10 | title: null,
11 | subtitle: null,
12 | summary: null,
13 | poster: null,
14 | link: null,
15 | publicationDate: null
16 | }
17 |
18 | export const reducer = handleActions({
19 | [INIT]: (state, { payload }) => ({
20 | ...state,
21 | title: get(payload, ['title'], null),
22 | subtitle: get(payload, ['subtitle'], null),
23 | summary: sanitize(get(payload, ['summary'], null)),
24 | link: get(payload, ['link'], null),
25 | poster: get(payload, ['poster'], null),
26 | publicationDate: parseDate(get(payload, ['publicationDate'], null))
27 | })
28 | }, INIT_STATE)
29 |
--------------------------------------------------------------------------------
/src/store/error/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { ERROR_MISSING_AUDIO_FILES } from '../types'
4 |
5 | export const errorLoad = error => ({
6 | type: error
7 | })
8 |
9 | export const errorMissingAudioFiles = createAction(ERROR_MISSING_AUDIO_FILES)
10 |
--------------------------------------------------------------------------------
/src/store/error/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { errorLoad, errorMissingAudioFiles } from './actions'
3 |
4 | test(`errorLoad: creates the NETWORK_NO_SOURCE action`, t => {
5 | t.deepEqual(errorLoad('NETWORK_NO_SOURCE'), {
6 | type: 'NETWORK_NO_SOURCE'
7 | })
8 | })
9 |
10 | test(`errorMissingAudioFiles: creates the ERROR_MISSING_AUDIO_FILES action`, t => {
11 | t.deepEqual(errorMissingAudioFiles(), {
12 | type: 'ERROR_MISSING_AUDIO_FILES'
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/store/error/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/error/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { NETWORK_EMPTY, NETWORK_NO_SOURCE, ERROR_MISSING_AUDIO_FILES } from '../types'
4 |
5 | export const INITIAL_STATE = {
6 | message: null,
7 | title: null
8 | }
9 |
10 | const loadingError = {
11 | title: 'ERROR.LOADING.TITLE',
12 | message: 'ERROR.LOADING.MESSAGE'
13 | }
14 |
15 | export const reducer = handleActions({
16 | [NETWORK_EMPTY]: () => loadingError,
17 | [NETWORK_NO_SOURCE]: () => loadingError,
18 |
19 | [ERROR_MISSING_AUDIO_FILES]: () => ({
20 | title: 'ERROR.MISSING_FILES.TITLE',
21 | message: 'ERROR.MISSING_FILES.MESSAGE'
22 | })
23 | }, INITIAL_STATE)
24 |
--------------------------------------------------------------------------------
/src/store/error/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as error } from './reducer'
3 |
4 | test(`error: it exports a reducer function`, t => {
5 | t.truthy(typeof error === 'function')
6 | })
7 |
8 | test(`error: it returns the initial state on default`, t => {
9 | const result = error(undefined, {
10 | action: 'FOO'
11 | })
12 |
13 | t.deepEqual(result, {
14 | message: null,
15 | title: null
16 | })
17 | })
18 |
19 | const types = ['NETWORK_EMPTY', 'NETWORK_NO_SOURCE']
20 |
21 | types.forEach(type => {
22 | test(`error: it sets the message and title on ${type}`, t => {
23 | const result = error(undefined, { type })
24 |
25 | t.deepEqual(result, {
26 | title: 'ERROR.LOADING.TITLE',
27 | message: 'ERROR.LOADING.MESSAGE'
28 | })
29 | })
30 | })
31 |
32 | test(`error: it sets the message and title on ERROR_MISSING_AUDIO_FILES`, t => {
33 | const result = error(undefined, {
34 | type: 'ERROR_MISSING_AUDIO_FILES'
35 | })
36 |
37 | t.deepEqual(result, {
38 | title: 'ERROR.MISSING_FILES.TITLE',
39 | message: 'ERROR.MISSING_FILES.MESSAGE'
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/store/files/index.js:
--------------------------------------------------------------------------------
1 | import * as selectors from './selectors'
2 |
3 | export * from './reducer'
4 |
5 | export {
6 | selectors
7 | }
8 |
--------------------------------------------------------------------------------
/src/store/files/reducer.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { handleActions } from 'redux-actions'
3 |
4 | import { INIT } from '../types'
5 |
6 | export const INITIAL_STATE = {
7 | audio: []
8 | }
9 |
10 | export const reducer = handleActions({
11 | [INIT]: (_, { payload }) => ({
12 | audio: get(payload, 'audio', [])
13 | })
14 |
15 | }, INITIAL_STATE)
16 |
--------------------------------------------------------------------------------
/src/store/files/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as files } from './reducer'
3 |
4 | let testAction
5 |
6 | test.beforeEach(t => {
7 | testAction = {
8 | type: 'INIT',
9 | payload: {
10 | audio: [{
11 | url: 'http://foo.bar'
12 | }, {
13 | url: 'http://foo.baz'
14 | }]
15 | }
16 | }
17 | })
18 |
19 | test(`files: it is a reducer function`, t => {
20 | t.is(typeof files, 'function')
21 | })
22 |
23 | test(`files: it extracts the audio meta information`, t => {
24 | const result = files({}, testAction)
25 |
26 | t.deepEqual(result, {
27 | audio: [{
28 | url: 'http://foo.bar'
29 | }, {
30 | url: 'http://foo.baz'
31 | }]
32 | })
33 | })
34 |
35 | test(`files: it does nothing if not a registered action is dispatched`, t => {
36 | const result = files('foobar', {
37 | type: 'NOT_A_REAL_TYPE'
38 | })
39 | t.is(result, 'foobar')
40 | })
41 |
--------------------------------------------------------------------------------
/src/store/files/selectors.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash/fp'
2 |
3 | export const selectAudio = get('audio')
4 |
--------------------------------------------------------------------------------
/src/store/ghost/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SIMULATE_PLAYTIME, ENABLE_GHOST_MODE, DISABLE_GHOST_MODE } from '../types'
4 |
5 | export const simulatePlaytime = createAction(SIMULATE_PLAYTIME)
6 | export const enableGhostMode = createAction(ENABLE_GHOST_MODE)
7 | export const disableGhostMode = createAction(DISABLE_GHOST_MODE)
8 |
--------------------------------------------------------------------------------
/src/store/ghost/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { simulatePlaytime, enableGhostMode, disableGhostMode } from './actions'
3 |
4 | test(`simulatePlaytime: creates the SIMULATE_PLAYTIME action`, t => {
5 | t.deepEqual(simulatePlaytime(100), {
6 | type: 'SIMULATE_PLAYTIME',
7 | payload: 100
8 | })
9 | })
10 |
11 | test(`enableGhostMode: creates the ENABLE_GHOST_MODE action`, t => {
12 | t.deepEqual(enableGhostMode(), {
13 | type: 'ENABLE_GHOST_MODE'
14 | })
15 | })
16 |
17 | test(`disableGhostMode: creates the DISABLE_GHOST_MODE action`, t => {
18 | t.deepEqual(disableGhostMode(), {
19 | type: 'DISABLE_GHOST_MODE'
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/store/ghost/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/ghost/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { toInt } from 'utils/helper'
3 |
4 | import { SIMULATE_PLAYTIME, ENABLE_GHOST_MODE, DISABLE_GHOST_MODE } from '../types'
5 |
6 | export const INITIAL_STATE = {
7 | time: 0,
8 | active: false
9 | }
10 |
11 | export const reducer = handleActions({
12 | [SIMULATE_PLAYTIME]: (state, { payload }) => ({
13 | ...state,
14 | time: toInt(payload)
15 | }),
16 |
17 | [ENABLE_GHOST_MODE]: (state) => ({
18 | ...state,
19 | active: true
20 | }),
21 |
22 | [DISABLE_GHOST_MODE]: (state) => ({
23 | ...state,
24 | active: false
25 | })
26 | }, INITIAL_STATE)
27 |
--------------------------------------------------------------------------------
/src/store/ghost/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as ghost } from './reducer'
3 |
4 | test(`ghost: it exports a reducer function`, t => {
5 | t.truthy(typeof ghost === 'function')
6 | })
7 |
8 | test(`ghost: it returns the initial state on default`, t => {
9 | const result = ghost(undefined, {
10 | type: 'FOO'
11 | })
12 |
13 | t.deepEqual(result, {
14 | time: 0,
15 | active: false
16 | })
17 | })
18 |
19 | test(`ghost: it sets the ghost time on SIMULATE_PLAYTIME`, t => {
20 | const result = ghost(undefined, {
21 | type: 'SIMULATE_PLAYTIME',
22 | payload: 100
23 | })
24 |
25 | t.deepEqual(result, {
26 | time: 100,
27 | active: false
28 | })
29 | })
30 |
31 | test(`ghost: it activates the ghost mode on ENABLE_GHOST_MODE`, t => {
32 | const result = ghost(undefined, {
33 | type: 'ENABLE_GHOST_MODE'
34 | })
35 |
36 | t.deepEqual(result, {
37 | time: 0,
38 | active: true
39 | })
40 | })
41 |
42 | test(`ghost: it disables the ghost mode on DISABLE_GHOST_MODE`, t => {
43 | const result = ghost(undefined, {
44 | type: 'DISABLE_GHOST_MODE'
45 | })
46 |
47 | t.deepEqual(result, {
48 | time: 0,
49 | active: false
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { createStore, applyMiddleware, compose } from 'redux'
3 | import { connect } from 'redux-vuex'
4 |
5 | import effects from '../effects'
6 | import reducers from './reducers'
7 | import actions from './actions'
8 |
9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
10 |
11 | const store = createStore(reducers, composeEnhancers(applyMiddleware(effects)))
12 |
13 | connect({ Vue, store, actions })
14 |
15 | export default store
16 |
--------------------------------------------------------------------------------
/src/store/last-action/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/last-action/reducer.js:
--------------------------------------------------------------------------------
1 | export const INITIAL_STATE = null
2 | export const reducer = (state = null, action) => action
3 |
--------------------------------------------------------------------------------
/src/store/last-action/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 |
3 | import { reducer as lastAction } from './reducer'
4 |
5 | test(`lastAction: returns the last action`, t => {
6 | t.is(lastAction('foo', 'bar'), 'bar')
7 | })
8 |
9 | test(`lastAction: returns the last action if state is undefined`, t => {
10 | t.is(lastAction(undefined, 'bar'), 'bar')
11 | })
12 |
--------------------------------------------------------------------------------
/src/store/mode/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/mode/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { INIT } from '../types'
4 |
5 | export const INITIAL_STATE = 'episode'
6 |
7 | export const reducer = handleActions({
8 | [INIT]: (state, { payload }) => payload.mode === 'live' ? 'live' : INITIAL_STATE
9 | }, INITIAL_STATE)
10 |
--------------------------------------------------------------------------------
/src/store/mode/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as mode } from './reducer'
3 |
4 | test(`mode: it falls back to default mode if not live`, t => {
5 | const result = mode(undefined, {
6 | type: 'INIT',
7 | payload: {
8 | mode: 'foobar'
9 | }
10 | })
11 | t.deepEqual(result, 'episode')
12 | })
13 |
14 | test(`mode: it sets the provided mode to live`, t => {
15 | const result = mode(undefined, {
16 | type: 'INIT',
17 | payload: {
18 | mode: 'live'
19 | }
20 | })
21 | t.deepEqual(result, 'live')
22 | })
23 |
24 | test(`mode: it does nothing if not the init action is dispatched`, t => {
25 | const result = mode('foo', {
26 | type: 'NOT_A_REAL_TYPE'
27 | })
28 | t.deepEqual(result, 'foo')
29 | })
30 |
31 | test(`mode: it has a default fallback if a missing state is provided`, t => {
32 | const result = mode(undefined, {
33 | type: 'NOT_A_REAL_TYPE'
34 | })
35 | t.deepEqual(result, 'episode')
36 | })
37 |
--------------------------------------------------------------------------------
/src/store/muted/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { MUTE, UNMUTE } from '../types'
4 |
5 | export const mute = createAction(MUTE)
6 | export const unmute = createAction(UNMUTE)
7 |
--------------------------------------------------------------------------------
/src/store/muted/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { mute, unmute } from './actions'
3 |
4 | test(`muteAction: creates the MUTE action`, t => {
5 | t.deepEqual(mute(), {
6 | type: 'MUTE'
7 | })
8 | })
9 |
10 | test(`unmuteAction: creates the UNMUTE action`, t => {
11 | t.deepEqual(unmute(), {
12 | type: 'UNMUTE'
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/store/muted/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/muted/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { MUTE, UNMUTE } from '../types'
4 |
5 | export const INITIAL_STATE = false
6 |
7 | export const reducer = handleActions({
8 | [MUTE]: () => true,
9 | [UNMUTE]: () => false
10 | }, INITIAL_STATE)
11 |
--------------------------------------------------------------------------------
/src/store/muted/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 |
3 | import { reducer as muted } from './reducer'
4 |
5 | // MUTED
6 | test(`muted: is a reducer function`, t => {
7 | t.is(typeof muted, 'function')
8 | })
9 |
10 | test(`muted: it does nothing if a unknown action is dispatched`, t => {
11 | const result = muted('CUSTOM', {
12 | type: 'NOT_A_REAL_TYPE'
13 | })
14 | t.is(result, 'CUSTOM')
15 | })
16 |
17 | test(`muted: it returns the correct rate`, t => {
18 | t.is(muted(undefined, {
19 | type: 'MUTE'
20 | }), true)
21 |
22 | t.is(muted(undefined, {
23 | type: 'UNMUTE'
24 | }), false)
25 | })
26 |
--------------------------------------------------------------------------------
/src/store/playback/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_PLAYBACK_PARAMS } from '../types'
4 |
5 | export const setUrlParams = createAction(SET_PLAYBACK_PARAMS)
6 |
--------------------------------------------------------------------------------
/src/store/playback/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/playback/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { SET_PLAYBACK_PARAMS } from '../types'
4 |
5 | export const INITIAL_STATE = {
6 | starttime: null,
7 | endtime: null,
8 | autoplay: false
9 | }
10 |
11 | export const reducer = handleActions({
12 | [SET_PLAYBACK_PARAMS]: (state, { payload }) => payload
13 | }, INITIAL_STATE)
14 |
--------------------------------------------------------------------------------
/src/store/player/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { INIT, LOAD, LOADING, LOADED, IDLE, PLAY, PAUSE, END } from '../types'
4 |
5 | export const init = createAction(INIT)
6 |
7 | export const idle = createAction(IDLE)
8 | export const load = createAction(LOAD)
9 | export const loading = createAction(LOADING)
10 | export const loaded = createAction(LOADED)
11 |
12 | export const playEvent = createAction(PLAY)
13 | export const pauseEvent = createAction(PAUSE)
14 | export const endEvent = createAction(END)
15 |
--------------------------------------------------------------------------------
/src/store/player/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 |
3 | import { init, idle, load, loading, loaded, playEvent, pauseEvent, endEvent } from './actions'
4 |
5 | test(`init: creates the INIT action`, t => {
6 | t.deepEqual(init({ foo: 'bar' }), {
7 | type: 'INIT',
8 | payload: { foo: 'bar' }
9 | })
10 | })
11 |
12 | test(`playEventAction: creates the PLAY action`, t => {
13 | t.deepEqual(playEvent(), {
14 | type: 'PLAY'
15 | })
16 | })
17 |
18 | test(`pauseEventAction: creates the PAUSE action`, t => {
19 | t.deepEqual(pauseEvent(), {
20 | type: 'PAUSE'
21 | })
22 | })
23 |
24 | test(`endEvent: creates the STOP action`, t => {
25 | t.deepEqual(endEvent(), {
26 | type: 'END'
27 | })
28 | })
29 |
30 | test(`idleAction: creates the IDLE action`, t => {
31 | t.deepEqual(idle(), {
32 | type: 'IDLE'
33 | })
34 | })
35 |
36 | test(`loadingAction: creates the LOADING action`, t => {
37 | t.deepEqual(loading('foo'), {
38 | type: 'LOADING',
39 | payload: 'foo'
40 | })
41 | })
42 |
43 | test(`loadAction: creates the LOAD action`, t => {
44 | t.deepEqual(load(), {
45 | type: 'LOAD'
46 | })
47 | })
48 |
49 | test(`loadedAction: creates the LOAD action`, t => {
50 | t.deepEqual(loaded('foo'), {
51 | type: 'LOADED',
52 | payload: 'foo'
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/src/store/playstate/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { IDLE, PLAY, PAUSE, END, STOP, UI_PLAY, UI_PAUSE, UI_RESTART } from '../types'
4 |
5 | export const idle = createAction(IDLE)
6 | export const playEvent = createAction(PLAY)
7 | export const pauseEvent = createAction(PAUSE)
8 | export const endEvent = createAction(END)
9 | export const play = createAction(UI_PLAY)
10 | export const pause = createAction(UI_PAUSE)
11 | export const restart = createAction(UI_RESTART)
12 | export const stop = createAction(STOP)
13 |
--------------------------------------------------------------------------------
/src/store/playstate/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 |
3 | import { play, playEvent, pause, pauseEvent, endEvent, restart, idle } from './actions'
4 |
5 | test(`playAction: creates the UI_PLAY action`, t => {
6 | t.deepEqual(play(), {
7 | type: 'UI_PLAY'
8 | })
9 | })
10 |
11 | test(`playEventAction: creates the PLAY action`, t => {
12 | t.deepEqual(playEvent(), {
13 | type: 'PLAY'
14 | })
15 | })
16 |
17 | test(`pauseAction: creates the UI_PAUSE action`, t => {
18 | t.deepEqual(pause(), {
19 | type: 'UI_PAUSE'
20 | })
21 | })
22 |
23 | test(`pauseEventAction: creates the PAUSE action`, t => {
24 | t.deepEqual(pauseEvent(), {
25 | type: 'PAUSE'
26 | })
27 | })
28 |
29 | test(`endEvent: creates the STOP action`, t => {
30 | t.deepEqual(endEvent(), {
31 | type: 'END'
32 | })
33 | })
34 |
35 | test(`restartAction: creates the RESTART action`, t => {
36 | t.deepEqual(restart(), {
37 | type: 'UI_RESTART'
38 | })
39 | })
40 |
41 | test(`idleAction: creates the IDLE action`, t => {
42 | t.deepEqual(idle(), {
43 | type: 'IDLE'
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/store/playstate/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/playstate/reducer.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { handleActions } from 'redux-actions'
3 |
4 | import { INIT, UPDATE_PLAYTIME, PLAY, PAUSE, STOP, IDLE, LOADING, ERROR_LOAD } from '../types'
5 |
6 | export const INITIAL_STATE = 'start'
7 |
8 | export const reducer = handleActions({
9 | [INIT]: (state, { payload }) => get(payload, 'playstate', state),
10 | [UPDATE_PLAYTIME]: (state) => state === 'end' ? 'pause' : state,
11 | [PLAY]: () => 'playing',
12 | [PAUSE]: () => 'pause',
13 | [STOP]: () => 'end',
14 | [IDLE]: () => 'idle',
15 | [LOADING]: () => 'loading',
16 | [ERROR_LOAD]: () => 'error'
17 | }, INITIAL_STATE)
18 |
--------------------------------------------------------------------------------
/src/store/playtime/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_PLAYTIME, UPDATE_PLAYTIME } from '../types'
4 |
5 | export const setPlaytime = createAction(SET_PLAYTIME)
6 | export const updatePlaytime = createAction(UPDATE_PLAYTIME)
7 |
--------------------------------------------------------------------------------
/src/store/playtime/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { setPlaytime, updatePlaytime } from './actions'
3 |
4 | test(`setPlaytimeAction: creates the SET_PLAYTIME action`, t => {
5 | t.deepEqual(setPlaytime(10), {
6 | type: 'SET_PLAYTIME',
7 | payload: 10
8 | })
9 | })
10 |
11 | test(`updatePlaytimeAction: creates the UPDATE_PLAYTIME action`, t => {
12 | t.deepEqual(updatePlaytime(10), {
13 | type: 'UPDATE_PLAYTIME',
14 | payload: 10
15 | })
16 | })
17 |
18 | test(`updatePlaytimeAction: creates the UPDATE_PLAYTIME action`, t => {
19 | t.deepEqual(updatePlaytime(10), {
20 | type: 'UPDATE_PLAYTIME',
21 | payload: 10
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/src/store/playtime/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/playtime/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { get } from 'lodash'
4 | import { toPlayerTime } from 'utils/time'
5 | import { toInt } from 'utils/helper'
6 |
7 | import { INIT, UPDATE_PLAYTIME, SET_PLAYTIME } from '../types'
8 |
9 | export const INITIAL_STATE = 0
10 |
11 | export const reducer = handleActions({
12 | [INIT]: (state, { payload }) => toPlayerTime(get(payload, 'playtime', state)),
13 | [UPDATE_PLAYTIME]: (state, { payload }) => toInt(payload),
14 | [SET_PLAYTIME]: (state, { payload }) => toInt(payload)
15 | }, INITIAL_STATE)
16 |
--------------------------------------------------------------------------------
/src/store/playtime/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as playtime } from './reducer'
3 |
4 | // PLAYTIME TESTS
5 | test(`playtime: is a reducer function`, t => {
6 | t.is(typeof playtime, 'function')
7 | })
8 |
9 | test(`playtime: parses the playtime on INIT`, t => {
10 | let result = playtime(undefined, {
11 | type: 'INIT',
12 | payload: {
13 | playtime: '01:00'
14 | }
15 | })
16 |
17 | t.is(result, 60000)
18 | })
19 |
20 | test(`playtime: parses playtime on UPDATE_PLAYTIME`, t => {
21 | let result = playtime(undefined, {
22 | type: 'UPDATE_PLAYTIME',
23 | payload: '60'
24 | })
25 |
26 | t.is(result, 60)
27 | })
28 |
29 | test(`playtime: parses playtime on SET_PLAYTIME`, t => {
30 | let result = playtime(undefined, {
31 | type: 'SET_PLAYTIME',
32 | payload: 60
33 | })
34 |
35 | t.is(result, 60)
36 | })
37 |
38 | test(`playtime: it does nothing if a unknown action is dispatched`, t => {
39 | const result = playtime(10, {
40 | type: 'NOT_A_REAL_TYPE'
41 | })
42 | t.is(result, 10)
43 | })
44 |
--------------------------------------------------------------------------------
/src/store/quantiles/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { LOAD_QUANTILES, SET_QUANTILE } from '../types'
4 |
5 | export const loadQuantiles = createAction(LOAD_QUANTILES, (quantiles = []) => quantiles)
6 | export const setQuantile = createAction(SET_QUANTILE, (start, end) => ({ start, end }))
7 |
--------------------------------------------------------------------------------
/src/store/quantiles/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { loadQuantiles, setQuantile } from './actions'
3 |
4 | test(`loadQuantiles: creates the LOAD_QUANTILES action`, t => {
5 | t.deepEqual(loadQuantiles([[0, 20]]), {
6 | type: 'LOAD_QUANTILES',
7 | payload: [[0, 20]]
8 | })
9 |
10 | t.deepEqual(loadQuantiles(), {
11 | type: 'LOAD_QUANTILES',
12 | payload: []
13 | })
14 | })
15 |
16 | test(`setQuantile: creates the SET_QUANTILE action`, t => {
17 | t.deepEqual(setQuantile(10, 20), {
18 | type: 'SET_QUANTILE',
19 | payload: {
20 | start: 10,
21 | end: 20
22 | }
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/store/quantiles/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/quantiles/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { head, findIndex } from 'lodash'
3 |
4 | import { LOAD_QUANTILES, SET_QUANTILE } from '../types'
5 |
6 | const findQuantile = (quantiles = [], start) =>
7 | findIndex(quantiles, quantile => head(quantile) === start)
8 |
9 | const newQuantile = (quantiles = [], quantile) =>
10 | [...quantiles, quantile]
11 |
12 | const updateQuantile = (quantiles = [], index, quantile) => [
13 | ...quantiles.slice(0, index),
14 | quantile,
15 | ...quantiles.slice(index + 1)
16 | ]
17 |
18 | export const INITIAL_STATE = []
19 |
20 | export const reducer = handleActions({
21 | [LOAD_QUANTILES]: (state, { payload }) => payload,
22 | [SET_QUANTILE]: (state, { payload }) => {
23 | const index = findQuantile(state, payload.start)
24 | const currentQuantile = [payload.start, payload.end]
25 |
26 | if (index < 0) {
27 | return newQuantile(state, currentQuantile)
28 | }
29 |
30 | return updateQuantile(state, index, currentQuantile)
31 | }
32 | }, INITIAL_STATE)
33 |
--------------------------------------------------------------------------------
/src/store/quantiles/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as quantiles } from './reducer'
3 |
4 | // QUANTILES TESTS
5 | test(`quantiles: is a reducer function`, t => {
6 | t.is(typeof quantiles, 'function')
7 | })
8 |
9 | test(`quantiles: it loads all quantiles`, t => {
10 | const testAction = {
11 | type: 'LOAD_QUANTILES',
12 | payload: [[0, 20]]
13 | }
14 |
15 | t.deepEqual(quantiles([], testAction), [[0, 20]])
16 | })
17 |
18 | test(`quantiles: it adds a new quantile if currently not exists`, t => {
19 | const testAction = {
20 | type: 'SET_QUANTILE',
21 | payload: {
22 | start: 0,
23 | end: 50
24 | }
25 | }
26 |
27 | t.deepEqual(quantiles([], testAction), [[0, 50]])
28 | })
29 |
30 | test(`quantiles: it updates an existing quantile if it's exists`, t => {
31 | const testAction = {
32 | type: 'SET_QUANTILE',
33 | payload: {
34 | start: 0,
35 | end: 60
36 | }
37 | }
38 |
39 | t.deepEqual(quantiles([[0, 20]], testAction), [[0, 60]])
40 | })
41 |
42 | test(`quantiles: it returns the state if not a matching action is called`, t => {
43 | const testAction = {
44 | type: 'INVALID_ACTION'
45 | }
46 |
47 | t.deepEqual(quantiles([[0, 20]], testAction), [[0, 20]])
48 | })
49 |
--------------------------------------------------------------------------------
/src/store/rate/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_RATE } from '../types'
4 |
5 | export const setRate = createAction(SET_RATE)
6 |
--------------------------------------------------------------------------------
/src/store/rate/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 |
3 | import { setRate } from './actions'
4 |
5 | test(`rateAction: creates the SET_RATE action`, t => {
6 | t.deepEqual(setRate(1), {
7 | type: 'SET_RATE',
8 | payload: 1
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/src/store/rate/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/rate/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { compose } from 'lodash/fp'
3 |
4 | import { toFloat } from 'utils/helper'
5 | import { inRange } from 'utils/math'
6 |
7 | import { SET_RATE } from '../types'
8 |
9 | const inRateRange = compose(inRange(0.5, 4), toFloat)
10 |
11 | export const INITIAL_STATE = 1
12 |
13 | export const reducer = handleActions({
14 | [SET_RATE]: (state, { payload }) => inRateRange(payload)
15 | }, INITIAL_STATE)
16 |
--------------------------------------------------------------------------------
/src/store/rate/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as rate } from './reducer'
3 |
4 | // RATE
5 | test(`rate: is a reducer function`, t => {
6 | t.is(typeof rate, 'function')
7 | })
8 |
9 | test(`rate: it does nothing if a unknown action is dispatched`, t => {
10 | const result = rate('CUSTOM', {
11 | type: 'NOT_A_REAL_TYPE'
12 | })
13 | t.is(result, 'CUSTOM')
14 | })
15 |
16 | test(`rate: it returns the correct rate`, t => {
17 | t.is(rate(undefined, {
18 | type: 'SET_RATE',
19 | payload: 1
20 | }), 1)
21 |
22 | t.is(rate(1, {
23 | type: 'SET_RATE',
24 | payload: 0.2
25 | }), 0.5)
26 |
27 | t.is(rate(1, {
28 | type: 'SET_RATE',
29 | payload: 5
30 | }), 4)
31 | })
32 |
--------------------------------------------------------------------------------
/src/store/reference/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/reference/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { get } from 'lodash'
3 |
4 | import { INIT } from '../types'
5 |
6 | export const INITIAL_STATE = {}
7 |
8 | export const reducer = handleActions({
9 | [INIT]: (state, { payload }) => ({
10 | ...state,
11 | config: get(payload, ['reference', 'config'], null),
12 | share: get(payload, ['reference', 'share'], null),
13 | origin: get(payload, ['reference', 'origin'], null)
14 | })
15 | }, INITIAL_STATE)
16 |
--------------------------------------------------------------------------------
/src/store/runtime/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_LANGUAGE, SET_RUNTIME } from '../types'
4 |
5 | export const setLanguage = createAction(SET_LANGUAGE)
6 | export const setRuntime = createAction(SET_RUNTIME)
7 |
--------------------------------------------------------------------------------
/src/store/runtime/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { setLanguage, setRuntime } from './actions'
3 |
4 | test(`setLanguage: creates the SET_LANGUAGE action`, t => {
5 | t.deepEqual(setLanguage('de'), {
6 | type: 'SET_LANGUAGE',
7 | payload: 'de'
8 | })
9 | })
10 |
11 | test(`setLanguage: creates the SET_LANGUAGE action`, t => {
12 | t.deepEqual(setRuntime({ language: 'de' }), {
13 | type: 'SET_RUNTIME',
14 | payload: { language: 'de' }
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/store/runtime/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/runtime/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { SET_RUNTIME, SET_LANGUAGE } from '../types'
4 |
5 | export const INITIAL_STATE = {}
6 |
7 | export const reducer = handleActions({
8 | [SET_RUNTIME]: (state, { payload }) => ({
9 | ...state,
10 | ...payload
11 | }),
12 | [SET_LANGUAGE]: (state, { payload }) => ({
13 | ...state,
14 | language: payload
15 | })
16 | }, INITIAL_STATE)
17 |
--------------------------------------------------------------------------------
/src/store/runtime/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as runtime } from './reducer'
3 |
4 | let testAction
5 |
6 | test.beforeEach(t => {
7 | testAction = {
8 | type: 'SET_RUNTIME',
9 | payload: {
10 | language: 'en',
11 | platform: 'desktop'
12 | }
13 | }
14 | })
15 |
16 | test(`runtime: it is a reducer function`, t => {
17 | t.is(typeof runtime, 'function')
18 | })
19 |
20 | test(`runtime: it extracts the runtime on SET_RUNTIME`, t => {
21 | const result = runtime('', testAction)
22 | t.deepEqual(result, {
23 | language: 'en',
24 | platform: 'desktop'
25 | })
26 | })
27 |
28 | test(`runtime: it returns an empty object if a runtime is not available`, t => {
29 | let result = runtime(undefined, {
30 | type: 'NOT_A_REAL_TYPE'
31 | })
32 | t.deepEqual(result, {})
33 | })
34 |
35 | test(`runtime: it sets the language in SET_LANGUAGE`, t => {
36 | let result = runtime({ platform: 'mobile', language: 'en' }, { type: 'SET_LANGUAGE', payload: 'de' })
37 |
38 | t.deepEqual(result, { platform: 'mobile', language: 'de' })
39 | })
40 |
41 | test(`runtime: it does nothing if not the init action is dispatched`, t => {
42 | const result = runtime('foobar', {
43 | type: 'NOT_A_REAL_TYPE'
44 | })
45 | t.is(result, 'foobar')
46 | })
47 |
--------------------------------------------------------------------------------
/src/store/selectors.js:
--------------------------------------------------------------------------------
1 | import { compose, get } from 'lodash/fp'
2 |
3 | import { selectors as chapters } from './chapters'
4 | import { selectors as share } from './share'
5 | import { selectors as files } from './files'
6 |
7 | // Chapters Tab
8 | const chaptersSlice = get('chapters')
9 | export const selectChapters = compose(chapters.selectChapters, chaptersSlice)
10 | export const selectNextChapters = compose(chapters.selectNextChapters, chaptersSlice)
11 | export const selectPreviousChapter = compose(chapters.selectPreviousChapter, chaptersSlice)
12 | export const selectCurrentChapter = compose(chapters.selectCurrentChapter, chaptersSlice)
13 | export const selectCurrentChapterTitle = compose(chapters.selectCurrentChapterTitle, chaptersSlice)
14 | export const selectCurrentChapterImage = compose(chapters.selectCurrentChapterImage, chaptersSlice)
15 |
16 | // Share Tab
17 | const shareSlice = get('share')
18 | export const selectShareContent = compose(share.selectShareContent, shareSlice)
19 |
20 | // Files Tab
21 | const filesSlice = get('files')
22 | export const selectAudioFiles = compose(files.selectAudio, filesSlice)
23 |
--------------------------------------------------------------------------------
/src/store/share/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_SHARE_CONTENT, SET_SHARE_EMBED_SIZE } from '../types'
4 |
5 | export const setShareContent = createAction(SET_SHARE_CONTENT)
6 | export const setShareEmbedSize = createAction(SET_SHARE_EMBED_SIZE)
7 |
--------------------------------------------------------------------------------
/src/store/share/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import {
3 | setShareContent,
4 | setShareEmbedSize
5 | } from './actions'
6 |
7 | test(`setShareContentAction: creates the SET_SHARE_CONTENT action`, t => {
8 | t.deepEqual(setShareContent('episode'), {
9 | type: 'SET_SHARE_CONTENT',
10 | payload: 'episode'
11 | })
12 | })
13 |
14 | test(`setShareEmbedSizeAction: creates the SET_SHARE_EMBED_SIZE action`, t => {
15 | t.deepEqual(setShareEmbedSize('250x400'), {
16 | type: 'SET_SHARE_EMBED_SIZE',
17 | payload: '250x400'
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/store/share/index.js:
--------------------------------------------------------------------------------
1 | import * as selectors from './selectors'
2 |
3 | export * from './actions'
4 | export * from './reducer'
5 |
6 | export {
7 | selectors
8 | }
9 |
--------------------------------------------------------------------------------
/src/store/share/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 |
3 | import { SET_SHARE_CONTENT, SET_SHARE_EMBED_SIZE } from '../types'
4 |
5 | export const INITIAL_STATE = {
6 | content: 'episode',
7 | embed: {
8 | available: ['250x400', '320x400', '375x400', '600x290', '768x290'],
9 | size: '320x400'
10 | }
11 | }
12 |
13 | export const reducer = handleActions({
14 | [SET_SHARE_CONTENT]: (state, { payload }) => ({
15 | ...state,
16 | content: payload
17 | }),
18 |
19 | [SET_SHARE_EMBED_SIZE]: (state, { payload }) => ({
20 | ...state,
21 | embed: {
22 | ...state.embed,
23 | size: payload
24 | }
25 | })
26 | }, INITIAL_STATE)
27 |
--------------------------------------------------------------------------------
/src/store/share/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as share } from './reducer'
3 |
4 | let expected
5 |
6 | test.beforeEach(t => {
7 | expected = {
8 | content: 'episode',
9 | embed: {
10 | available: ['250x400', '320x400', '375x400', '600x290', '768x290'],
11 | size: '320x400'
12 | }
13 | }
14 | })
15 |
16 | test(`share: is a reducer function`, t => {
17 | t.is(typeof share, 'function')
18 | })
19 |
20 | test(`share: it returns the state on default`, t => {
21 | let result = share(undefined, {
22 | type: 'FOO_BAR'
23 | })
24 |
25 | t.deepEqual(result, expected)
26 | })
27 |
28 | test(`share: it sets the share content on SET_SHARE_CONTENT`, t => {
29 | let result = share(undefined, {
30 | type: 'SET_SHARE_CONTENT',
31 | payload: 'episode'
32 | })
33 |
34 | expected.content = 'episode'
35 | t.deepEqual(result, expected)
36 | })
37 |
38 | test(`share: it sets the embed size on SET_SHARE_EMBED_SIZE`, t => {
39 | let result = share(undefined, {
40 | type: 'SET_SHARE_EMBED_SIZE',
41 | payload: '250x400'
42 | })
43 |
44 | expected.embed.size = '250x400'
45 | t.deepEqual(result, expected)
46 | })
47 |
--------------------------------------------------------------------------------
/src/store/share/selectors.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash/fp'
2 |
3 | export const selectShareEmbed = get('embed')
4 | export const selectShareContent = get('content')
5 |
--------------------------------------------------------------------------------
/src/store/show/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/show/reducer.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { handleActions } from 'redux-actions'
3 | import { sanitize } from 'utils/dom'
4 |
5 | import { INIT } from '../types'
6 |
7 | export const INITIAL_STATE = {
8 | title: null,
9 | subtitle: null,
10 | summary: null,
11 | poster: null,
12 | link: null
13 | }
14 |
15 | export const reducer = handleActions({
16 | [INIT]: (state, { payload }) => ({
17 | ...state,
18 | title: get(payload, ['show', 'title'], null),
19 | subtitle: get(payload, ['show', 'subtitle'], null),
20 | summary: sanitize(get(payload, ['show', 'summary'], null)),
21 | link: get(payload, ['show', 'link'], null),
22 | poster: get(payload, ['show', 'poster'], null)
23 | })
24 | }, INITIAL_STATE)
25 |
--------------------------------------------------------------------------------
/src/store/speakers/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/speakers/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { get } from 'lodash'
3 |
4 | import { INIT } from '../types'
5 |
6 | export const INITIAL_STATE = []
7 |
8 | export const reducer = handleActions({
9 | [INIT]: (state, { payload }) => get(payload, 'contributors', [])
10 | }, INITIAL_STATE)
11 |
--------------------------------------------------------------------------------
/src/store/speakers/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as speakers } from './reducer'
3 |
4 | let testAction
5 |
6 | test.beforeEach(t => {
7 | testAction = {
8 | type: 'INIT',
9 | payload: {
10 | contributors: [{
11 | name: 'foo',
12 | group: { slug: 'onair' }
13 | }, {
14 | name: 'bar',
15 | group: { slug: 'team' }
16 | }]
17 | }
18 | }
19 | })
20 |
21 | test(`speakers: it is a reducer function`, t => {
22 | t.is(typeof speakers, 'function')
23 | })
24 |
25 | test(`speakers: it sets the onair speakers on INIT`, t => {
26 | const result = speakers(undefined, testAction)
27 |
28 | t.deepEqual(result, testAction.payload.contributors)
29 | })
30 |
31 | test(`speakers: it does nothing if not a registered action is dispatched`, t => {
32 | const result = speakers('foobar', {
33 | type: 'NOT_A_REAL_TYPE'
34 | })
35 | t.is(result, 'foobar')
36 | })
37 |
--------------------------------------------------------------------------------
/src/store/tabs/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { TOGGLE_TAB, SET_TABS } from '../types'
4 |
5 | export const toggleTab = createAction(TOGGLE_TAB)
6 | export const setTabs = createAction(SET_TABS)
7 |
--------------------------------------------------------------------------------
/src/store/tabs/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { toggleTab, setTabs } from './actions'
3 |
4 | test(`toggleTab: creates the TOGGLE_TAB action`, t => {
5 | t.deepEqual(toggleTab('settings'), {
6 | type: 'TOGGLE_TAB',
7 | payload: 'settings'
8 | })
9 | })
10 |
11 | test(`setTabs: creates the SET_TABS action`, t => {
12 | t.deepEqual(setTabs({ settings: false }), {
13 | type: 'SET_TABS',
14 | payload: { settings: false }
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/store/tabs/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/tabs/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { get } from 'lodash'
3 |
4 | import { INIT, TOGGLE_TAB, SET_TABS } from '../types'
5 |
6 | export const INITIAL_STATE = {
7 | chapters: false,
8 | audio: false,
9 | share: false,
10 | files: false,
11 | info: false,
12 | transcripts: false
13 | }
14 |
15 | export const reducer = handleActions({
16 | [INIT]: (state, { payload }) => ({
17 | ...INITIAL_STATE,
18 | ...get(payload, 'tabs', null)
19 | }),
20 |
21 | [TOGGLE_TAB]: (state, { payload }) => ({
22 | ...INITIAL_STATE,
23 | [payload]: !get(state, payload, false)
24 | }),
25 |
26 | [SET_TABS]: (state, { payload }) => payload
27 | }, INITIAL_STATE)
28 |
--------------------------------------------------------------------------------
/src/store/theme/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_THEME } from '../types'
4 |
5 | export const setTheme = createAction(SET_THEME)
6 |
--------------------------------------------------------------------------------
/src/store/theme/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { setTheme } from './actions'
3 |
4 | test(`setThemeAction: creates the SET_THEME action`, t => {
5 | t.deepEqual(setTheme('theme'), {
6 | type: 'SET_THEME',
7 | payload: 'theme'
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/store/theme/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/theme/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as theme } from './reducer'
3 |
4 | test(`theme: is a reducer function`, t => {
5 | t.is(typeof theme, 'function')
6 | })
7 |
8 | test(`theme: it sets the theme on INIT`, t => {
9 | let result = theme(undefined, {
10 | type: 'INIT',
11 | paylaod: {
12 | theme: {
13 | main: '#fff',
14 | highlight: '#000'
15 | }
16 | }
17 | })
18 |
19 | t.is(typeof result, 'object')
20 | })
21 |
22 | test(`theme: it has a default fallback if no theme is provided`, t => {
23 | let result = theme(undefined, {
24 | type: 'INIT'
25 | })
26 |
27 | t.is(typeof result, 'object')
28 | })
29 |
30 | test(`theme: it sets the theme on SET_THEME`, t => {
31 | let result = theme(undefined, {
32 | type: 'SET_THEME',
33 | paylaod: {
34 | theme: {
35 | main: '#fff'
36 | }
37 | }
38 | })
39 |
40 | t.is(typeof result, 'object')
41 | })
42 |
43 | test(`theme: it does nothing if a unknown action is dispatched`, t => {
44 | const result = theme('CUSTOM', {
45 | type: 'NOT_A_REAL_TYPE'
46 | })
47 | t.is(result, 'CUSTOM')
48 | })
49 |
--------------------------------------------------------------------------------
/src/store/transcripts/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import {
4 | INIT_TRANSCRIPTS,
5 | SET_TRANSCRIPTS_TIMELINE,
6 | SET_TRANSCRIPTS_CHAPTERS,
7 | UPDATE_TRANSCRIPTS,
8 | TOGGLE_FOLLOW_TRANSCRIPTS,
9 | SEARCH_TRANSCRIPTS,
10 | RESET_SEARCH_TRANSCRIPTS,
11 | SET_SEARCH_TRANSCRIPTS_RESULTS,
12 | NEXT_SEARCH_RESULT,
13 | PREVIOUS_SEARCH_RESULT
14 | } from '../types'
15 |
16 | export const initTranscripts = createAction(INIT_TRANSCRIPTS)
17 | export const setTranscriptsChapters = createAction(SET_TRANSCRIPTS_CHAPTERS, (chapters = []) => chapters)
18 | export const setTranscriptsTimeline = createAction(SET_TRANSCRIPTS_TIMELINE, (transcripts = []) => transcripts)
19 | export const updateTranscripts = createAction(UPDATE_TRANSCRIPTS, (playtime = 0) => playtime)
20 | export const followTranscripts = createAction(TOGGLE_FOLLOW_TRANSCRIPTS, (follow = true) => follow)
21 | export const searchTranscripts = createAction(SEARCH_TRANSCRIPTS)
22 | export const resetSearchTranscription = createAction(RESET_SEARCH_TRANSCRIPTS)
23 | export const setTranscriptsSearchResults = createAction(SET_SEARCH_TRANSCRIPTS_RESULTS, (results = []) => results)
24 | export const nextTranscriptsSearchResult = createAction(NEXT_SEARCH_RESULT)
25 | export const previousTranscriptsSearchResult = createAction(PREVIOUS_SEARCH_RESULT)
26 |
--------------------------------------------------------------------------------
/src/store/transcripts/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/visible-components/index.js:
--------------------------------------------------------------------------------
1 | export * from './reducer'
2 |
--------------------------------------------------------------------------------
/src/store/visible-components/reducer.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 | import { handleActions } from 'redux-actions'
3 |
4 | import { INIT } from '../types'
5 |
6 | export const INITIAL_STATE = [
7 | 'tabInfo',
8 | 'tabChapters',
9 | 'tabFiles',
10 | 'tabAudio',
11 | 'tabShare',
12 | 'tabTranscripts',
13 | 'poster',
14 | 'showTitle',
15 | 'episodeTitle',
16 | 'subtitle',
17 | 'progressbar',
18 | 'controlSteppers',
19 | 'controlChapters'
20 | ]
21 |
22 | const toVisibleComponentState = (components = []) =>
23 | components.reduce((result, component) => ({
24 | ...result,
25 | [component]: true
26 | }), {})
27 |
28 | export const reducer = handleActions({
29 | [INIT]: (_, { payload }) => toVisibleComponentState(get(payload, 'visibleComponents', INITIAL_STATE))
30 | }, toVisibleComponentState(INITIAL_STATE))
31 |
--------------------------------------------------------------------------------
/src/store/volume/actions.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions'
2 |
3 | import { SET_VOLUME } from '../types'
4 |
5 | export const setVolume = createAction(SET_VOLUME)
6 |
--------------------------------------------------------------------------------
/src/store/volume/actions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 |
3 | import { setVolume } from './actions'
4 |
5 | test(`volumeAction: creates the SET_VOLUME action`, t => {
6 | t.deepEqual(setVolume(0.2), {
7 | type: 'SET_VOLUME',
8 | payload: 0.2
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/src/store/volume/index.js:
--------------------------------------------------------------------------------
1 | export * from './actions'
2 | export * from './reducer'
3 |
--------------------------------------------------------------------------------
/src/store/volume/reducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { compose } from 'lodash/fp'
3 |
4 | import { inRange } from 'utils/math'
5 | import { toFloat } from 'utils/helper'
6 |
7 | import { SET_VOLUME } from '../types'
8 |
9 | export const INITIAL_STATE = 1
10 |
11 | const inVolumeRange = compose(inRange(0, INITIAL_STATE), toFloat)
12 |
13 | export const reducer = handleActions({
14 | [SET_VOLUME]: (state, { payload }) => inVolumeRange(payload)
15 | }, INITIAL_STATE)
16 |
--------------------------------------------------------------------------------
/src/store/volume/reducer.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { reducer as volume } from './reducer'
3 |
4 | test(`volume: is a reducer function`, t => {
5 | t.is(typeof volume, 'function')
6 | })
7 |
8 | test(`volume: it returns the correct volume`, t => {
9 | t.is(volume(undefined, {
10 | type: 'SET_RATE',
11 | payload: 1
12 | }), 1)
13 |
14 | t.is(volume(1, {
15 | type: 'SET_VOLUME',
16 | payload: -1
17 | }), 0)
18 |
19 | t.is(volume(1, {
20 | type: 'SET_VOLUME',
21 | payload: 2
22 | }), 1)
23 |
24 | t.is(volume(1, {
25 | type: 'SET_VOLUME',
26 | payload: 0.2
27 | }), 0.2)
28 | })
29 |
--------------------------------------------------------------------------------
/src/styles/_animations.scss:
--------------------------------------------------------------------------------
1 | @keyframes bounceInDown {
2 | from, 60%, 75%, 90%, to {
3 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
4 | }
5 |
6 | 0% {
7 | opacity: 0;
8 | transform: translate3d(0, -3000px, 0);
9 | }
10 |
11 | 60% {
12 | opacity: 1;
13 | transform: translate3d(0, 25px, 0);
14 | }
15 |
16 | 75% {
17 | transform: translate3d(0, -10px, 0);
18 | }
19 |
20 | 90% {
21 | transform: translate3d(0, 5px, 0);
22 | }
23 |
24 | to {
25 | transform: none;
26 | }
27 | }
28 |
29 | @keyframes bounceOutUp {
30 | 20% {
31 | transform: translate3d(0, -10px, 0);
32 | }
33 |
34 | 40%, 45% {
35 | opacity: 1;
36 | transform: translate3d(0, 20px, 0);
37 | }
38 |
39 | to {
40 | opacity: 0;
41 | transform: translate3d(0, -2000px, 0);
42 | }
43 | }
44 |
45 | @keyframes stop-and-scroll {
46 | 0% {
47 | transform: translateX(0);
48 | }
49 | 30% {
50 | transform: translateX(0);
51 | }
52 | 100%{
53 | transform: translateX(-100%);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/styles/_embed.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 |
3 | .podlove {
4 | &.embed {
5 | height: 100%;
6 |
7 | .header {
8 | height: calc(100% - #{$player-height} - #{$tabs-header-height})
9 | }
10 |
11 | .player {
12 | height: $player-height;
13 | }
14 |
15 | // Share mode
16 | .tabs {
17 | .tab-header-item.active {
18 | position: fixed;
19 | top: 0;
20 | border-color: rgba(0,0,0, 0.1);
21 | border-style: solid;
22 | border-width: 1px 1px 0 1px;
23 |
24 | .title {
25 | display: block;
26 | }
27 |
28 | .close {
29 | display: block;
30 | position: fixed;
31 | top: $padding / 1.5;
32 | right: $padding / 1.5;
33 | }
34 | }
35 |
36 | .tab-body {
37 | top: 100%;
38 | position: fixed;
39 | transition: top $animation-duration;
40 | }
41 |
42 | .tab-body.active {
43 | top: $tabs-header-height;
44 | width: 100%;
45 | height: calc(100% - #{$tabs-header-height});
46 | background: $background-color;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/_font.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Fira Sans;
3 | src: url('~styles/fonts/FiraSans-Light.eot') format('eot'),
4 | url('~styles/fonts/FiraSans-Light.woff') format('woff'),
5 | url('~styles/fonts/FiraSans-Light.woff2') format('woff2');
6 | font-weight: 300;
7 | }
8 |
9 | @font-face {
10 | font-family: Fira Sans;
11 | src: url('~styles/fonts/FiraSans-Regular.eot') format('eot'),
12 | url('~styles/fonts/FiraSans-Regular.woff') format('woff'),
13 | url('~styles/fonts/FiraSans-Regular.woff2') format('woff2');
14 | font-weight: 500;
15 | }
16 |
17 | @font-face {
18 | font-family: Fira Sans;
19 | src: url('~styles/fonts/FiraSans-Bold.eot') format('eot'),
20 | url('~styles/fonts/FiraSans-Bold.woff') format('woff'),
21 | url('~styles/fonts/FiraSans-Bold.woff2') format('woff2');
22 | font-weight: 700;
23 | }
24 |
25 | @font-face {
26 | font-family: Fira Mono;
27 | src: url('~styles/fonts/FiraMono-Regular.eot') format('eot'),
28 | url('~styles/fonts/FiraMono-Regular.woff') format('woff'),
29 | url('~styles/fonts/FiraMono-Regular.woff2') format('woff2');
30 | font-weight: 500;
31 | }
32 |
33 | @mixin font() {
34 | font-family: 'Fira Sans', sans-serif;
35 | }
36 |
37 | @mixin font-monospace() {
38 | font-family: 'Fira Mono', monospace;
39 | }
40 |
41 | .podlove {
42 | font-size: 14px;
43 | font-weight: 300;
44 | }
45 |
--------------------------------------------------------------------------------
/src/styles/_global.scss:
--------------------------------------------------------------------------------
1 | @import '~normalize.css/normalize.css';
2 |
3 | html {
4 | box-sizing: border-box;
5 | }
6 |
7 | *,
8 | *::before,
9 | *::after {
10 | box-sizing: inherit;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | padding: 0;
16 | line-height: 1.5
17 | }
18 |
19 | a {
20 | color: inherit;
21 | text-decoration: none;
22 |
23 | &:hover {
24 | text-decoration: none;
25 | }
26 | }
27 |
28 | img {
29 | max-width: 100%;
30 | width: auto;
31 | }
32 |
33 | // Removes outline only for mouse users
34 | :focus:not(:focus-visible) { outline: none }
35 |
--------------------------------------------------------------------------------
/src/styles/_loader.scss:
--------------------------------------------------------------------------------
1 | .loader {
2 | width: 100%;
3 | height: 100%;
4 | position: fixed;
5 | left: 0;
6 | top: 0;
7 | z-index: 999;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | opacity: 1;
12 | transition: opacity linear 300ms;
13 |
14 | .dot {
15 | width: 20px;
16 | height: 20px;
17 | margin: 3px;
18 | border-radius: 100%;
19 | display: inline-block;
20 | animation: loader 1.4s ease-in-out 0s infinite both;
21 | }
22 |
23 | .bounce1 {
24 | animation-delay: -0.32s;
25 | }
26 |
27 | .bounce2 {
28 | animation-delay: -0.16s;
29 | }
30 |
31 | &.done {
32 | opacity: 0;
33 | }
34 | }
35 |
36 | @-webkit-keyframes loader {
37 | 0%,
38 | 80%,
39 | 100% {
40 | transform: scale(0);
41 | }
42 | 40% {
43 | transform: scale(1);
44 | }
45 | }
46 |
47 | @keyframes loader {
48 | 0%,
49 | 80%,
50 | 100% {
51 | transform: scale(0);
52 | }
53 | 40% {
54 | transform: scale(1);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/styles/_marquee.scss:
--------------------------------------------------------------------------------
1 | .marquee-container {
2 | overflow: hidden;
3 | position: relative;
4 |
5 | .marquee {
6 | position: absolute;
7 | width: 100%;
8 | height: 100%;
9 | margin: 0;
10 | text-align: left;
11 |
12 | /* Starting position */
13 | transform: translateX(0%);
14 |
15 | /* Apply animation to this element */
16 | animation: scroll 10s linear infinite;
17 | }
18 |
19 | /* Move it (define the animation) */
20 | @keyframes scroll {
21 | 0% {
22 | opacity: 1;
23 | transform: translateX(0%);
24 | }
25 | 20% {
26 | opacity: 1;
27 | transform: translateX(0%);
28 | }
29 | 90% {
30 | opacity: 1;
31 | }
32 | 100% {
33 | opacity: 0;
34 | transform: translateX(-100%);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/_share.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 | $button-size: 60px;
3 |
4 | .channel-link {
5 | display: flex;
6 | align-items: center;
7 | flex-direction: column;
8 | margin: $margin / 2;
9 | cursor: pointer;
10 | opacity: 1;
11 |
12 | .channel-icon {
13 | width: $button-size;
14 | height: $button-size;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | border-radius: 4px;
19 | border: 2px solid transparent;
20 | }
21 |
22 | &.active .channel-icon {
23 | border-color: currentColor;
24 | }
25 |
26 | &:hover {
27 | opacity: 0.8;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/styles/_text.scss:
--------------------------------------------------------------------------------
1 | h1, h2, h3, h4 {
2 | &.title {
3 | margin: 0 0 ($margin / 2) 0;
4 | }
5 | }
6 |
7 | p {
8 | margin: 0 0 $margin 0;
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/_transitions.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 |
3 | // Fade Transitions
4 | .button-enter-active, .button-leave-active {
5 | transition: opacity $animation-duration * 2;
6 | }
7 |
8 | .button-enter, .button-leave-to {
9 | opacity: 0
10 | }
11 |
12 | // Height Transitions
13 | .progressbar-enter-active, .progressbar-leave-active {
14 | height: $progress-bar-height;
15 | transition: height $animation-duration * 2;
16 | }
17 |
18 | .progressbar-enter, .progressbar-leave-to {
19 | height: 0;
20 | }
21 |
--------------------------------------------------------------------------------
/src/styles/_utils.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 |
3 | .truncate {
4 | white-space: nowrap;
5 | overflow: hidden;
6 | text-overflow: ellipsis;
7 | }
8 |
9 | .text-left {
10 | text-align: left;
11 | }
12 |
13 | .text-right {
14 | text-align: right;
15 | }
16 |
17 | .text-center {
18 | text-align: center;
19 | }
20 |
21 | .seperator {
22 | border-bottom: 1px solid $subtile-color;
23 | padding-bottom: $padding * 2;
24 | }
25 |
26 | .centered {
27 | display: flex;
28 | align-items: center;
29 | justify-content: center;
30 |
31 | &.column {
32 | flex-direction: column;
33 | }
34 | }
35 |
36 | .shadowed {
37 | box-shadow: 0px 1px 5px rgba(0, 0 ,0 , 0.1);
38 | }
39 |
40 | .spaced {
41 | display: flex;
42 | justify-content: space-between;
43 | }
44 |
45 | .visually-hidden {
46 | position: absolute !important;
47 | height: 1px;
48 | width: 1px;
49 | overflow: hidden;
50 | clip: rect(1px, 1px, 1px, 1px);
51 | }
52 |
--------------------------------------------------------------------------------
/src/styles/fonts/FiraMono-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraMono-Regular.eot
--------------------------------------------------------------------------------
/src/styles/fonts/FiraMono-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraMono-Regular.woff
--------------------------------------------------------------------------------
/src/styles/fonts/FiraMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraMono-Regular.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Bold.eot
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Bold.woff
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Bold.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Light.eot
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Light.woff
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Light.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Regular.eot
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Regular.woff
--------------------------------------------------------------------------------
/src/styles/fonts/FiraSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/podlove/podlove-web-player/d05afaf24cc0f18ed572ae2c99579f8d87abdc21/src/styles/fonts/FiraSans-Regular.woff2
--------------------------------------------------------------------------------
/src/styles/resets/_button.scss:
--------------------------------------------------------------------------------
1 | /* Base */
2 | [role="button"],
3 | input[type="submit"],
4 | input[type="reset"],
5 | input[type="button"],
6 | button {
7 | box-sizing: border-box;
8 | }
9 |
10 | /* Reset `button` and button-style `input` default styles */
11 | input[type="submit"],
12 | input[type="reset"],
13 | input[type="button"],
14 | button {
15 | background: none;
16 | border: 0;
17 | color: inherit;
18 | cursor: pointer;
19 | font: inherit;
20 | line-height: normal;
21 | overflow: visible;
22 | padding: 0;
23 | appearance: button; /* for input */
24 | user-select: none; /* for button */
25 | }
26 | input::-moz-focus-inner,
27 | button::-moz-focus-inner {
28 | border: 0;
29 | padding: 0;
30 | }
31 | button:focus {
32 | outline:0;
33 | }
34 | /* Make `a` like a button */
35 | [role="button"] {
36 | color: inherit;
37 | cursor: default;
38 | display: inline-block;
39 | text-align: center;
40 | text-decoration: none;
41 | white-space: pre;
42 | user-select: none;
43 | }
44 |
--------------------------------------------------------------------------------
/src/styles/resets/_input.scss:
--------------------------------------------------------------------------------
1 | input::-webkit-outer-spin-button,
2 | input::-webkit-inner-spin-button {
3 | display: none;
4 | -webkit-appearance: none;
5 | margin: 0;
6 | }
7 |
8 | input[type=number] {
9 | -moz-appearance:textfield;
10 | }
11 |
--------------------------------------------------------------------------------
/src/styles/resets/_lists.scss:
--------------------------------------------------------------------------------
1 | ul, ol {
2 | list-style: none;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/resets/_resets.scss:
--------------------------------------------------------------------------------
1 | @import './range';
2 | @import './input';
3 | @import './button';
4 | @import './lists';
5 |
--------------------------------------------------------------------------------
/src/utils/binary-search.js:
--------------------------------------------------------------------------------
1 | export const binarySearch = (list = []) => search => {
2 | let minIndex = 0
3 | let maxIndex = list.length - 1
4 | let currentIndex
5 | let currentElement
6 |
7 | while (minIndex <= maxIndex) {
8 | currentIndex = (minIndex + maxIndex) / 2 | 0
9 | currentElement = list[currentIndex]
10 |
11 | if (currentElement < search) {
12 | minIndex = currentIndex + 1
13 | } else if (currentElement > search) {
14 | maxIndex = currentIndex - 1
15 | } else {
16 | return currentIndex
17 | }
18 | }
19 |
20 | return maxIndex
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/binary-search.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { binarySearch } from './binary-search'
3 |
4 | const search = binarySearch([
5 | 0,
6 | 1,
7 | 2,
8 | 3,
9 | 4,
10 | 5,
11 | 10
12 | ])
13 |
14 | test(`it finds an item in a list`, t => {
15 | t.is(search(3), 3)
16 | })
17 |
18 | test(`it finds the nearest index`, t => {
19 | t.is(search(9), 5)
20 | })
21 |
22 | test(`it falls back to -1 if no lower index was found`, t => {
23 | t.is(search(-5), -1)
24 | })
25 |
--------------------------------------------------------------------------------
/src/utils/effects.js:
--------------------------------------------------------------------------------
1 | import { noop, isArray, isString } from 'lodash'
2 | import { compose, get, isFunction } from 'lodash/fp'
3 |
4 | import { isDefinedAndNotNull } from './predicates'
5 |
6 | const truthy = input => input === true
7 |
8 | const isArrayProperty = input => isArray(input) ? input.length > 0 : input
9 | const isStringProperty = input => isString(input) ? input.length > 0 : input
10 |
11 | // Check if payload has property
12 | export const hasProperty = property => compose(truthy, isArrayProperty, isStringProperty, get(property))
13 |
14 | // Dispatches only if the data is defined
15 | export const prohibitiveDispatch = (dispatch, action) => data => isDefinedAndNotNull(data) ? dispatch(action(data)) : null
16 |
17 | // effect handler
18 | export const handleActions = effectFunc => (store, action) => {
19 | const effect = get(action.type)(effectFunc)
20 |
21 | return isFunction(effect) ? effect(store, action, store.getState()) : noop
22 | }
23 |
24 | export const conditionalEffect = effectFunc => (precondition = true) => precondition ? effectFunc : noop
25 |
--------------------------------------------------------------------------------
/src/utils/helper.js:
--------------------------------------------------------------------------------
1 | import { isUndefinedOrNull } from './predicates'
2 |
3 | /**
4 | * Collection of functional helpers
5 | */
6 |
7 | export const inAnimationFrame = func => (...args) => window.requestAnimationFrame(() => func.apply(null, args))
8 | export const asyncAnimation = func => (...args) => new Promise(resolve => {
9 | window.requestAnimationFrame(resolve(func.apply(null, args)))
10 | })
11 |
12 | export const callWith = (...args) => func => func.apply(null, args)
13 |
14 | // Math helpers
15 | export const toInt = (input = 0) => isNaN(parseInt(input, 10)) ? 0 : parseInt(input, 10)
16 | export const toFloat = (input = 0) => isNaN(parseFloat(input)) ? 0 : parseFloat(input)
17 |
18 | // Functional Helper
19 |
20 | export const fallbackTo = fallback => value => isUndefinedOrNull(value) ? fallback : value
21 |
--------------------------------------------------------------------------------
/src/utils/keyboard.js:
--------------------------------------------------------------------------------
1 | import keyboard from 'keyboardjs'
2 |
3 | export default (key, onPress, onRelease) => {
4 | keyboard.bind(key, event => {
5 | // Scope is in input or other dom element
6 | if (event.target.nodeName !== 'BODY') {
7 | return
8 | }
9 |
10 | event.preventDefault()
11 | onPress && onPress()
12 | }, event => {
13 | // Scope is in input or other dom element
14 | if (event.target.nodeName !== 'BODY') {
15 | return
16 | }
17 |
18 | event.preventDefault()
19 | onRelease && onRelease()
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/math.js:
--------------------------------------------------------------------------------
1 | import { curry } from 'lodash/fp'
2 |
3 | export const toPercent = (input = 0) => {
4 | input = parseFloat(input) * 100
5 | return Math.round(input)
6 | }
7 |
8 | export const round = (input = 0) => Math.ceil(input * 100) / 100
9 |
10 | export const interpolate = (num = 0) => Math.round(num * 100) / 100
11 |
12 | export const roundUp = curry((base, number) => {
13 | number = Math.ceil(number * 100)
14 |
15 | if (number % base === 0) {
16 | return (number + base) / 100
17 | }
18 |
19 | return (number + (base - number % base)) / 100
20 | })
21 |
22 | export const relativePosition = (current = 0, maximum = 0) =>
23 | ((current * 100) / maximum) + '%'
24 |
25 | export const inRange = (lower = 0, upper = 0) => (value = 0) => {
26 | if (value < lower) {
27 | return lower
28 | }
29 |
30 | if (value > upper) {
31 | return upper
32 | }
33 |
34 | return value
35 | }
36 |
37 | export const toDecimal = (input = 0) => parseFloat(Math.round(input * 100) / 100).toFixed(2)
38 |
--------------------------------------------------------------------------------
/src/utils/math.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { toPercent, roundUp, round, interpolate, relativePosition } from './math'
3 |
4 | test('exports a method called toPercent', t => {
5 | t.is(typeof toPercent, 'function')
6 | })
7 |
8 | test('exports a method called roundUp', t => {
9 | t.is(typeof roundUp, 'function')
10 | })
11 |
12 | test('exports a method called round', t => {
13 | t.is(typeof round, 'function')
14 | })
15 |
16 | test(`toPrecent: transforms absolute value to percentage`, t => {
17 | t.is(toPercent(), 0)
18 | t.is(toPercent(1.5), 150)
19 | })
20 |
21 | test(`round: rounds to floats with 2 nachkomma`, t => {
22 | t.is(round(), 0.00)
23 | t.is(round(1), 1.00)
24 | t.is(round(1.33777777), 1.34)
25 | })
26 |
27 | test(`roundUp: rounds to next full float quantile`, t => {
28 | t.is(roundUp(5)(1), 1.05)
29 | t.is(roundUp(5)(1.05), 1.10)
30 | t.is(roundUp(5)(1.06), 1.10)
31 | })
32 |
33 | test(`interpolate: interpolates a given number`, t => {
34 | t.is(interpolate(12.555), 12.56)
35 | t.is(interpolate(10), 10)
36 | })
37 |
38 | test(`relativePosition: returns position in percent relative to a maximum value`, t => {
39 | t.is(relativePosition(50, 100), '50%')
40 | t.is(relativePosition(40, 160), '25%')
41 | })
42 |
--------------------------------------------------------------------------------
/src/utils/predicates.js:
--------------------------------------------------------------------------------
1 | import { isUndefined, isNull } from 'lodash/fp'
2 |
3 | const or = (p1, p2) => x => p1(x) || p2(x)
4 | // const and = (p1, p2) => x => p1(x) && p2(x)
5 | const not = p => x => !p(x)
6 |
7 | export const isUndefinedOrNull = or(isUndefined, isNull)
8 | export const isDefinedAndNotNull = not(isUndefinedOrNull)
9 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import request from 'superagent'
2 |
3 | export default url =>
4 | (typeof url === 'string'
5 | ? request
6 | .get(url)
7 | .query({ format: 'json' })
8 | .set('Accept', 'application/json')
9 | .then(res => res.body)
10 | : new Promise(resolve => resolve(url)))
11 |
--------------------------------------------------------------------------------
/src/utils/request.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import superagent from 'superagent'
3 | import nocker from 'superagent-nock'
4 |
5 | import request from './request'
6 |
7 | let nock
8 |
9 | test.beforeEach(t => {
10 | nock = nocker(superagent)
11 | })
12 |
13 | test(`request: exports a function`, t => {
14 | t.is(typeof request, 'function')
15 | })
16 |
17 | test.cb(`request: should resolve an url`, t => {
18 | t.plan(1)
19 |
20 | nock('http://localhost')
21 | .get('/foo')
22 | .reply(200, { foo: 'bar' })
23 |
24 | request('http://localhost/foo')
25 | .then(result => {
26 | t.deepEqual(result, { foo: 'bar' })
27 | t.end()
28 | })
29 | })
30 |
31 | test.cb(`request: should return an object`, t => {
32 | t.plan(1)
33 |
34 | request({ foo: 'bar' })
35 | .then(result => {
36 | t.deepEqual(result, { foo: 'bar' })
37 | t.end()
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/src/utils/runtime.js:
--------------------------------------------------------------------------------
1 | import { head } from 'lodash'
2 | import browser from 'detect-browser'
3 | import MobileDetect from 'mobile-detect'
4 |
5 | import { version } from '../../package.json'
6 |
7 | const platform = new MobileDetect(window.navigator.userAgent)
8 |
9 | const locale = navigator.language || navigator.userLanguage || 'en-us'
10 |
11 | const currentLanguage = (() => {
12 | return head(locale.split('-'))
13 | })()
14 |
15 | export default {
16 | version,
17 | browser: `${browser.name}:${browser.version}`,
18 | platform: (platform.tablet() || platform.mobile()) ? 'mobile' : 'desktop',
19 | language: currentLanguage,
20 | locale
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | import curry from 'lodash/fp/curry'
2 | import get from 'lodash/get'
3 | import merge from 'lodash/merge'
4 |
5 | const PODLOVE_WEB_PLAYER_TOKEN = 'pwp'
6 |
7 | const getItem = curry((hash, key) => {
8 | try {
9 | const fromStore = window.localStorage.getItem(PODLOVE_WEB_PLAYER_TOKEN) || ''
10 | let obj = JSON.parse(fromStore)
11 |
12 | if (!hash) {
13 | return obj || {}
14 | }
15 |
16 | if (!key) {
17 | return get(obj, hash, {})
18 | }
19 |
20 | return get(obj, [hash, key])
21 | } catch (err) {
22 | return undefined
23 | }
24 | })
25 |
26 | const setItem = hash => (...args) => {
27 | let data
28 |
29 | if (args.length > 1) {
30 | data = { [args[0]]: args[1] }
31 | } else {
32 | data = args[0]
33 | }
34 |
35 | try {
36 | const currentStore = getItem(null, null)
37 | const toStore = JSON.stringify(merge(currentStore, { [hash]: data }))
38 |
39 | return window.localStorage.setItem(PODLOVE_WEB_PLAYER_TOKEN, toStore)
40 | } catch (err) {
41 | return undefined
42 | }
43 | }
44 |
45 | export default hash => ({
46 | set: setItem(hash),
47 | get: getItem(hash)
48 | })
49 |
--------------------------------------------------------------------------------
/src/utils/text-search.js:
--------------------------------------------------------------------------------
1 | export const textSearch = (input = []) => (query = '') => {
2 | const queryExpr = new RegExp(query, 'ig')
3 |
4 | return input.reduce((results, item, index) => {
5 | const searchHits = item.match(queryExpr) || []
6 |
7 | // add n times the chunk index, for each hit one
8 | searchHits.forEach(() => {
9 | results.push(index)
10 | })
11 |
12 | return results
13 | }, [])
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/text-search.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import { textSearch } from './text-search'
3 |
4 | const search = textSearch([
5 | 'foo',
6 | 'bar',
7 | 'baz',
8 | 'bla bla bla',
9 | 'gnarz'
10 | ])
11 |
12 | test(`it finds an item in a list`, t => {
13 | t.deepEqual(search('bar'), [1])
14 | })
15 |
16 | test(`it returns an empty array if nothing was found`, t => {
17 | t.deepEqual(search('bar1'), [])
18 | })
19 |
20 | test(`it repeats an index if text contains multiple hits`, t => {
21 | t.deepEqual(search('bla'), [3, 3, 3])
22 | })
23 |
--------------------------------------------------------------------------------
/src/utils/url.js:
--------------------------------------------------------------------------------
1 | import queryString from 'query-string'
2 | import { isString } from 'lodash'
3 |
4 | import { toPlayerTime } from 'utils/time'
5 |
6 | export const locationParams = queryString.parse(window.location.search)
7 |
8 | const parseParameters = parameters => {
9 | const parsed = {}
10 |
11 | if (parameters.t) {
12 | const [start, stop] = parameters.t.split(',')
13 | parsed.starttime = isString(start) ? toPlayerTime(start) : undefined
14 | parsed.stoptime = isString(stop) ? toPlayerTime(stop) : undefined
15 | }
16 |
17 | if (parameters.episode) {
18 | parsed.episode = parameters.episode
19 | }
20 |
21 | if (parameters.autoplay) {
22 | parsed.autoplay = true
23 | }
24 |
25 | return parsed
26 | }
27 |
28 | export const urlParameters = { ...parseParameters(locationParams) }
29 |
30 | export const addQueryParameter = (url, additionalParameters = {}) => {
31 | const parser = document.createElement('a')
32 | parser.href = url
33 |
34 | const existingParameters = queryString.parse(parser.search)
35 | parser.search = queryString.stringify(Object.assign({}, existingParameters, additionalParameters), { encode: false })
36 |
37 | return parser.href
38 | }
39 |
--------------------------------------------------------------------------------