Compare commits

...

18 Commits

32 changed files with 4305 additions and 508 deletions

View File

@ -1,3 +0,0 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["react-app", "prettier"]
}

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
node_modules/ node_modules/
dist/ build/

8
babel.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = api => {
// caching the babel config
api.cache.using(() => process.env.NODE_ENV)
return {
presets: ['@babel/preset-env', '@babel/preset-react']
// plugins: [api.env('development') && 'react-refresh/babel'].filter(Boolean)
}
}

3851
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,29 +6,52 @@
"scripts": { "scripts": {
"start": "webpack-dev-server --mode development", "start": "webpack-dev-server --mode development",
"build": "webpack --mode production", "build": "webpack --mode production",
"build-fresh": "rm -rf build/ && npm run build",
"test": "tests", "test": "tests",
"lint": "prettier --check src/**/*.js", "lint": "eslint src",
"format": "prettier --write src/**/*.js" "format": "prettier --write src/**/*.js"
}, },
"author": "Alfred Melch (dev@melch.pro)", "author": "Alfred Melch (dev@melch.pro)",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"classnames": "^2.2.6",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-icons": "^3.8.0",
"react-redux": "^7.1.3", "react-redux": "^7.1.3",
"redux": "^4.0.4", "redux": "^4.0.4",
"reselect": "^4.0.0", "reselect": "^4.0.0"
"styled-components": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.5", "@babel/core": "^7.7.5",
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.7.6",
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.7.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"html-webpack-plugin": "^3.2.0", "css-loader": "^3.3.2",
"eslint": "^6.7.2",
"eslint-config-prettier": "^6.7.0",
"eslint-config-react-app": "^5.1.0",
"eslint-plugin-flowtype": "^4.5.2",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^2.3.0",
"html-webpack-plugin": "^4.0.0-beta.11",
"mini-css-extract-plugin": "^0.8.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-loader": "^3.0.0",
"postcss-normalize": "^8.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"react-refresh": "^0.7.0",
"style-loader": "^1.0.1",
"webpack": "^4.41.2", "webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10", "webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0" "webpack-dev-server": "^3.9.0",
"webpackbar": "^4.0.0"
} }
} }

7
postcss.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env'),
require('postcss-normalize')
]
}

View File

@ -1,13 +1,13 @@
import React from 'react' import React from 'react'
import { TextInput } from './components/TextInput'
import { TextOutput } from './components/TextOutput'
import { RsvpReader } from './components/RsvpReader' import { RsvpReader } from './components/RsvpReader'
export function App() { import { IconContext } from 'react-icons'
export const App = () => {
return ( return (
<div> <IconContext.Provider value={{ color: 'grey', size: '1.4em' }}>
<RsvpReader></RsvpReader> <RsvpReader></RsvpReader>
</div> </IconContext.Provider>
) )
} }

View File

@ -1,18 +0,0 @@
import React from 'react'
import { useDispatch } from 'react-redux'
import {
incrementSentence,
incrementSegment,
incrementWord
} from '../store/actions'
export const MainControl = () => {
const dispatch = useDispatch()
return (
<div>
<button onClick={() => dispatch(incrementSegment())}>></button>
<button onClick={() => dispatch(incrementWord())}>Word></button>
<button onClick={() => dispatch(incrementSentence())}>Sentence></button>
</div>
)
}

31
src/components/Options.js Normal file
View File

@ -0,0 +1,31 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setMaxLength, setWpm } from '../store/actions'
export const Options = () => {
const dispatch = useDispatch()
const maxLength = useSelector(state => state.maxLength)
const wpm = useSelector(state => state.wpm)
return (
<div>
<input
type="range"
min="3"
max="14"
value={maxLength}
onChange={e => dispatch(setMaxLength(e.target.value))}
/>
{maxLength}
<input
type="range"
min="100"
max="1000"
value={wpm}
onChange={e => dispatch(setWpm(e.target.value))}
/>
{wpm}
</div>
)
}

View File

