Compare commits
No commits in common. "0db5d4340905cc06f3df1d46ef6ed751e1b8d499" and "294b66b93547eeff18c6e9fcc5315dc2b10daa2f" have entirely different histories.
0db5d43409
...
294b66b935
3
.babelrc
Normal file
3
.babelrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
@ -1,8 +0,0 @@
|
||||
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
3851
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -6,52 +6,29 @@
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --mode development",
|
||||
"build": "webpack --mode production",
|
||||
"build-fresh": "rm -rf build/ && npm run build",
|
||||
"test": "tests",
|
||||
"lint": "eslint src",
|
||||
"lint": "prettier --check src/**/*.js",
|
||||
"format": "prettier --write src/**/*.js"
|
||||
},
|
||||
"author": "Alfred Melch (dev@melch.pro)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-icons": "^3.8.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"redux": "^4.0.4",
|
||||
"reselect": "^4.0.0"
|
||||
"reselect": "^4.0.0",
|
||||
"styled-components": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.1",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"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",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"prettier": "^1.19.1",
|
||||
"react-refresh": "^0.7.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpackbar": "^4.0.0"
|
||||
"webpack-dev-server": "^3.9.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
require('postcss-preset-env'),
|
||||
require('postcss-normalize')
|
||||
]
|
||||
}
|
10
src/App.js
10
src/App.js
@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
import { TextInput } from './components/TextInput'
|
||||
import { TextOutput } from './components/TextOutput'
|
||||
import { RsvpReader } from './components/RsvpReader'
|
||||
|
||||
import { IconContext } from 'react-icons'
|
||||
|
||||
export const App = () => {
|
||||
export function App() {
|
||||
return (
|
||||
<IconContext.Provider value={{ color: 'grey', size: '1.4em' }}>
|
||||
<div>
|
||||
<RsvpReader></RsvpReader>
|
||||
</IconContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
18
src/components/MainControl.js
Normal file
18
src/components/MainControl.js
Normal file
@ -0,0 +1,18 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.mainItem {
|
||||
composes: item;
|
||||
flex: 2;
|
||||
}
|
@ -1,29 +1,32 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TextInput } from './TextInput'
|
||||
import { TextOutput } from './TextOutput'
|
||||
import { SegmentControl } from './SegmentControl'
|
||||
import { MainControl } from './MainControl'
|
||||
import { RsvpSegment } from './RsvpSegment'
|
||||
import { Options } from './Options'
|
||||
import { PlayerControl } from './PlayerControl'
|
||||
|
||||
import styles from './RsvpReader.css'
|
||||
const FlexRow = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const FlexItem = styled.div`
|
||||
flex: ${props => props.flex || 1};
|
||||
border: 1px solid black;
|
||||
`
|
||||
|
||||
export const RsvpReader = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.item}>
|
||||
<FlexRow>
|
||||
<FlexItem>
|
||||
<TextInput />
|
||||
</div>
|
||||
<div className={styles.mainItem}>
|
||||
</FlexItem>
|
||||
<FlexItem flex={2}>
|
||||
<RsvpSegment />
|
||||
<SegmentControl>
|
||||
<PlayerControl />
|
||||
</SegmentControl>
|
||||
<Options></Options>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<MainControl></MainControl>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<TextOutput />
|
||||
</div>
|
||||
</div>
|
||||
</FlexItem>
|
||||
</FlexRow>
|
||||
)
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
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 =
|
||||
'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.'
|
||||
|
||||
@ -15,7 +27,7 @@ export const TextInput = () => {
|
||||
defaultValue={text}
|
||||
onInput={e => setTextState(e.target.value)}
|
||||
></textarea>
|
||||
<button onClick={() => dispatch(setText(text))}>Load</button>
|
||||
<Button onClick={() => dispatch(setText(text))}>Load</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
.current {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sentenceBeginning {
|
||||
color: red;
|
||||
}
|
@ -1,39 +1,36 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import classNames from 'classnames'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { getNextSmallerNumber } from '../lib/array-util.js'
|
||||
import {
|
||||
selectParsedText,
|
||||
selectCurrentSegmentIndex
|
||||
} from '../store/selectors.js'
|
||||
import { selectParsedText } from '../store/selectors.js'
|
||||
|
||||
import styles from './TextOutput.css'
|
||||
const Segment = styled.span`
|
||||
color: ${props => (props.sentenceBeginning ? 'red' : 'black')};
|
||||
font-weight: ${props => (props.current ? 'bold' : 'normal')};
|
||||
`
|
||||
|
||||
const SelectedSegment = styled.span`
|
||||
font-weight: ;
|
||||
`
|
||||
|
||||
export const TextOutput = () => {
|
||||
const { segments, sentences, words } = useSelector(selectParsedText)
|
||||
const curSegment = useSelector(selectCurrentSegmentIndex)
|
||||
const parsedText = useSelector(selectParsedText)
|
||||
const curSegment = useSelector(state => state.curSegment)
|
||||
const { segments, words, sentences } = parsedText
|
||||
return (
|
||||
<div>
|
||||
{segments.map((segment, idx) => {
|
||||
const isCurrent = curSegment === idx
|
||||
const isWordStart = words.includes(idx)
|
||||
const wordBeginning = isWordStart
|
||||
? idx
|
||||
: getNextSmallerNumber(idx, words)
|
||||
const isSentenceStart = sentences.includes(wordBeginning)
|
||||
const current = curSegment === idx
|
||||
const sentenceBeginning = sentences.includes(idx)
|
||||
|
||||
return (
|
||||
<span
|
||||
<Segment
|
||||
key={idx}
|
||||
className={classNames({
|
||||
[styles.current]: isCurrent,
|
||||
[styles.sentenceBeginning]: isSentenceStart
|
||||
})}
|
||||
current={current}
|
||||
sentenceBeginning={sentenceBeginning}
|
||||
>
|
||||
{isWordStart && ' '}
|
||||
{segment}
|
||||
</span>
|
||||
{segment}{' '}
|
||||
</Segment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
@ -1,75 +0,0 @@
|
||||
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])
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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])
|
||||
}
|
@ -12,9 +12,10 @@ function createRootElement() {
|
||||
body.appendChild(root)
|
||||
return root
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
<App></App>
|
||||
</Provider>,
|
||||
createRootElement()
|
||||
)
|
||||
|
@ -25,6 +25,6 @@ export function getNextSmallerNumber(num, sortedArray) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function lastEntry(arr) {
|
||||
function lastEntry(arr) {
|
||||
return arr[arr.length - 1]
|
||||
}
|
||||
|
@ -1,16 +1,4 @@
|
||||
export const setText = text => ({ type: 'SET_TEXT', payload: text })
|
||||
export const resetSegment = () => ({ type: 'SET_CURRENT_SEGMENT', payload: 0 })
|
||||
export const incrementSegment = () => ({ type: 'INC_SEGMENT' })
|
||||
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' })
|
||||
export const incrementSegment = () => ({ type: 'INCREMENT_SEGMENT' })
|
||||
export const incrementWord = () => ({ type: 'INCREMENT_WORD' })
|
||||
export const incrementSentence = () => ({ type: 'INCREMENT_SENTENCE' })
|
||||
|
@ -1,37 +1,26 @@
|
||||
import {
|
||||
selectNextWord,
|
||||
selectNextSentence,
|
||||
selectPrevWord,
|
||||
selectPrevSentence
|
||||
} from './selectors'
|
||||
import { selectNextWord, selectNextSentence } from './selectors'
|
||||
|
||||
export const initialState = {
|
||||
originalText: 'Sample Text',
|
||||
curIdx: 0,
|
||||
maxLength: 5,
|
||||
isPlaying: false,
|
||||
wpm: 300,
|
||||
running: false
|
||||
curSegment: 0
|
||||
}
|
||||
|
||||
const reducer = {
|
||||
SET_TEXT: (state, payload) => ({
|
||||
...state,
|
||||
originalText: payload,
|
||||
curIdx: 0
|
||||
curSegment: 0
|
||||
}),
|
||||
SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curIdx: payload }),
|
||||
INC_SEGMENT: state => ({ ...state, curIdx: state.curIdx + 1 }),
|
||||
DEC_SEGMENT: state => ({ ...state, curIdx: state.curIdx - 1 }),
|
||||
INC_WORD: state => ({ ...state, curIdx: selectNextWord(state) }),
|
||||
DEC_WORD: state => ({ ...state, curIdx: selectPrevWord(state) }),
|
||||
INC_SENTENCE: state => ({ ...state, curIdx: selectNextSentence(state) }),
|
||||
DEC_SENTENCE: state => ({ ...state, curIdx: selectPrevSentence(state) }),
|
||||
SET_MAX_LENGTH: (state, payload) => ({ ...state, maxLength: payload }),
|
||||
SET_WPM: (state, payload) => ({ ...state, wpm: payload }),
|
||||
START: state => ({ ...state, running: true }),
|
||||
STOP: state => ({ ...state, running: false, curIdx: 0 }),
|
||||
PAUSE: state => ({ ...state, running: false })
|
||||
SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curSegment: payload }),
|
||||
INCREMENT_SEGMENT: state => ({
|
||||
...state,
|
||||
curSegment: state.curSegment + 1
|
||||
}),
|
||||
INCREMENT_WORD: state => ({ ...state, curSegment: selectNextWord(state) }),
|
||||
INCREMENT_SENTENCE: state => ({
|
||||
...state,
|
||||
curSegment: selectNextSentence(state)
|
||||
})
|
||||
}
|
||||
|
||||
export const reducerFn = (state, { type, payload }) => {
|
||||
|
@ -5,12 +5,9 @@ import { getNextBiggerNumber, getNextSmallerNumber } from '../lib/array-util.js'
|
||||
|
||||
// parsedText selectors
|
||||
|
||||
export const selectMaxLength = state => state.maxLength
|
||||
|
||||
export const selectParsedText = createSelector(
|
||||
state => state.originalText,
|
||||
selectMaxLength,
|
||||
(originalText, maxLength) => parseText(originalText, maxLength)
|
||||
originalText => parseText(originalText)
|
||||
)
|
||||
|
||||
export const selectSegments = createSelector(
|
||||
@ -18,20 +15,15 @@ export const selectSegments = createSelector(
|
||||
parsedText => parsedText.segments
|
||||
)
|
||||
|
||||
export const selectCurrentSegmentIndex = state => state.curIdx
|
||||
export const selectCurrentSegmentIndex = createSelector(
|
||||
state => state.curSegment,
|
||||
curIdx => curIdx
|
||||
)
|
||||
|
||||
export const selectCurrentSegment = createSelector(
|
||||
selectParsedText,
|
||||
selectCurrentSegmentIndex,
|
||||
(parsedText, curIdx) => parsedText.segments[curIdx]
|
||||
)
|
||||
|
||||
export const hasNextSegment = createSelector(
|
||||
selectSegments,
|
||||
selectCurrentSegmentIndex,
|
||||
(segments, idx) => {
|
||||
return idx < segments.length - 1
|
||||
}
|
||||
state => state.curSegment,
|
||||
(parsedText, curSegment) => parsedText.segments[curSegment]
|
||||
)
|
||||
|
||||
// next/prev words
|
||||
@ -43,7 +35,7 @@ export const selectWords = createSelector(
|
||||
|
||||
export const selectNextWord = createSelector(
|
||||
selectWords,
|
||||
selectCurrentSegmentIndex,
|
||||
state => state.curSegment,
|
||||
(words, curSegment) => getNextBiggerNumber(curSegment, words)
|
||||
)
|
||||
|
||||
@ -54,6 +46,7 @@ export const selectPrevWord = createSelector(
|
||||
)
|
||||
|
||||
export const selectHasNextWord = createSelector(selectNextWord, Boolean)
|
||||
|
||||
export const selectHasPrevWord = createSelector(selectPrevWord, Boolean)
|
||||
|
||||
// next/prev sentences
|
||||
@ -77,7 +70,3 @@ export const selectPrevSentence = createSelector(
|
||||
|
||||
export const selectHasNextSentence = createSelector(selectNextSentence, 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)
|
||||
|
@ -1,42 +0,0 @@
|
||||
.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;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
25
src/trash/Book.js
Normal file
25
src/trash/Book.js
Normal file
@ -0,0 +1,25 @@
|
||||
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--
|
||||
}
|
||||
}
|
||||
}
|
139
src/trash/Chapter.js
Normal file
139
src/trash/Chapter.js
Normal file
@ -0,0 +1,139 @@
|
||||
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 }
|
45
src/trash/Player.js
Normal file
45
src/trash/Player.js
Normal file
@ -0,0 +1,45 @@
|
||||
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]
|
||||
}
|
||||
}
|
70
src/trash/RSVPController.js
Normal file
70
src/trash/RSVPController.js
Normal file
@ -0,0 +1,70 @@
|
||||
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')
|
||||
}
|
||||
}
|
25
src/trash/RSVPPlayer.js
Normal file
25
src/trash/RSVPPlayer.js
Normal file
@ -0,0 +1,25 @@
|
||||
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')
|
||||
}
|
||||
}
|
@ -1,92 +1,20 @@
|
||||
const path = require('path')
|
||||
|
||||
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 = (env, argv) => {
|
||||
const isEnvProduction = argv.mode === 'production'
|
||||
const isEnvDevelopment = argv.mode === 'development'
|
||||
|
||||
return {
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: 'static/js/[name].[contenthash:8].js',
|
||||
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}`
|
||||
module.exports = {
|
||||
plugins: [new HtmlWebpackPlugin()],
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
// creat an index.html
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
devServer: {
|
||||
quiet: true
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user