diff --git a/src/components/main/Header/index.tsx b/src/components/main/Header/index.tsx index 496bff7a..6007e165 100644 --- a/src/components/main/Header/index.tsx +++ b/src/components/main/Header/index.tsx @@ -1,9 +1,8 @@ -import React, { FC, useCallback, useEffect } from 'react'; +import React, { FC, useCallback } from 'react'; import { connect } from 'react-redux'; import { push as historyPush } from 'connected-react-router'; import { Link } from 'react-router-dom'; import { Logo } from '~/components/main/Logo'; -import { Player } from '~/utils/player'; import * as style from './style.scss'; import { Filler } from '~/components/containers/Filler'; @@ -11,8 +10,11 @@ import { selectUser } from '~/redux/auth/selectors'; import { Group } from '~/components/containers/Group'; import * as MODAL_ACTIONS from '~/redux/modal/actions'; import { DIALOGS } from '~/redux/modal/constants'; +import { pick } from 'ramda'; -const mapStateToProps = selectUser; +const mapStateToProps = state => ({ + user: pick(['username', 'is_user'])(selectUser(state)), +}); const mapDispatchToProps = { push: historyPush, @@ -21,14 +23,10 @@ const mapDispatchToProps = { type IProps = ReturnType & typeof mapDispatchToProps & {}; -const HeaderUnconnected: FC = ({ username, is_user, showDialog }) => { +const HeaderUnconnected: FC = ({ user: { username, is_user }, showDialog }) => { const onLogin = useCallback(() => showDialog(DIALOGS.LOGIN), [showDialog]); const onOpenEditor = useCallback(() => showDialog(DIALOGS.EDITOR), [showDialog]); - useEffect(() => { - console.log({ Player }); - }, []); - return (
diff --git a/src/components/media/AudioPlayer/index.tsx b/src/components/media/AudioPlayer/index.tsx index dab96e83..ddb168a2 100644 --- a/src/components/media/AudioPlayer/index.tsx +++ b/src/components/media/AudioPlayer/index.tsx @@ -1,5 +1,73 @@ -import React from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { selectPlayer } from '~/redux/player/selectors'; +import * as PLAYER_ACTIONS from '~/redux/player/actions'; +import { IFile } from '~/redux/types'; +import { PLAYER_STATES } from '~/redux/player/constants'; +import { Player } from '~/utils/player'; -export const AudioPlayer = () =>
PLAYER
; +const mapStateToProps = state => ({ + player: selectPlayer(state), +}); -export default AudioPlayer; +const mapDispatchToProps = { + playerSetFile: PLAYER_ACTIONS.playerSetFile, + playerPlay: PLAYER_ACTIONS.playerPlay, + playerPause: PLAYER_ACTIONS.playerPause, +}; + +type Props = ReturnType & + typeof mapDispatchToProps & { + file: IFile; + }; + +const AudioPlayerUnconnected = ({ + file, + player: { file: current, status }, + + playerSetFile, + playerPlay, + playerPause, +}: Props) => { + const [playing, setPlaying] = useState(false); + const [progress, setProgress] = useState(0); + + const onPlay = useCallback(() => { + if (current && current.id === file.id) { + if (status === PLAYER_STATES.PLAYING) return playerPause(); + return playerPlay(); + } + + playerSetFile(file); + }, [file, current, status, playerPlay, playerPause, playerSetFile]); + + const onProgress = useCallback( + ({ detail }) => { + if (!detail || !detail.progress) return; + setProgress(detail.progress); + }, + [setProgress] + ); + + useEffect(() => { + const active = current && current.id === file.id; + setPlaying(current && current.id === file.id); + + if (active) Player.on('playprogress', onProgress); + + return () => { + if (active) Player.off('playprogress', onProgress); + }; + }, [file, current, setPlaying, onProgress]); + + return ( +
+ - {file.url} - {progress} - {playing && 'playing'} +
+ ); +}; + +export const AudioPlayer = connect( + mapStateToProps, + mapDispatchToProps +)(AudioPlayerUnconnected); diff --git a/src/components/node/Comment/index.tsx b/src/components/node/Comment/index.tsx index f5406a55..c66dda99 100644 --- a/src/components/node/Comment/index.tsx +++ b/src/components/node/Comment/index.tsx @@ -8,8 +8,7 @@ import assocPath from 'ramda/es/assocPath'; import append from 'ramda/es/append'; import reduce from 'ramda/es/reduce'; import { UPLOAD_TYPES } from '~/redux/uploads/constants'; -import { Player } from '~/utils/player'; -import classNames from 'classnames'; +import { AudioPlayer } from '~/components/media/AudioPlayer'; type IProps = HTMLAttributes & { is_empty?: boolean; @@ -65,16 +64,7 @@ const Comment: FC = ({ comment, is_empty, is_same, is_loading, className {groupped.audio && (
{groupped.audio.map(file => ( -
{ - Player.set(getURL(file)); - Player.load(); - Player.play(); - }} - > - {file.name} -
+ ))}
)} diff --git a/src/containers/main/MainLayout/index.tsx b/src/containers/main/MainLayout/index.tsx index 9862f05b..08bef189 100644 --- a/src/containers/main/MainLayout/index.tsx +++ b/src/containers/main/MainLayout/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { SidePane } from '~/components/main/SidePane'; import * as styles from './styles.scss'; import { Header } from '~/components/main/Header'; diff --git a/src/redux/player/actions.ts b/src/redux/player/actions.ts new file mode 100644 index 00000000..98c9a73c --- /dev/null +++ b/src/redux/player/actions.ts @@ -0,0 +1,20 @@ +import { IPlayerState } from './reducer'; +import { PLAYER_ACTIONS } from './constants'; + +export const playerSetFile = (file: IPlayerState['file']) => ({ + type: PLAYER_ACTIONS.SET_FILE, + file, +}); + +export const playerSetStatus = (status: IPlayerState['status']) => ({ + type: PLAYER_ACTIONS.SET_STATUS, + status, +}); + +export const playerPlay = () => ({ + type: PLAYER_ACTIONS.PLAY, +}); + +export const playerPause = () => ({ + type: PLAYER_ACTIONS.PAUSE, +}); diff --git a/src/redux/player/constants.ts b/src/redux/player/constants.ts new file mode 100644 index 00000000..5e7f808d --- /dev/null +++ b/src/redux/player/constants.ts @@ -0,0 +1,15 @@ +const prefix = 'PLAYER.'; + +export const PLAYER_ACTIONS = { + SET_FILE: `${prefix}SET_FILE`, + SET_STATUS: `${prefix}SET_STATUS`, + + PLAY: `${prefix}PLAY`, + PAUSE: `${prefix}PAUSE`, +}; + +export const PLAYER_STATES = { + PLAYING: 'PLAYING', + PAUSED: 'PAUSED', + UNSET: 'UNSET', +}; diff --git a/src/redux/player/handlers.ts b/src/redux/player/handlers.ts new file mode 100644 index 00000000..a83fb08e --- /dev/null +++ b/src/redux/player/handlers.ts @@ -0,0 +1,14 @@ +import { PLAYER_ACTIONS } from './constants'; +import assocPath from 'ramda/es/assocPath'; +import { playerSetFile, playerSetStatus } from './actions'; + +const setFile = (state, { file }: ReturnType) => + assocPath(['file'], file, state); + +const setStatus = (state, { status }: ReturnType) => + assocPath(['status'], status, state); + +export const PLAYER_HANDLERS = { + [PLAYER_ACTIONS.SET_FILE]: setFile, + [PLAYER_ACTIONS.SET_STATUS]: setStatus, +}; diff --git a/src/redux/player/reducer.ts b/src/redux/player/reducer.ts new file mode 100644 index 00000000..e6f26872 --- /dev/null +++ b/src/redux/player/reducer.ts @@ -0,0 +1,16 @@ +import { createReducer } from '~/utils/reducer'; +import { PLAYER_HANDLERS } from './handlers'; +import { PLAYER_STATES } from './constants'; +import { IFile } from '../types'; + +export type IPlayerState = Readonly<{ + status: typeof PLAYER_STATES[keyof typeof PLAYER_STATES]; + file: IFile; +}>; + +const INITIAL_STATE: IPlayerState = { + status: PLAYER_STATES.UNSET, + file: null, +}; + +export default createReducer(INITIAL_STATE, PLAYER_HANDLERS); diff --git a/src/redux/player/sagas.ts b/src/redux/player/sagas.ts new file mode 100644 index 00000000..050309d0 --- /dev/null +++ b/src/redux/player/sagas.ts @@ -0,0 +1,24 @@ +import { takeLatest } from 'redux-saga/effects'; +import { PLAYER_ACTIONS } from './constants'; +import { playerSetFile } from './actions'; +import { Player } from '~/utils/player'; +import { getURL } from '~/utils/dom'; + +function setFileSaga({ file }: ReturnType) { + Player.set(getURL(file)); + Player.play(); +} + +function playSaga() { + Player.play(); +} + +function pauseSaga() { + Player.pause(); +} + +export default function* playerSaga() { + yield takeLatest(PLAYER_ACTIONS.SET_FILE, setFileSaga); + yield takeLatest(PLAYER_ACTIONS.PAUSE, pauseSaga); + yield takeLatest(PLAYER_ACTIONS.PLAY, playSaga); +} diff --git a/src/redux/player/selectors.ts b/src/redux/player/selectors.ts new file mode 100644 index 00000000..21626c08 --- /dev/null +++ b/src/redux/player/selectors.ts @@ -0,0 +1,3 @@ +import { IState } from '~/redux/store'; + +export const selectPlayer = (state: IState) => state.player; diff --git a/src/redux/store.ts b/src/redux/store.ts index 9a4ed978..9bc610ea 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -19,6 +19,9 @@ import flowSaga from '~/redux/flow/sagas'; import uploadReducer, { IUploadState } from '~/redux/uploads/reducer'; import uploadSaga from '~/redux/uploads/sagas'; +import playerReducer, { IPlayerState } from '~/redux/player/reducer'; +import playerSaga from '~/redux/player/sagas'; + import { IAuthState } from '~/redux/auth/types'; import modalReducer, { IModalState } from '~/redux/modal/reducer'; @@ -36,6 +39,7 @@ export interface IState { node: INodeState; uploads: IUploadState; flow: IFlowState; + player: IPlayerState; } export const sagaMiddleware = createSagaMiddleware(); @@ -54,6 +58,7 @@ export const store = createStore( node: nodeReducer, uploads: uploadReducer, flow: flowReducer, + player: playerReducer, }), composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware)) ); @@ -63,6 +68,7 @@ export function configureStore(): { store: Store; persistor: Persistor } sagaMiddleware.run(nodeSaga); sagaMiddleware.run(uploadSaga); sagaMiddleware.run(flowSaga); + sagaMiddleware.run(playerSaga); const persistor = persistStore(store); diff --git a/src/utils/player.ts b/src/utils/player.ts index d4f9dc68..7e3c940a 100644 --- a/src/utils/player.ts +++ b/src/utils/player.ts @@ -1,3 +1,7 @@ +import { store } from '~/redux/store'; +import { playerSetStatus } from '~/redux/player/actions'; +import { PLAYER_STATES } from '~/redux/player/constants'; + type PlayerEventType = keyof HTMLMediaElementEventMap; type PlayerEventListener = ( @@ -38,7 +42,7 @@ export class PlayerClass { this.element.addEventListener(type, callback); }; - public off = (type: PlayerEventType, callback: PlayerEventListener) => { + public off = (type: string, callback) => { this.element.removeEventListener(type, callback); }; @@ -50,6 +54,10 @@ export class PlayerClass { this.element.play(); }; + public pause = () => { + this.element.pause(); + }; + public getDuration = () => { return this.element.currentTime; }; @@ -65,6 +73,9 @@ export class PlayerClass { const Player = new PlayerClass(); -Player.element.addEventListener('playprogress', ({ detail }: CustomEvent) => console.log(detail)); +// Player.element.addEventListener('playprogress', ({ detail }: CustomEvent) => console.log(detail)); + +Player.on('play', () => store.dispatch(playerSetStatus(PLAYER_STATES.PLAYING))); +Player.on('pause', () => store.dispatch(playerSetStatus(PLAYER_STATES.PAUSED))); export { Player };