@ -0,0 +1,38 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
hasNextSegment,
selectRunning,
selectInterval
} from '../store/selectors'
import { incrementSegment, stop, pause, start } from '../store/actions'
import { FiPlay, FiPause, FiSquare } from 'react-icons/fi'
import { useInterval } from './generics/useInterval'
import { IconButton } from '../styles/IconButton'
export const PlayerControl = () => {
const dispatch = useDispatch()
const running = useSelector(selectRunning)
const hasNext = useSelector(hasNextSegment)
const interval = useSelector(selectInterval)
useInterval(
() => {
if (!hasNext) dispatch(pause())
else dispatch(incrementSegment())
},
running ? interval : null
)
return (
<>
<IconButton Icon={FiSquare} onClick={() => dispatch(stop())} />
<IconButton
Icon={running ? FiPause : FiPlay}
onClick={() => dispatch(running ? pause() : start())}
disabled={!hasNext}
/>
</>
)
}

View File

@ -0,0 +1,13 @@
.container {
display: flex;
}
.item {
flex: 1;
border: 1px solid black;
}
.mainItem {
composes: item;
flex: 2;
}

View File

@ -1,32 +1,29 @@
import React from 'react' import React from 'react'
import styled from 'styled-components'
import { TextInput } from './TextInput' import { TextInput } from './TextInput'
import { TextOutput } from './TextOutput' import { TextOutput } from './TextOutput'
import { MainControl } from './MainControl' import { SegmentControl } from './SegmentControl'
import { RsvpSegment } from './RsvpSegment' import { RsvpSegment } from './RsvpSegment'
import { Options } from './Options'
import { PlayerControl } from './PlayerControl'
const FlexRow = styled.div` import styles from './RsvpReader.css'
display: flex;
`
const FlexItem = styled.div`
flex: ${props => props.flex || 1};
border: 1px solid black;
`
export const RsvpReader = () => { export const RsvpReader = () => {
return ( return (
<FlexRow> <div className={styles.container}>
<FlexItem> <div className={styles.item}>
<TextInput /> <TextInput />
</FlexItem> </div>
<FlexItem flex={2}> <div className={styles.mainItem}>
<RsvpSegment /> <RsvpSegment />
<MainControl></MainControl> <SegmentControl>
</FlexItem> <PlayerControl />
<FlexItem> </SegmentControl>
<Options></Options>
</div>
<div className={styles.item}>
<TextOutput /> <TextOutput />
</FlexItem> </div>
</FlexRow> </div>
) )
} }

View File

@ -0,0 +1,46 @@
import React from 'react'
import { useDispatch } from 'react-redux'
import {
FiSkipBack,
FiSkipForward,
FiRewind,
FiFastForward
} from 'react-icons/fi'
import { IconButton } from '../styles/IconButton'
import {
incrementSentence,
incrementWord,
decrementSentence,
decrementWord
} from '../store/actions'
export const SegmentControl = ({ children }) => {
const dispatch = useDispatch()
return (
<div>
<IconButton
title="Previous Sentence"
onClick={() => dispatch(decrementSentence())}
Icon={FiRewind}
/>
<IconButton
Icon={FiSkipBack}
title="Previous Word"
onClick={() => dispatch(decrementWord())}
/>
{children}
<IconButton
Icon={FiSkipForward}
title="Next Word"
onClick={() => dispatch(incrementWord())}
/>
<IconButton
Icon={FiFastForward}
title="Next Sentence"
onClick={() => dispatch(incrementSentence())}
/>
</div>
)
}

View File

