From 4d414e5c20e123d46d6e8557251f01fad81301fb Mon Sep 17 00:00:00 2001 From: Artur Finger Date: Fri, 23 Jun 2023 14:11:04 +0300 Subject: [PATCH] DE-6498: Add Storybook Add Storybook as a framework to develop and document UI. So far this is groundwork, once the Storybook docs are finished, ./app folder can be deleted. * FIXES Several small fixes to some components such as `Duration` and `CurrentTime`, fixes to the UI and some improvements to the API. * FEATURES Create `PlayPauseIndicator`. Add CSS classes `.pp-ui-layers` and `.pp-ui-layer` intended to stack layers of UI on top of each other (e.g. player and its overlay). * TOOLING In Jest remove `transformIgnorePatterns` of `@castlabs/prestoplay` because it is not being transformed anyway, because it is getting mocked instead. --- .storybook/preview.ts | 2 + .storybook/style.css | 4 + CHANGELOG.md | 12 + app/src/App.tsx | 37 ++- app/src/youtube.css | 59 ---- jest.config.js | 4 +- jest.resolver.js | 31 +++ package-lock.json | 75 ++++-- package.json | 2 +- src/Player.ts | 34 ++- src/components/BaseThemeOverlay.tsx | 37 +-- src/components/CurrentTime.tsx | 9 +- src/components/Duration.tsx | 29 +- src/components/ForSize.tsx | 31 +++ src/components/HorizontalBar.tsx | 14 + src/components/Label.tsx | 3 + src/components/MenuSlidein.tsx | 11 +- src/components/MuteButton.tsx | 9 +- src/components/PlayPauseButton.tsx | 49 +--- src/components/PlayPauseIndicator.tsx | 31 +++ src/components/PlayerControls.tsx | 95 ++----- src/components/PlayerSurface.tsx | 9 + src/components/PosterImage.tsx | 6 + src/components/RateButton.tsx | 30 ++- src/components/RateText.tsx | 12 +- src/components/SeekButton.tsx | 4 + src/components/VerticalBar.tsx | 15 ++ src/components/VolumeBar.tsx | 3 +- src/index.ts | 1 + src/react.ts | 100 ++++++- src/services/controls.ts | 94 +++++++ src/themes/pp-ui-base-theme.css | 102 ++++++- story/stories/Intro.mdx | 35 ++- .../stories/components/BufferingIndicator.mdx | 4 +- story/stories/components/CurrentTime.mdx | 14 + .../components/CurrentTime.stories.tsx | 47 ++++ story/stories/components/Duration.mdx | 18 ++ story/stories/components/Duration.stories.tsx | 52 ++++ story/stories/components/FullscreenButton.mdx | 14 + .../components/FullscreenButton.stories.tsx | 36 +++ story/stories/components/HorizontalBar.mdx | 14 + .../components/HorizontalBar.stories.tsx | 33 +++ story/stories/components/Label.mdx | 14 + story/stories/components/Label.stories.tsx | 48 ++++ story/stories/components/MuteButton.mdx | 16 ++ .../stories/components/MuteButton.stories.tsx | 54 ++++ .../components/PlayPauseIndicator.stories.tsx | 44 +++ story/stories/components/PlayerControls.mdx | 15 ++ .../components/PlayerControls.stories.tsx | 48 ++++ story/stories/components/PlayerSurface.mdx | 6 +- .../components/PlayerSurface.stories.tsx | 30 +-- story/stories/components/PosterImage.mdx | 14 + .../components/PosterImage.stories.tsx | 46 ++++ story/stories/components/RateButton.mdx | 18 ++ .../stories/components/RateButton.stories.tsx | 84 ++++++ story/stories/components/RateText.mdx | 19 ++ story/stories/components/RateText.stories.tsx | 57 ++++ story/stories/components/SeekBar.mdx | 15 ++ story/stories/components/SeekBar.stories.tsx | 38 +++ story/stories/components/SeekButton.mdx | 17 ++ .../stories/components/SeekButton.stories.tsx | 60 +++++ story/stories/components/Spacer.mdx | 15 ++ story/stories/components/Spacer.stories.tsx | 38 +++ story/stories/components/StartButton.mdx | 16 ++ .../components/StartButton.stories.tsx | 43 +++ story/stories/components/Thumbnail.mdx | 15 ++ .../stories/components/Thumbnail.stories.tsx | 38 +++ story/stories/components/TimeLeft.mdx | 15 ++ story/stories/components/TimeLeft.stories.tsx | 38 +++ story/stories/components/VerticalBar.mdx | 15 ++ .../components/VerticalBar.stories.tsx | 40 +++ story/stories/components/VolumeBar.mdx | 15 ++ .../stories/components/VolumeBar.stories.tsx | 38 +++ story/stories/examples/DefaultSkin.mdx | 12 +- .../stories/examples/DefaultSkin.stories.tsx | 11 +- story/stories/examples/YoutubeSkin.mdx | 30 +++ .../stories/examples/YoutubeSkin.stories.tsx | 156 +++++++++++ story/stories/examples/YoutubeSkinSource.ts | 255 ++++++++++++++++++ story/stories/examples/youtubeStyle.ts | 124 +++++++++ story/stories/prep.tsx | 40 ++- types/presto.d.ts | 2 +- 81 files changed, 2490 insertions(+), 330 deletions(-) create mode 100644 jest.resolver.js create mode 100644 src/components/ForSize.tsx create mode 100644 src/components/PlayPauseIndicator.tsx create mode 100644 src/services/controls.ts create mode 100644 story/stories/components/CurrentTime.mdx create mode 100644 story/stories/components/CurrentTime.stories.tsx create mode 100644 story/stories/components/Duration.mdx create mode 100644 story/stories/components/Duration.stories.tsx create mode 100644 story/stories/components/FullscreenButton.mdx create mode 100644 story/stories/components/FullscreenButton.stories.tsx create mode 100644 story/stories/components/HorizontalBar.mdx create mode 100644 story/stories/components/HorizontalBar.stories.tsx create mode 100644 story/stories/components/Label.mdx create mode 100644 story/stories/components/Label.stories.tsx create mode 100644 story/stories/components/MuteButton.mdx create mode 100644 story/stories/components/MuteButton.stories.tsx create mode 100644 story/stories/components/PlayPauseIndicator.stories.tsx create mode 100644 story/stories/components/PlayerControls.mdx create mode 100644 story/stories/components/PlayerControls.stories.tsx create mode 100644 story/stories/components/PosterImage.mdx create mode 100644 story/stories/components/PosterImage.stories.tsx create mode 100644 story/stories/components/RateButton.mdx create mode 100644 story/stories/components/RateButton.stories.tsx create mode 100644 story/stories/components/RateText.mdx create mode 100644 story/stories/components/RateText.stories.tsx create mode 100644 story/stories/components/SeekBar.mdx create mode 100644 story/stories/components/SeekBar.stories.tsx create mode 100644 story/stories/components/SeekButton.mdx create mode 100644 story/stories/components/SeekButton.stories.tsx create mode 100644 story/stories/components/Spacer.mdx create mode 100644 story/stories/components/Spacer.stories.tsx create mode 100644 story/stories/components/StartButton.mdx create mode 100644 story/stories/components/StartButton.stories.tsx create mode 100644 story/stories/components/Thumbnail.mdx create mode 100644 story/stories/components/Thumbnail.stories.tsx create mode 100644 story/stories/components/TimeLeft.mdx create mode 100644 story/stories/components/TimeLeft.stories.tsx create mode 100644 story/stories/components/VerticalBar.mdx create mode 100644 story/stories/components/VerticalBar.stories.tsx create mode 100644 story/stories/components/VolumeBar.mdx create mode 100644 story/stories/components/VolumeBar.stories.tsx create mode 100644 story/stories/examples/YoutubeSkin.mdx create mode 100644 story/stories/examples/YoutubeSkin.stories.tsx create mode 100644 story/stories/examples/YoutubeSkinSource.ts create mode 100644 story/stories/examples/youtubeStyle.ts diff --git a/.storybook/preview.ts b/.storybook/preview.ts index b0c0268..edd8e88 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,3 +1,4 @@ +import { PrestoContextDecorator } from "../story/stories/prep"; import "./style.css" import type { Preview } from "@storybook/react"; @@ -16,6 +17,7 @@ const preview: Preview = { order: ['Intro'], }, }, + decorators: [PrestoContextDecorator], }; export default preview; diff --git a/.storybook/style.css b/.storybook/style.css index 62cfa8b..5401ee2 100644 --- a/.storybook/style.css +++ b/.storybook/style.css @@ -15,3 +15,7 @@ button.docblock-code-toggle { .sb-clpp-anchor:hover { color: #58baf6; } + +.css-79elbk { + display: none; +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cc303..a3bde36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Breaking changes +* Removed `showWhenDisabled` props from `PlayerControls`, it is no longer meaningful after the recent changes + to controls visibility. Now controls are always visible when video is paused or idle. * Removed the `player` prop from all components (except `PlayerSurface`). Instead of it, all components should be descendants of `PlayerSurface` and this way they get access to the `player` instance from its context. ```jsx @@ -36,9 +38,19 @@ ## Fixes +* Fixes to `BaseThemeOverlay`: + * Before start of playback make sure `CurrentTime` displays a valid value of `0:00`. + * Make sure seek bar is visible. + * Improve responsiveness of the UI. * Improve positioning by `HoverContainer`. * All components now accept `style` and `className` prop and apply them to their top-most element. * Removed `children` prop from components that do not render any child components. +* Fix `Duration` to display the correct value even when it is rendered after the video has been loaded. + +## Changes + +* Player controls auto-hide after 3s instead of 5s. +* Added `mode` prop to `PlayerControls` which can be used to configure the visibility of player controls. # 0.6.0 (Beta) diff --git a/app/src/App.tsx b/app/src/App.tsx index 1dcda0b..e13ff5b 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -3,7 +3,6 @@ import React, { useMemo, useState, } from 'react' -import ClickAway from 'react-hook-click-away' import { Asset, TestAssets } from './Asset' import { BasicOverlayPage } from './BasicOverlayPage' @@ -78,26 +77,24 @@ export function App() {

PRESTOplay React Components

- setNavVisible(false)}> -
{page} diff --git a/app/src/youtube.css b/app/src/youtube.css index 1b86708..a2c6fd0 100644 --- a/app/src/youtube.css +++ b/app/src/youtube.css @@ -105,65 +105,6 @@ position: relative; } -.pp-yt-center-background { - position: absolute; - top: 50%; - left: 50%; - right: 50%; - bottom: 50%; - width: 64px; - height: 64px; - - transform: translate(-50%,-50%); - z-index: -1; - border-radius: 32px; - background-color: rgba(0, 0, 0, .25); -} - -.pp-yt-center-toggle .pp-ui-playpause-toggle { - opacity: 0; -} - -.pp-yt-center-toggle.pp-ui-playpause-toggle-pause { - opacity: 0; - animation-name: center-fade; - animation-duration: 500ms; - animation-direction: normal; - animation-fill-mode: forwards; - animation-timing-function: ease; -} - -.pp-yt-center-toggle.pp-ui-playpause-toggle-play { - opacity: 0; - animation-name: center-fade-2; - animation-duration: 500ms; - animation-direction: normal; - animation-fill-mode: forwards; - animation-timing-function: ease; -} - -@keyframes center-fade { - from { - opacity: .8; - scale: .25; - } - to { - opacity: 0; - scale: 1.75; - } -} - - -@keyframes center-fade-2 { - from { - opacity: .8; - scale: .25; - } - to { - opacity: 0; - scale: 1.75; - } -} .pp-yt-gradient-bottom { height: 146px; diff --git a/jest.config.js b/jest.config.js index bcd696c..da41629 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,8 +5,8 @@ module.exports = { }, testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", testEnvironment: 'jsdom', - transformIgnorePatterns: ['/node_modules/(?!(@castlabs/prestoplay)/)'], moduleNameMapper: { '@castlabs/prestoplay.*': '/tests/fake_clpp.js', - } + }, + resolver: '/jest.resolver.js', } diff --git a/jest.resolver.js b/jest.resolver.js new file mode 100644 index 0000000..374ceb4 --- /dev/null +++ b/jest.resolver.js @@ -0,0 +1,31 @@ +/** + * Imports from @react-hook are for some reason not resolved correctly, and they end up + * importing ESM code instead of transpiled ES5 which causes a crash of the test suite. + * Fix it by correcting those paths here. + * + * (The package.json.exports field in @react-hook/ is the problem. + * Ideally I would fix this via the `options.packageFilter`, but that + * for some reason is not working - possibly a bug in Jest. So I'm just + * doing a simple replace here instead.) + */ +function fixReactHookPaths(path) { + const replacements = { + '@react-hook/resize-observer/dist/module/index.js': '@react-hook/resize-observer/dist/main/index.js', + '@react-hook/passive-layout-effect/dist/module/index.js': '@react-hook/passive-layout-effect/dist/main/index.js', + '@react-hook/latest/dist/module/index.js': '@react-hook/latest/dist/main/index.js', + } + + Object.keys(replacements).forEach(key => { + path = path.replace(key, replacements[key]) + }) + + return path +} + +/** +* https://jestjs.io/docs/configuration/#resolver-string +*/ +module.exports = (path, options) => { + let result = options.defaultResolver(path, options) + return fixReactHookPaths(result) +} diff --git a/package-lock.json b/package-lock.json index 3d32f41..f242c2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.6.0", "license": "Apache-2.0", "dependencies": { - "react-hook-click-away": "^1.0.0" + "@react-hook/resize-observer": "^1.2.6" }, "devDependencies": { "@babel/preset-env": "^7.22.4", @@ -2956,7 +2956,6 @@ }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", - "dev": true, "license": "Apache-2.0" }, "node_modules/@mdx-js/react": { @@ -3130,6 +3129,35 @@ "node": ">= 8" } }, + "node_modules/@react-hook/latest": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", + "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@react-hook/passive-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", + "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@react-hook/resize-observer": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz", + "integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==", + "dependencies": { + "@juggle/resize-observer": "^3.3.1", + "@react-hook/latest": "^1.0.2", + "@react-hook/passive-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "23.0.2", "dev": true, @@ -13220,7 +13248,6 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -13581,7 +13608,6 @@ }, "node_modules/loose-envify": { "version": "1.4.0", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -15586,7 +15612,6 @@ }, "node_modules/react": { "version": "18.2.0", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -15698,11 +15723,6 @@ "react": ">=16.3.0" } }, - "node_modules/react-hook-click-away": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/react-hook-click-away/-/react-hook-click-away-1.0.0.tgz", - "integrity": "sha512-bPvigfb6lQ0LbBNnrmPK+Hf/ud+qr/qs2JQJsGxuGgo9MYAjL+QF1KhTLpQzsVcPihCOuhrzXGtphA/VuC1LKw==" - }, "node_modules/react-inspector": { "version": "6.0.1", "dev": true, @@ -20184,8 +20204,7 @@ } }, "@juggle/resize-observer": { - "version": "3.4.0", - "dev": true + "version": "3.4.0" }, "@mdx-js/react": { "version": "2.3.0", @@ -20276,6 +20295,28 @@ } } }, + "@react-hook/latest": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", + "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", + "requires": {} + }, + "@react-hook/passive-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz", + "integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==", + "requires": {} + }, + "@react-hook/resize-observer": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz", + "integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==", + "requires": { + "@juggle/resize-observer": "^3.3.1", + "@react-hook/latest": "^1.0.2", + "@react-hook/passive-layout-effect": "^1.2.0" + } + }, "@rollup/plugin-commonjs": { "version": "23.0.2", "dev": true, @@ -26939,8 +26980,7 @@ "dev": true }, "js-tokens": { - "version": "4.0.0", - "dev": true + "version": "4.0.0" }, "js-yaml": { "version": "3.14.1", @@ -27177,7 +27217,6 @@ }, "loose-envify": { "version": "1.4.0", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -28384,7 +28423,6 @@ }, "react": { "version": "18.2.0", - "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -28462,11 +28500,6 @@ "react-side-effect": "^2.1.0" } }, - "react-hook-click-away": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/react-hook-click-away/-/react-hook-click-away-1.0.0.tgz", - "integrity": "sha512-bPvigfb6lQ0LbBNnrmPK+Hf/ud+qr/qs2JQJsGxuGgo9MYAjL+QF1KhTLpQzsVcPihCOuhrzXGtphA/VuC1LKw==" - }, "react-inspector": { "version": "6.0.1", "dev": true, diff --git a/package.json b/package.json index 9703968..828cb2d 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,6 @@ "react" ], "dependencies": { - "react-hook-click-away": "^1.0.0" + "@react-hook/resize-observer": "^1.2.6" } } diff --git a/src/Player.ts b/src/Player.ts index 2532e64..59c3327 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,6 +1,7 @@ import { clpp } from '@castlabs/prestoplay' import { EventEmitter, EventListener, EventType } from './EventEmitter' +import { Controls, ControlsVisibilityMode } from './services/controls' import { fromPrestoTrack, getAbrTrack, @@ -341,11 +342,16 @@ export class Player { */ private _configLoaded = false + private _controls = new Controls() + constructor(initializer?: PlayerInitializer) { this._initializer = initializer this._actionQueuePromise = new Promise((resolve) => { this._actionQueueResolved = resolve }) + this._controls.onChange = (visible) => { + this.emitUIEvent('controlsVisible', visible) + } } /** @@ -404,6 +410,12 @@ export class Player { if (isEnabledState(currentState) !== isEnabledState(previousState)) { this.emitUIEvent('enabled', isEnabledState(currentState)) } + + if (!isEnabledState(currentState) || currentState === State.Paused) { + this._controls.pin() + } else { + this._controls.unpin() + } }) this.pp_.on('timeupdate', () => { @@ -511,7 +523,7 @@ export class Player { } get duration(): number { - return this.pp_ ? this.pp_.getDuration() : 0 + return (this.pp_ && this.pp_.getDuration() > 0) ? this.pp_.getDuration() : 0 } get live(): boolean { @@ -638,16 +650,20 @@ export class Player { get controlsVisible(): boolean { - return this._controlsVisible + return this._controls.visible } set controlsVisible(value: boolean) { - if (value !== this._controlsVisible) { - this._controlsVisible = value - this.emitUIEvent('controlsVisible', value) - } + this._controls.setVisible(value) } + set controlsAutoHideDelayMs(value: number) { + this._controls.hideDelayMs = value + } + + set controlsVisibilityMode(value: ControlsVisibilityMode) { + this._controls.mode = value + } get slideInMenuVisible(): boolean { return this._slideInMenuVisible @@ -658,6 +674,8 @@ export class Player { this._slideInMenuVisible = value this.emitUIEvent('slideInMenuVisible', value) } + + this.controlsVisible = !value } /** @@ -668,6 +686,10 @@ export class Player { */ surfaceInteraction() { this.emitUIEvent('surfaceInteraction', undefined) + + if (!this.slideInMenuVisible) { + this.controlsVisible = true + } } diff --git a/src/components/BaseThemeOverlay.tsx b/src/components/BaseThemeOverlay.tsx index 48e19dd..e1265cf 100644 --- a/src/components/BaseThemeOverlay.tsx +++ b/src/components/BaseThemeOverlay.tsx @@ -3,6 +3,7 @@ import React from 'react' import { BufferingIndicator } from './BufferingIndicator' import { CurrentTime } from './CurrentTime' import { Duration } from './Duration' +import { ForSize } from './ForSize' import { FullscreenButton } from './FullscreenButton' import { HorizontalBar } from './HorizontalBar' import { Label } from './Label' @@ -122,26 +123,32 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
- - - - {props.seekBar === 'none' ? null : } + + + +
- + + {props.seekBar === 'none' ? null : } +
- -
diff --git a/src/components/CurrentTime.tsx b/src/components/CurrentTime.tsx index eb0a48b..2c0f5b9 100644 --- a/src/components/CurrentTime.tsx +++ b/src/components/CurrentTime.tsx @@ -20,6 +20,13 @@ import type { export interface CurrentTimeProps extends BaseComponentProps { disableHoveringDisplay?: boolean children?: React.ReactNode + /** + * Time in seconds. + * + * By default you should leave this `undefined` and let the component + * display the real the current time of the player. + */ + seconds?: number } /** @@ -27,7 +34,7 @@ export interface CurrentTimeProps extends BaseComponentProps { */ export const CurrentTime = (props: CurrentTimeProps) => { const { player } = useContext(PrestoContext) - const [currentTime, setCurrentTime] = useState('') + const [currentTime, setCurrentTime] = useState(timeToString(props.seconds ?? 0, getMinimalFormat(0))) const [isHovering, setHovering] = useState(false) const hoveringRef = useRef() const enabledClass = usePrestoEnabledStateClass() diff --git a/src/components/Duration.tsx b/src/components/Duration.tsx index 15ba6bc..8cc2aa2 100644 --- a/src/components/Duration.tsx +++ b/src/components/Duration.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react' +import React from 'react' -import { usePrestoEnabledStateClass, usePrestoUiEvent } from '../react' +import { useDuration, usePrestoEnabledStateClass } from '../react' import { getMinimalFormat, timeToString, @@ -11,30 +11,35 @@ import { BaseComponentProps, } from './types' +const toString = (duration: number) => { + if (duration === Infinity) { + return 'Live' + } + return timeToString(duration, getMinimalFormat(duration)) +} export interface DurationProps extends BaseComponentProps { children?: React.ReactNode + /** + * Time in seconds. + * + * By default you should leave this `undefined` and let the component + * display the real duration of the video. + */ + seconds?: number } /** * Duration. */ export const Duration = (props: DurationProps) => { - const [duration, setDuration] = useState('') + const duration = useDuration() const enabledClass = usePrestoEnabledStateClass() - usePrestoUiEvent('durationchange', (duration) => { - if (duration === Infinity) { - setDuration('Live') - } else { - setDuration(timeToString(duration, getMinimalFormat(duration))) - } - }) - return (
) } + +export const HorizontalBarStory = (props: HorizontalBarProps) => { + const style = { + border: '1px dashed white', + padding: '0 20px', + } + return ( + +
Item 1
+
Item 2
+
Item 3
+
+ ) +} diff --git a/src/components/Label.tsx b/src/components/Label.tsx index 0fea69d..94dae19 100644 --- a/src/components/Label.tsx +++ b/src/components/Label.tsx @@ -3,6 +3,9 @@ import React, { FC } from 'react' import type { BaseComponentProps } from './types' export interface LabelProps extends BaseComponentProps { + /** + * The text to display. + */ label?: string children?: React.ReactNode testId?: string diff --git a/src/components/MenuSlidein.tsx b/src/components/MenuSlidein.tsx index 429fdd8..05274a9 100644 --- a/src/components/MenuSlidein.tsx +++ b/src/components/MenuSlidein.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react' -import { useClickAway } from 'react-hook-click-away' +import React, { useContext, useEffect, useRef, useState } from 'react' import { PrestoContext } from '../context/PrestoContext' import { usePrestoUiEvent } from '../react' @@ -55,14 +54,6 @@ export const MenuSlidein = (props: MenuSlideinProps) => { const [videoListVisible, setVideoListVisible] = useState(false) const ref = useRef(null) - const hide = useCallback(() => { - player.slideInMenuVisible = false - setVisible(false) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useClickAway(ref, hide) - usePrestoUiEvent('slideInMenuVisible', (visible) => { setVisible(visible) }) diff --git a/src/components/MuteButton.tsx b/src/components/MuteButton.tsx index a1f8437..b32d4e9 100644 --- a/src/components/MuteButton.tsx +++ b/src/components/MuteButton.tsx @@ -10,6 +10,13 @@ import type { BasePlayerComponentButtonProps } from './types' export interface MuteButtonProps extends BasePlayerComponentButtonProps { children?: React.ReactNode + /** + * Whether audio is muted or not. + * + * By default you should leave this `undefined` and let the component + * display the real mute state of the player. + */ + muted?: boolean } /** @@ -37,7 +44,7 @@ export const MuteButton = (props: MuteButtonProps) => { {props.children} diff --git a/src/components/PlayPauseButton.tsx b/src/components/PlayPauseButton.tsx index b8333df..e26b3fa 100644 --- a/src/components/PlayPauseButton.tsx +++ b/src/components/PlayPauseButton.tsx @@ -1,9 +1,7 @@ -import { clpp } from '@castlabs/prestoplay' -import React, { useContext, useDebugValue, useState } from 'react' +import React, { useContext } from 'react' import { PrestoContext } from '../context/PrestoContext' -import { Player, State } from '../Player' -import { usePrestoUiEvent } from '../react' +import { useIsPlaying } from '../react' import { BaseButton } from './BaseButton' @@ -24,54 +22,13 @@ export interface PlayPauseButtonProps extends BasePlayerComponentButtonProps { children?: React.ReactNode } -type Config = { - player: Player - state: State - resetRate: boolean - reason?: clpp.events.BufferingReasons -} - -function isPlayingState(config: Config): boolean { - const { player, state, resetRate, reason } = config - - if (state === State.Buffering && reason === clpp.events.BufferingReasons.SEEKING) { - return player.playing - } - - if (state !== State.Playing) { - return false - } - - if (resetRate && player.rate !== 1) { - return false - } - - return true -} - -const useIsPlaying = (player: Player, resetRate: boolean): boolean => { - const [isPlaying, setIsPlaying] = useState(isPlayingState({ state: player.state, player, resetRate })) - - usePrestoUiEvent('ratechange', () => { - setIsPlaying(isPlayingState({ state: player.state, player, resetRate })) - }) - - usePrestoUiEvent('statechanged', ({ currentState, reason }) => { - setIsPlaying(isPlayingState({ state: currentState, player, resetRate, reason })) - }) - - useDebugValue(isPlaying ? 'playing' : 'not playing') - - return isPlaying -} - /** * The play / pause toggle button. */ export const PlayPauseButton = (props: PlayPauseButtonProps) => { const { resetRate } = props const { player } = useContext(PrestoContext) - const isPlaying = useIsPlaying(player, resetRate ?? false) + const isPlaying = useIsPlaying(resetRate ?? false) const toggle = () => { if (resetRate && player.rate !== 1) { diff --git a/src/components/PlayPauseIndicator.tsx b/src/components/PlayPauseIndicator.tsx new file mode 100644 index 0000000..32a780f --- /dev/null +++ b/src/components/PlayPauseIndicator.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +import { useIsPlaying } from '../react' + +type Props = { + className?: string + /** + * Specify this to override playing state + */ + isPlaying?: boolean +} + +/** + * Visual indicator of play / pause event. + */ +export const PlayPauseIndicator = (props: Props) => { + const isPlaying = useIsPlaying() + + const className = `pp-ui-playpause-indicator pp-ui-playpause-toggle pp-ui-state-${props.isPlaying ?? isPlaying ? 'pause' : 'play'}` + +` pp-ui-circle-bg ${props.className || ''}` + + return ( +
+
+
+ +
+
+
+ ) +} diff --git a/src/components/PlayerControls.tsx b/src/components/PlayerControls.tsx index b0648e5..adb23d3 100644 --- a/src/components/PlayerControls.tsx +++ b/src/components/PlayerControls.tsx @@ -6,7 +6,8 @@ import React, { } from 'react' import { PrestoContext } from '../context/PrestoContext' -import { usePrestoUiEvent } from '../react' +import { useControlsVisible } from '../react' +import { ControlsVisibilityMode } from '../services/controls' import { focusElement, focusNextElement, @@ -15,12 +16,25 @@ import { import type { BaseComponentProps } from './types' -const DEFAULT_HIDE_DELAY = 5 -const debug = false - export interface PlayerControlsProps extends BaseComponentProps { + /** + * Time in milliseconds after which the controls will be automatically hidden. + * This applies only when the mode is set to 'auto'. + * + * Defaults to 3000. + */ hideDelay?: number - showWhenDisabled?: boolean + /** + * Visibility mode of the content. If set to 'auto' the content appears + * based on user interaction with the player or when the player is paused, + * and it automatically hides after the specified delay. + * + * Defaults to 'auto'. + */ + mode?: ControlsVisibilityMode + /** + * Content to display. This is intended to be used for player controls. + */ children?: React.ReactNode } @@ -29,67 +43,21 @@ export interface PlayerControlsProps extends BaseComponentProps { * A horizontal area component that contains player controls. */ export const PlayerControls = (props: PlayerControlsProps) => { - const { player } = useContext(PrestoContext) - const [controlsVisible, setControlsVisible_] = useState( - player.controlsVisible || (props.showWhenDisabled && !player.enabled)) const [lastFocusIndex, setLastFocusIndex] = useState(-1) + const { player } = useContext(PrestoContext) + const controlsVisible = useControlsVisible() - const timer = useRef|null>(null) - const ref = useRef(null) - - const setControlsVisible = (visible: boolean, fromUiEvent = false) => { - if (!fromUiEvent) { - player.controlsVisible = visible - } - setControlsVisible_(visible) - } - - usePrestoUiEvent('controlsVisible', (visible) => { - setControlsVisible(visible, true) - if (visible) { - createTimer() - } - }) - - usePrestoUiEvent('slideInMenuVisible', (visible) => { - setControlsVisible(!visible) - if (!visible) { - createTimer() - } - }) - - usePrestoUiEvent('surfaceInteraction', () => { - if (!player.slideInMenuVisible) { - setControlsVisible(true) - createTimer() - } - }) - - const interactionTimerCallback = () => { - setControlsVisible(false) - } - - function createTimer(): void { - if (timer.current) { - clearTimeout(timer.current) - timer.current = null - } - if (controlsVisible) { - timer.current = setTimeout(interactionTimerCallback, (props.hideDelay || DEFAULT_HIDE_DELAY) * 1000) + useEffect(() => { + if (props.hideDelay) { + player.controlsAutoHideDelayMs = props.hideDelay } - } + }, [player, props.hideDelay]) useEffect(() => { - createTimer() + player.controlsVisibilityMode = props.mode ?? 'auto' + }, [player, props.mode]) - return () => { - if (timer.current) { - clearTimeout(timer.current) - timer.current = null - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + const ref = useRef(null) useEffect(() => { const onFocusIn = () => { @@ -125,17 +93,12 @@ export const PlayerControls = (props: PlayerControlsProps) => { } }) - const mouseMove = () => { - createTimer() - } - return (
{props.children}
diff --git a/src/components/PlayerSurface.tsx b/src/components/PlayerSurface.tsx index f3b88e6..8a4228f 100644 --- a/src/components/PlayerSurface.tsx +++ b/src/components/PlayerSurface.tsx @@ -55,6 +55,15 @@ const getContext = (nullableContext: Partial) => { .every(value => value != null) ? nullableContext as PrestoContextType : null } +/** + * Stub for storybook + */ +export const PlayerSurfaceForStory = (props: PlayerProps) => { + return
+ {props.children} +
+} + /** * Player Surface. diff --git a/src/components/PosterImage.tsx b/src/components/PosterImage.tsx index de65134..6c11d05 100644 --- a/src/components/PosterImage.tsx +++ b/src/components/PosterImage.tsx @@ -6,7 +6,13 @@ import { usePrestoUiEvent } from '../react' import type { BaseComponentProps } from './types' export interface PosterImageProps extends BaseComponentProps { + /** + * Poster image source URL. + */ src: string + /** + * Alternative description of the image. + */ alt?: string } diff --git a/src/components/RateButton.tsx b/src/components/RateButton.tsx index 5767726..54796fe 100644 --- a/src/components/RateButton.tsx +++ b/src/components/RateButton.tsx @@ -8,9 +8,26 @@ import { BaseButton } from './BaseButton' import type { BasePlayerComponentButtonProps } from './types' -export interface RateButtonProps extends BasePlayerComponentButtonProps{ +export interface RateButtonProps extends BasePlayerComponentButtonProps { + /** + * Playback rate/speed factor. + * + * e.g. 2 to play twice as fast, or 0.5 to play half as fast. + * + * Defaults to 2. + */ factor?: number + /** + * Maximum allowed playback rate. + * + * Defaults to 64. + */ max?: number + /** + * Minimum allowed playback rate. + * + * Defaults to 0.5. + */ min?: number children?: React.ReactNode } @@ -22,16 +39,21 @@ export interface RateButtonProps extends BasePlayerComponentButtonProps{ export const RateButton = (props: RateButtonProps) => { const { player } = useContext(PrestoContext) const enabled = usePrestoEnabledState() - + + const max = props.max ?? 64 + const min = props.min ?? 0.5 + const factor = props.factor ?? 2 + function adjustRate() { - player.rate = Math.min(props.max || 64, Math.max(props.min || 0.5, player.rate * (props.factor || 2))) + const newRate = player.rate * factor + player.rate = Math.min(max, Math.max(min, newRate)) } return ( {props.children} diff --git a/src/components/RateText.tsx b/src/components/RateText.tsx index 017cf1f..389c650 100644 --- a/src/components/RateText.tsx +++ b/src/components/RateText.tsx @@ -9,8 +9,18 @@ import type { BaseComponentProps } from './types' export interface RateTextProps extends BaseComponentProps { children?: React.ReactNode + /** + * Playback rate. + * + * By default you should leave this `undefined` and let the component + * display the real playback rate of the video. + */ + rate?: number } +/** + * A component that displays the current playback rate. + */ export const RateText = (props: RateTextProps) => { const [rate, setRate] = useState(1) const enabledClass = usePrestoEnabledStateClass() @@ -24,7 +34,7 @@ export const RateText = (props: RateTextProps) => { return (