@ -1,20 +1,8 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import styled from 'styled-components'
import { setText } from '../store/actions.js' import { setText } from '../store/actions.js'
const Button = styled.button`
padding: 0.5em 1em;
border-radius: 3px;
background: white;
color: grey;
&:hover {
background: lightgrey;
}
`
const lorem = const lorem =
'Excepteur aliqua cupidatat ullamco laboris cupidatat elit sint cillum incididunt. Anim sit excepteur laboris commodo ullamco consequat tempor. Velit elit eiusmod aute aliquip amet sunt minim deserunt voluptate esse ea sint. Commodo ipsum dolor dolor Lorem et consectetur minim ut in voluptate. Nulla qui consectetur nostrud sint anim minim duis qui amet. Ipsum reprehenderit eiusmod quis Lorem. Consectetur ipsum quis incididunt proident ea sit mollit veniam in excepteur.' 'Excepteur aliqua cupidatat ullamco laboris cupidatat elit sint cillum incididunt. Anim sit excepteur laboris commodo ullamco consequat tempor. Velit elit eiusmod aute aliquip amet sunt minim deserunt voluptate esse ea sint. Commodo ipsum dolor dolor Lorem et consectetur minim ut in voluptate. Nulla qui consectetur nostrud sint anim minim duis qui amet. Ipsum reprehenderit eiusmod quis Lorem. Consectetur ipsum quis incididunt proident ea sit mollit veniam in excepteur.'
@ -27,7 +15,7 @@ export const TextInput = () => {
defaultValue={text} defaultValue={text}
onInput={e => setTextState(e.target.value)} onInput={e => setTextState(e.target.value)}
></textarea> ></textarea>
<Button onClick={() => dispatch(setText(text))}>Load</Button> <button onClick={() => dispatch(setText(text))}>Load</button>
</div> </div>
) )
} }

View File

@ -0,0 +1,7 @@
.current {
text-decoration: underline;
}
.sentenceBeginning {
color: red;
}

View File

@ -1,36 +1,39 @@
import React from 'react' import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import styled from 'styled-components' import classNames from 'classnames'
import { selectParsedText } from '../store/selectors.js' import { getNextSmallerNumber } from '../lib/array-util.js'
import {
selectParsedText,
selectCurrentSegmentIndex
} from '../store/selectors.js'
const Segment = styled.span` import styles from './TextOutput.css'
color: ${props => (props.sentenceBeginning ? 'red' : 'black')};
font-weight: ${props => (props.current ? 'bold' : 'normal')};
`
const SelectedSegment = styled.span`
font-weight: ;
`
export const TextOutput = () => { export const TextOutput = () => {
const parsedText = useSelector(selectParsedText) const { segments, sentences, words } = useSelector(selectParsedText)
const curSegment = useSelector(state => state.curSegment) const curSegment = useSelector(selectCurrentSegmentIndex)
const { segments, words, sentences } = parsedText
return ( return (
<div> <div>
{segments.map((segment, idx) => { {segments.map((segment, idx) => {
const current = curSegment === idx const isCurrent = curSegment === idx
const sentenceBeginning = sentences.includes(idx) const isWordStart = words.includes(idx)
const wordBeginning = isWordStart
? idx
: getNextSmallerNumber(idx, words)
const isSentenceStart = sentences.includes(wordBeginning)
return ( return (
<Segment <span
key={idx} key={idx}
current={current} className={classNames({
sentenceBeginning={sentenceBeginning} [styles.current]: isCurrent,
[styles.sentenceBeginning]: isSentenceStart
})}
> >
{segment}{' '} {isWordStart && ' '}
</Segment> {segment}
</span>
) )
})} })}
</div> </div>

View File

@ -0,0 +1,75 @@
import React, { useState, useEffect, useRef } from 'react'
import { FiPlay, FiPause, FiSquare } from 'react-icons/fi'
function safeCall(fn) {
typeof fn === 'function' && fn()
}
export const Player = ({ onTick, onStart, onStop, onPause, delay }) => {
const [isRunning, start, stop] = usePlayer(stop => onTick(stop), delay)
const handleStart = () => {
safeCall(onStart)
start()
console.log('yay')
}
const handleStop = () => {
safeCall(onStop)
stop()
}
const handlePause = () => {
safeCall(onPause)
stop()
}
return (
<div>
<button onClick={handleStop}>
<FiSquare />
</button>
{isRunning && (
<button onClick={handlePause}>
<FiPause />
</button>
)}
{!isRunning && (
<button onClick={handleStart}>
<FiPlay />
</button>
)}
</div>
)
}
function usePlayer(onTick, delay) {
const [isRunning, setRunning] = useState(false)
const start = () => setRunning(true)
const stop = () => setRunning(false)
useInterval(() => onTick(stop), isRunning ? delay : null)
return [isRunning, start, stop]
}
// from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}

View File

@ -0,0 +1,21 @@
import { useRef, useEffect } from 'react'
// from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
export function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}

View File

@ -12,10 +12,9 @@ function createRootElement() {
body.appendChild(root) body.appendChild(root)
return root return root
} }
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<App></App> <App />
</Provider>, </Provider>,
createRootElement() createRootElement()
) )

View File

@ -25,6 +25,6 @@ export function getNextSmallerNumber(num, sortedArray) {
return null return null
} }
function lastEntry(arr) { export function lastEntry(arr) {
return arr[arr.length - 1] return arr[arr.length - 1]
} }

View File

@ -1,4 +1,16 @@
export const setText = text => ({ type: 'SET_TEXT', payload: text }) export const setText = text => ({ type: 'SET_TEXT', payload: text })
export const incrementSegment = () => ({ type: 'INCREMENT_SEGMENT' }) export const resetSegment = () => ({ type: 'SET_CURRENT_SEGMENT', payload: 0 })
export const incrementWord = () => ({ type: 'INCREMENT_WORD' }) export const incrementSegment = () => ({ type: 'INC_SEGMENT' })
export const incrementSentence = () => ({ type: 'INCREMENT_SENTENCE' }) export const decrementSegment = () => ({ type: 'DEC_SEGMENT' })
export const incrementWord = () => ({ type: 'INC_WORD' })
export const decrementWord = () => ({ type: 'DEC_WORD' })
export const incrementSentence = () => ({ type: 'INC_SENTENCE' })
export const decrementSentence = () => ({ type: 'DEC_SENTENCE' })
export const setMaxLength = length => ({
type: 'SET_MAX_LENGTH',
payload: length
})
export const setWpm = wpm => ({ type: 'SET_WPM', payload: wpm })
export const start = () => ({ type: 'START' })
export const stop = () => ({ type: 'STOP' })
export const pause = () => ({ type: 'PAUSE' })

View File

@ -1,26 +1,37 @@
import { selectNextWord, selectNextSentence } from './selectors' import {
selectNextWord,
selectNextSentence,
selectPrevWord,
selectPrevSentence
} from './selectors'
export const initialState = { export const initialState = {
originalText: 'Sample Text', originalText: 'Sample Text',
curSegment: 0 curIdx: 0,
maxLength: 5,
isPlaying: false,
wpm: 300,
running: false
} }
const reducer = { const reducer = {
SET_TEXT: (state, payload) => ({ SET_TEXT: (state, payload) => ({
...state, ...state,
originalText: payload, originalText: payload,
curSegment: 0 curIdx: 0
}), }),
SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curSegment: payload }), SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curIdx: payload }),
INCREMENT_SEGMENT: state => ({ INC_SEGMENT: state => ({ ...state, curIdx: state.curIdx + 1 }),
...state, DEC_SEGMENT: state => ({ ...state, curIdx: state.curIdx - 1 }),
curSegment: state.curSegment + 1 INC_WORD: state => ({ ...state, curIdx: selectNextWord(state) }),
}), DEC_WORD: state => ({ ...state, curIdx: selectPrevWord(state) }),
INCREMENT_WORD: state => ({ ...state, curSegment: selectNextWord(state) }), INC_SENTENCE: state => ({ ...state, curIdx: selectNextSentence(state) }),
INCREMENT_SENTENCE: state => ({ DEC_SENTENCE: state => ({ ...state, curIdx: selectPrevSentence(state) }),
...state, SET_MAX_LENGTH: (state, payload) => ({ ...state, maxLength: payload }),
curSegment: selectNextSentence(state) SET_WPM: (state, payload) => ({ ...state, wpm: payload }),
}) START: state => ({ ...state, running: true }),
STOP: state => ({ ...state, running: false, curIdx: 0 }),
PAUSE: state => ({ ...state, running: false })
} }
export const reducerFn = (state, { type, payload }) => { export const reducerFn = (state, { type, payload }) => {

View File

@ -5,9 +5,12 @@ import { getNextBiggerNumber, getNextSmallerNumber } from '../lib/array-util.js'
// parsedText selectors // parsedText selectors
export const selectMaxLength = state => state.maxLength
export const selectParsedText = createSelector( export const selectParsedText = createSelector(
state => state.originalText, state => state.originalText,
originalText => parseText(originalText) selectMaxLength,
(originalText, maxLength) => parseText(originalText, maxLength)
) )
export const selectSegments = createSelector( export const selectSegments = createSelector(
@ -15,15 +18,20 @@ export const selectSegments = createSelector(
parsedText => parsedText.segments parsedText => parsedText.segments
) )
export const selectCurrentSegmentIndex = createSelector( export const selectCurrentSegmentIndex = state => state.curIdx
state => state.curSegment,
curIdx => curIdx
)
export const selectCurrentSegment = createSelector( export const selectCurrentSegment = createSelector(
selectParsedText, selectParsedText,
state => state.curSegment, selectCurrentSegmentIndex,
(parsedText, curSegment) => parsedText.segments[curSegment] (parsedText, curIdx) => parsedText.segments[curIdx]
)
export const hasNextSegment = createSelector(
selectSegments,
selectCurrentSegmentIndex,
(segments, idx) => {
return idx < segments.length - 1
}
) )
// next/prev words // next/prev words
@ -35,7 +43,7 @@ export const selectWords = createSelector(
export const selectNextWord = createSelector( export const selectNextWord = createSelector(
selectWords, selectWords,
state => state.curSegment, selectCurrentSegmentIndex,
(words, curSegment) => getNextBiggerNumber(curSegment, words) (words, curSegment) => getNextBiggerNumber(curSegment, words)
) )
@ -46,7 +54,6 @@ export const selectPrevWord = createSelector(
) )
export const selectHasNextWord = createSelector(selectNextWord, Boolean) export const selectHasNextWord = createSelector(selectNextWord, Boolean)
export const selectHasPrevWord = createSelector(selectPrevWord, Boolean) export const selectHasPrevWord = createSelector(selectPrevWord, Boolean)
// next/prev sentences // next/prev sentences
@ -70,3 +77,7 @@ export const selectPrevSentence = createSelector(
export const selectHasNextSentence = createSelector(selectNextSentence, Boolean) export const selectHasNextSentence = createSelector(selectNextSentence, Boolean)
export const selectHasPrevSentence = createSelector(selectPrevSentence, Boolean) export const selectHasPrevSentence = createSelector(selectPrevSentence, Boolean)
export const selectRunning = state => state.running
export const selectWpm = state => state.wpm
export const selectInterval = createSelector(selectWpm, wpm => 60000 / wpm)

42
src/styles/IconButton.css Normal file
View File

@ -0,0 +1,42 @@
.svgButton {
display: inline-block;
border: none;
border-radius: 2px;
background: none;
cursor: pointer;
padding: 3px;
/* width: 1.4em; */
/* height: 1.4em; */
}
.svgButton svg {
outline: none;
transition: transform 0.1s linear;
vertical-align: middle;
}
.svgButton:focus {
outline: 2px dashed #17171d;
}
.svgButton:hover svg {
transform: scale(1.1);
}
.svgButton::-moz-focus-inner {
border: 0;
}
.svgButton[disabled] svg {
transform: scale(1.5);
color: '#c7c7c7';
}
.a11yLabel {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
white-space: nowrap;
}

22
src/styles/IconButton.js Normal file
View File

@ -0,0 +1,22 @@
import React from 'react'
import styles from './IconButton.css'
const SvgButton = ({ children, ...props }) => (
<button className={styles.svgButton} {...props}>
{children}
</button>
)
const AccessLabel = ({ children }) => (
<span className={styles.a11yLabel}>{children}</span>
)
export const IconButton = ({ Icon, title, ...props }) => {
return (
<SvgButton tabIndex="0" title={title} {...props}>
{Icon && <Icon aria-hidden="true" focusable="false" />}
<AccessLabel>{title}</AccessLabel>
</SvgButton>
)
}

View File

@ -1,25 +0,0 @@
import { Chapter } from './Chapter.js'
import { Player } from './Player.js'
export class Book {
constructor() {
this.chapters = [new Chapter('')]
this.curIdx = 0
this.player = new Player()
}
get currentChapter() {
return this.chapters(curIdx)
}
addChapter(chapter) {
this.chapters.push(chapter)
}
removeChapter(chapter) {
this.chapters.filter(el => el !== chapter)
if (this.curIdx > 0) {
this.curIdx--
}
}
}

View File

@ -1,139 +0,0 @@
import { parseText } from './textProcessing/parseText.js'
export class Chapter {
constructor(text, maxLength = -1) {
this._originalText = text
this._maxLength = maxLength
this.initSegments()
}
initSegments() {
let { segments, words, sentences } = parseText(this._text, this._maxLength)
this.segments = segments
this.words = words
this.sentences = sentences
this.resetIndex()
}
resetIndex() {
this.currentIdx = 0
}
setText(text) {
this._originalText = text
this.initSegments()
}
setMaxLength(maxLength) {
this._maxLength = maxLength
this.initSegments()
}
get currentSegment() {
return this.segments[this.currentIdx]
}
get metainfo() {
return {
segmentCount: this.segments.length,
wordsCount: this.words.length,
sentenceCount: this.sentences.length,
currentSegment: currentIdx + 1,
currentWord: -1,
currentSentence: -1
}
}
next() {
if (!this.hasNext()) return null
this.currentIdx += 1
return this.currentSegment
}
prev() {
if (!this.hasPrev()) return null
this.currentIdx -= 1
return this.currentSegment
}
nextWord() {
if (!this.hasNextWord()) return null
this.currentIdx = getNextBiggerNumber(this.currentIdx, this.words)
return this.currentSegment
}
prevWord() {
if (!this.hasPrevWord()) return null
this.currentIdx = getNextSmallerNumber(this.currentIdx, this.words)
return this.currentSegment
}
nextSentence() {
if (!this.hasNextSentence()) return null
this.currentIdx = getNextBiggerNumber(this.currentIdx, this.sentences)
return this.currentSegment
}
prevSentence() {
if (!this.hasPrevSentence()) return null
this.currentIdx = getNextSmallerNumber(this.currentIdx, this.sentences)
return this.currentSegment
}
hasNext() {
return this.currentIdx < this.segments.length - 1
}
hasPrev() {
return this.currentIdx > 0
}
hasNextWord() {
return this.currentIdx < lastEntry(this.words)
}
hasPrevWord() {
return this.currentIdx > this.words[0]
}
hasNextSentence() {
return this.currentIdx < lastEntry(this.sentences)
}
hasPrevSentence() {
return this.currentIdx > this.sentences[0]
}
}
/**
* Returns the next bigger number from a sorted Array of numbers.
* Returns null if num is the biggest number
* @param {Number} idx
* @param {Array<Number>} sortedArray
*/
function getNextBiggerNumber(num, sortedArray) {
for (let currentNumber of sortedArray) {
if (currentNumber > num) return currentNumber
}
return null
}
/**
* Returns the next smaller number from a sorted Array of numbers.
* Returns null if num is the smallest number
* @param {Number} idx
* @param {Array<Number>} sortedArray
*/
function getNextSmallerNumber(num, sortedArray) {
let reversedArray = [...sortedArray].reverse()
for (let currentNumber of reversedArray) {
if (currentNumber < num) return currentNumber
}
return null
}
function lastEntry(arr) {
return arr[arr.length - 1]
}
export const _privates = { getNextBiggerNumber }

View File

@ -1,45 +0,0 @@
export class Player {
constructor(interval = 200) {
this.intervalHandle = null
this.interval = interval
this.subscribers = {}
}
get playing() {
return this.intervalHandle !== null
}
start() {
clearInterval(this.intervalHandle)
this.intervalHandle = setInterval(this.tick.bind(this), this.interval)
}
stop() {
clearInterval(this.intervalHandle)
this.intervalHandle = null
}
toggle() {
if (this.playing) this.stop()
else this.start()
}
updateInterval(interval) {
this.interval = interval
if (this.intervalHandle) this.start()
}
tick() {
for (let callback of Object.values(this.subscribers)) {
callback()
}
}
subscribe(name, callback) {
this.subscribers[name] = callback
}
unsubscribe(name) {
delete subscribers[name]
}
}

View File

@ -1,70 +0,0 @@
import { RSVPPlayer } from './RSVPPlayer.js'
const initialOptions = {
maxLength: 10,
autoPlayNextChapter: false,
wordsPerMinute: 200,
pivotMethod: 'default',
pauseOnPunctuation: true
}
export class RSVPController {
constructor(options = {}) {
this.options = initialOptions
this.setOptions(options)
this.rsvpPlayer = new RSVPPlayer()
this.rsvpPlayer.onTick = this.onChange
this.applyOptions()
}
setOptions(options) {
this.options = { ...this.options, ...options }
}
applyOptions() {
let { options } = this
let { chapter, player } = this.rsvpPlayer
chapter.setMaxLength = options.maxLength
player.updateInterval(1000 / options.wordsPerMinute)
}
updateOptions(options) {
this.setOptions(options)
this.applyOptions()
this.onChange()
}
processAction(action, payload) {
let { chapter, player } = this.rsvpPlayer
switch (action) {
case 'prevSentence':
chapter.prevSentence()
break
case 'nextSentence':
chapter.nextSentence()
break
case 'prevWord':
chapter.prevWord()
break
case 'nextWord':
chapter.nextWord()
break
case 'playpause':
player.toggle()
break
case 'stop':
player.stop()
chapter.reset()
break
case 'load':
chapter = new Chapter(payload, 10)
}
this.onChange()
}
onChange() {
console.warn('RSVPController: onChange not set')
}
}

View File

@ -1,25 +0,0 @@
import { Player } from './Player.js'
import { Book } from './Book.js'
import { Chapter } from './Chapter.js'
export class RSVPPlayer {
constructor(maxLength) {
this.chapter = new Chapter(maxLength)
this.player = new Player()
this.player.subscribe('main', this.tick.bind(this))
}
tick() {
if (!this.chapter.hasNext()) {
this.player.stop()
} else {
this.chapter.next()
}
this.onTick()
}
onTick() {
console.warn('RSVPPlayer: onTick not set')
}
}

View File

@ -1,20 +1,92 @@
const path = require('path') const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const WebpackBar = require('webpackbar')
module.exports = { module.exports = (env, argv) => {
plugins: [new HtmlWebpackPlugin()], const isEnvProduction = argv.mode === 'production'
devtool: 'source-map', const isEnvDevelopment = argv.mode === 'development'
module: {
rules: [ return {
{ output: {
test: /.js$/, path: path.resolve(__dirname, 'build'),
exclude: /node_modules/, filename: 'static/js/[name].[contenthash:8].js',
loader: 'babel-loader' chunkFilename: 'static/js/[name].[contenthash:8].chunk.js'
},
devtool: isEnvProduction ? 'source-map' : 'cheap-module-source-map',
module: {
rules: [
// process javascript with babel
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
// process css. css modules are enabled.
{
test: /\.css$/,
use: [
isEnvDevelopment && 'style-loader',
isEnvProduction && MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: isEnvProduction
? '[hash:base64]'
: '[path][name]__[local]'
}
}
}
].filter(Boolean)
}
]
},
optimization: {
minimize: isEnvProduction,
splitChunks: {
chunks: 'all',
name: false
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
} }
] },
}, plugins: [
devServer: { // creat an index.html
quiet: true new HtmlWebpackPlugin(),
// show a progress bar
new WebpackBar(),
// create a report.html for bundle size
// extract css to css files
isEnvProduction &&
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
}),
// use cssnano to minify css
isEnvProduction &&
new OptimizeCssAssetsPlugin({
cssProcessorOptions: {
map: { inline: false, annotation: true }
}
}),
isEnvProduction &&
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
// hot reload not working right now
// isEnvDevelopment &&
// new ReactRefreshWebpackPlugin({ disableRefreshCheck: true })
].filter(Boolean),
devServer: {
stats: 'minimal'
}
} }
} }