UNPKG

@microlink/vanilla

Version:

Turn links into beautiful previews.

1,787 lines (1,720 loc) 68.5 kB
import React, { useContext, forwardRef, createElement, useRef, useMemo, useCallback, useLayoutEffect, useState, useEffect } from 'react'; import { css, styled, keyframes } from 'styled-components'; import { fetchFromApi, getApiUrl as getApiUrl$1 } from '@microlink/mql'; import { createRoot } from 'react-dom'; function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function toPropertyKey(t) { var i = toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _defineProperty(e, r, t) { return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; } function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; } function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; } const _excluded$8 = ["accessibility", "debounce", "ellipsis", "is", "lines", "text"]; function ownKeys$4(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$4(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$4(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$4(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } const i = _ref => { let { accessibility: i = true, debounce: u = 300, ellipsis: c = "…", is: o = "div", lines: s = 3, text: f } = _ref, d = _objectWithoutProperties(_ref, _excluded$8); const v = useRef(null), a = useRef("."), m = _objectSpread$4(_objectSpread$4({ ref: v }, i ? { title: f } : {}), d), b = useMemo(() => "string" == typeof f && f.length > 0, [f]), g = useCallback(() => { if (!b) return; const t = t => { a.current = t, null != v.current && (v.current.textContent = t); }, e = () => { var t, e; return null !== (e = null === (t = v.current) || void 0 === t ? void 0 : t.clientHeight) && void 0 !== e ? e : 0; }; t("."); const r = (e() + 1) * s + 1; if (t(f), e() <= r) return; let n = 0, l = 0, i = f.length; for (; n <= i;) { l = Math.floor((n + i) / 2); t(f.slice(0, l).trim() + c), e() <= r ? n = l + 1 : i = l - 1; } t(f.slice(0, l - 1).trim() + c); }, [c, b, s, f]); return useLayoutEffect(() => { if (g(), null == v.current) return; const t = new ResizeObserver(((t, e) => { let r; const n = () => { r = void 0, t(); }; return () => { const l = null == r; clearTimeout(r), r = setTimeout(n, e), l && t(); }; })(g, u)); return t.observe(v.current), () => t.disconnect(); }, [g, u]), b ? /*#__PURE__*/createElement(o, m, a.current) : null; }; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var ipv4 = {exports: {}}; var hasRequiredIpv4; function requireIpv4 () { if (hasRequiredIpv4) return ipv4.exports; hasRequiredIpv4 = 1; const IP_RANGES = [ // 10.0.0.0 - 10.255.255.255 /^(:{2}f{4}:)?10(?:\.\d{1,3}){3}$/, // 127.0.0.0 - 127.255.255.255 /^(:{2}f{4}:)?127(?:\.\d{1,3}){3}$/, // 169.254.1.0 - 169.254.254.255 /^(::f{4}:)?169\.254\.([1-9]|1?\d\d|2[0-4]\d|25[0-4])\.\d{1,3}$/, // 172.16.0.0 - 172.31.255.255 /^(:{2}f{4}:)?(172\.1[6-9]|172\.2\d|172\.3[01])(?:\.\d{1,3}){2}$/, // 192.168.0.0 - 192.168.255.255 /^(:{2}f{4}:)?192\.168(?:\.\d{1,3}){2}$/, // fc00::/7 /^f[cd][\da-f]{2}(::1$|:[\da-f]{1,4}){1,7}$/, // fe80::/10s /^fe[89ab][\da-f](::1$|:[\da-f]{1,4}){1,7}$/, // localhost in IPv4 /^localhost$|^0\.0\.0\.0$/]; const regex = new RegExp(`^(${IP_RANGES.map(re => re.source).join('|')})$`); ipv4.exports = regex.test.bind(regex); ipv4.exports.regex = regex; return ipv4.exports; } var ipv6 = {exports: {}}; var hasRequiredIpv6; function requireIpv6 () { if (hasRequiredIpv6) return ipv6.exports; hasRequiredIpv6 = 1; const IP_RANGES = [ // localhost in IPv6 /^\[(::1|::)\]$/]; const regex = new RegExp(`^(${IP_RANGES.map(re => re.source).join('|')})$`); ipv6.exports = regex.test.bind(regex); ipv6.exports.regex = regex; return ipv6.exports; } var src; var hasRequiredSrc; function requireSrc () { if (hasRequiredSrc) return src; hasRequiredSrc = 1; src = hostname => requireIpv4()(hostname) || requireIpv6()(hostname); return src; } var srcExports = requireSrc(); var isLocalAddress = /*@__PURE__*/getDefaultExportFromCjs(srcExports); const isSSR = typeof window === 'undefined'; const castArray = value => [].concat(value); const getPreferredMedia = (data, mediaProps) => { let prefer; for (let index = 0; index < mediaProps.length; index++) { const key = mediaProps[index]; const value = data[key]; if (!isNil(value)) { prefer = key; break; } } return prefer; }; const isFunction = fn => typeof fn === 'function'; const isObject = obj => typeof obj === 'object'; const isNil = value => value == null; const getUrlPath = data => isObject(data) ? data.url : data; const someProp = (data, props) => data[props.find(prop => !isNil(data[prop]))]; const media = { mobile: function () { return css` @media (max-width: 48em) { ${css(...arguments)}; } `; }, desktop: function () { return css` @media (min-width: 48em) { ${css(...arguments)}; } `; } }; const getApiUrl = _ref => { let { apiKey, contrast = false, data, endpoint, force, headers, media, prerender, proxy, ttl, url } = _ref; return getApiUrl$1(url, { apiKey, audio: media.includes('audio'), data, endpoint, force, headers, iframe: media.includes('iframe'), palette: contrast, prerender, proxy, screenshot: media.includes('screenshot'), ttl, video: media.includes('video') }); }; const isLarge = cardSize => cardSize === 'large'; const isSmall = cardSize => cardSize === 'small'; const imageProxy = url => isLocalAddress(new URL(url).hostname) ? url : `https://images.weserv.nl/?${new URLSearchParams({ url, default: url, l: 9, af: '', il: '', n: -1 }).toString()}`; const isLazySupported = !isSSR && 'IntersectionObserver' in window; const formatSeconds = secs => { const secsToNum = parseInt(secs, 10); const hours = Math.floor(secsToNum / 3600); const minutes = Math.floor(secsToNum / 60) % 60; const seconds = secsToNum % 60; return [hours, minutes, seconds].filter((v, i) => v > 0 || i > 0).map(v => v >= 10 ? v : `0${v}`).join(':'); }; const clampNumber = (num, min, max) => { switch (true) { case num <= min: return min; case num >= max: return max; default: return num; } }; const BASE_CLASSNAME = 'microlink_card'; const CONTENT_BASE_CLASSNAME = `${BASE_CLASSNAME}__content`; const MEDIA_BASE_CLASSNAME = `${BASE_CLASSNAME}__media`; const CONTROLS_BASE_CLASSNAME = `${MEDIA_BASE_CLASSNAME}__controls`; const classNames = { main: BASE_CLASSNAME, content: CONTENT_BASE_CLASSNAME, title: `${CONTENT_BASE_CLASSNAME}_title`, description: `${CONTENT_BASE_CLASSNAME}_description`, url: `${CONTENT_BASE_CLASSNAME}_url`, mediaWrapper: `${MEDIA_BASE_CLASSNAME}_wrapper`, media: MEDIA_BASE_CLASSNAME, image: `${MEDIA_BASE_CLASSNAME}_image`, videoWrapper: `${MEDIA_BASE_CLASSNAME}_video_wrapper`, video: `${MEDIA_BASE_CLASSNAME}_video`, audioWrapper: `${MEDIA_BASE_CLASSNAME}_audio_wrapper`, audio: `${MEDIA_BASE_CLASSNAME}_audio`, mediaControls: CONTROLS_BASE_CLASSNAME, playbackControl: `${CONTROLS_BASE_CLASSNAME}_playback`, volumeControl: `${CONTROLS_BASE_CLASSNAME}_volume`, rwControl: `${CONTROLS_BASE_CLASSNAME}_rewind`, ffwControl: `${CONTROLS_BASE_CLASSNAME}_fast_forward`, rateControl: `${CONTROLS_BASE_CLASSNAME}_rate`, progressBar: `${CONTROLS_BASE_CLASSNAME}_progress_bar`, progressTime: `${CONTROLS_BASE_CLASSNAME}_progress_time`, spinner: `${CONTROLS_BASE_CLASSNAME}_spinner`, iframe: `${BASE_CLASSNAME}__iframe` }; const _excluded$7 = ["$useNanoClamp", "children"]; function ownKeys$3(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$3(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$3(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$3(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } const Clamp = _ref => { let { children, className, lines } = _ref; return isNil(children) ? null : /*#__PURE__*/React.createElement(i, { className: className, lines: lines, text: children, is: "p" }); }; const StyledClamp = styled(Clamp)` &&& { text-align: inherit; font-weight: inherit; font-family: inherit; color: inherit; margin: 0; ${_ref2 => { let { $useNanoClamp } = _ref2; return !$useNanoClamp && css` overflow: hidden; white-space: nowrap; text-overflow: ellipsis; `; }} } `; const CardText = _ref3 => { let { $useNanoClamp = true, children } = _ref3, props = _objectWithoutProperties(_ref3, _excluded$7); const textProps = $useNanoClamp ? props : _objectSpread$3(_objectSpread$3({}, props), {}, { as: 'p', title: children }); return /*#__PURE__*/React.createElement(StyledClamp, _extends({ $useNanoClamp: $useNanoClamp }, textProps), children); }; const speed = { short: '100ms', medium: '150ms', long: '300ms' }; const animation = { short: 'cubic-bezier(.25,.8,.25,1)', medium: 'cubic-bezier(.25,.8,.25,1)', long: 'cubic-bezier(.4, 0, .2, 1)' }; const createTransition = (properties, s) => { const suffix = `${speed[s]} ${animation[s]}`; return properties.map(property => `${property} ${suffix}`).join(', '); }; const transition = { short: function () { for (var _len = arguments.length, properties = new Array(_len), _key = 0; _key < _len; _key++) { properties[_key] = arguments[_key]; } return createTransition(properties, 'short'); }, medium: function () { for (var _len2 = arguments.length, properties = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { properties[_key2] = arguments[_key2]; } return createTransition(properties, 'medium'); }, long: function () { for (var _len3 = arguments.length, properties = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { properties[_key3] = arguments[_key3]; } return createTransition(properties, 'long'); } }; // https://primer.style/design/foundations/typography const font = { sans: "InterUI, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif", mono: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace" }; const _excluded$6 = ["autoPlay", "children", "controls", "loop", "mediaRef", "muted", "playsInline", "size"]; function ownKeys$2(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$2(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$2(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } const initialState = {}; const GlobalContext = /*#__PURE__*/React.createContext(initialState); const GlobalState = _ref => { let { autoPlay, children, controls, loop, mediaRef, muted, playsInline, size } = _ref, rest = _objectWithoutProperties(_ref, _excluded$6); const [state, setState] = useState(initialState); const updateState = useCallback(newState => setState(currentState => _objectSpread$2(_objectSpread$2({}, currentState), newState)), []); const props = useMemo(() => ({ autoPlay, controls, loop, mediaRef, muted, playsInline, size }), [autoPlay, controls, loop, mediaRef, muted, playsInline, size]); const values = useMemo(() => ({ props, state, updateState }), [props, state, updateState]); return /*#__PURE__*/React.createElement(GlobalContext.Provider, { value: values }, children(rest)); }; /* global URL */ const REGEX_STRIP_WWW = /^www\./; const BADGE_WIDTH = '16px'; const BADGE_HEIGHT = '12px'; const getHostname = href => { if (isNil(href)) return ''; const { hostname } = new URL(href); return hostname.replace(REGEX_STRIP_WWW, ''); }; const mobileDescriptionStyle = css` ${media.mobile` > p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } `}; `; const Content = styled('div').attrs({ className: classNames.content })` display: flex; padding: 10px 15px; min-width: 0; box-sizing: border-box; ${_ref => { let { $cardSize } = _ref; return css` flex: ${!isLarge($cardSize) ? 1 : '0 0 125px'}; justify-content: ${!isSmall($cardSize) ? 'space-around' : 'space-between'}; flex-direction: ${!isSmall($cardSize) ? 'column' : 'row'}; align-items: ${!isSmall($cardSize) ? 'stretch' : 'center'}; `; }}; `; const Header = styled('header').attrs({ className: classNames.title })` text-align: left; font-weight: bold; margin: 0; width: 100%; ${_ref2 => { let { $cardSize } = _ref2; return css` flex-grow: ${!isSmall($cardSize) ? 1.2 : 0.8}; font-size: ${!isSmall($cardSize) ? '16px' : '15px'}; ${isSmall($cardSize) && css` min-width: 0; padding-right: 14px; `} `; }} `; const Description = styled('div').attrs({ className: classNames.description })` text-align: left; font-size: 14px; flex-grow: 2; margin: auto 0; line-height: 18px; font-weight: normal; ${_ref3 => { let { $cardSize } = _ref3; return !isLarge($cardSize) && mobileDescriptionStyle; }}; `; const Footer = styled('footer').attrs({ className: classNames.url })` display: flex; align-items: center; justify-content: space-between; text-align: left; margin: 0; flex-grow: 0; font-weight: normal; ${_ref4 => { let { $cardSize } = _ref4; return css` font-size: ${!isSmall($cardSize) ? '12px' : '10px'}; ${!isSmall($cardSize) && 'width: 100%;'} `; }}; `; const Author = styled(CardText)` opacity: 0.75; transition: ${transition.medium('opacity')}; will-change: opacity; .${classNames.main}:hover & { opacity: 1; } `; const PoweredBy = styled('span').attrs({ title: 'microlink.io' })` background: url('https://cdn.microlink.io/logo/logo.svg') no-repeat center center; display: block; margin-left: 15px; transition: ${transition.medium('filter', 'opacity')}; will-change: filter, opacity; &:not(:hover) { filter: grayscale(100%); opacity: 0.75; } min-width: ${BADGE_WIDTH}; width: ${BADGE_WIDTH}; background-size: ${BADGE_WIDTH}; height: ${BADGE_HEIGHT}; `; const CardContent = () => { const { state: { description, title, url }, props: { size } } = useContext(GlobalContext); const isSmallCard = isSmall(size); const formattedUrl = useMemo(() => getHostname(url), [url]); const handleOnClick = useCallback(e => { e.preventDefault(); window.open('https://www.microlink.io', '_blank'); }, []); return /*#__PURE__*/React.createElement(Content, { $cardSize: size }, /*#__PURE__*/React.createElement(Header, { $cardSize: size }, /*#__PURE__*/React.createElement(CardText, { $useNanoClamp: false }, title)), !isSmallCard && /*#__PURE__*/React.createElement(Description, { $cardSize: size }, /*#__PURE__*/React.createElement(CardText, { lines: 2 }, description)), /*#__PURE__*/React.createElement(Footer, { $cardSize: size }, /*#__PURE__*/React.createElement(Author, { $useNanoClamp: false }, formattedUrl), /*#__PURE__*/React.createElement(PoweredBy, { onClick: handleOnClick }))); }; const emptyStatePulse = keyframes` 0% { background: #e1e8ed; } 70% { background: #cdd4d8; } 100% { background: #e1e8ed; } `; const emptyStateImagePulse = keyframes` 0% { background: #e1e8ed; } 70% { background: #dce3e8; } 100% { background: #e1e8ed; } `; const emptyStateAnimation = css` animation: ${emptyStatePulse} .75s linear infinite; `; const emptyStateImageAnimation = css` animation: ${emptyStateImagePulse} 1.25s linear infinite; `; const ImageLoadCatcher = styled('img')` height: 1px; width: 1px; position: absolute; z-index: -1; `; const loadingOverlay = css` &::after { content: ''; position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: #e1e8ed; z-index: 1; transition: ${transition.medium('opacity', 'visibility')}; will-change: opacity; ${_ref => { let { $isLoading } = _ref; return css` opacity: ${$isLoading ? 1 : 0}; visibility: ${$isLoading ? '$visible' : 'hidden'}; `; }}; } `; const mediaSizeStyles = { small: css` flex: 0 0 48px; `, normal: css` flex: 0 0 125px; ${media.mobile` flex: 0 0 92px; `} `, large: css` flex: 1; &::before { padding-bottom: 0; } ` }; const StyledWrap = styled('div')` background: transparent no-repeat center center / cover; display: block; overflow: hidden; height: auto; position: relative; &::before { content: ''; padding-bottom: 100%; display: block; } ${_ref => { let { $cardSize } = _ref; return mediaSizeStyles[$cardSize]; }}; ${loadingOverlay}; `; const Wrap$1 = props => { const { props: { size } } = useContext(GlobalContext); return /*#__PURE__*/React.createElement(StyledWrap, _extends({ $cardSize: size }, props)); }; const ImageWrap = styled(Wrap$1).attrs({ className: `${classNames.media} ${classNames.image}` })` background-image: ${_ref => { let { $url } = _ref; return $url ? `url('${imageProxy($url)}')` : ''; }}; `; const ImageComponent = props => { const { state: { imageUrl } } = useContext(GlobalContext); return /*#__PURE__*/React.createElement(ImageWrap, _extends({ $url: imageUrl }, props)); }; var _FooterEmpty; const MediaEmpty = styled(ImageComponent)` ${emptyStateImageAnimation}; `; const HeaderEmpty = styled('span')` opacity: 0.8; height: 16px; width: ${_ref => { let { $cardSize } = _ref; return !isSmall($cardSize) ? '60%' : '75%'; }}; display: block; background: #e1e8ed; margin: ${_ref2 => { let { $cardSize } = _ref2; return !isSmall($cardSize) ? '2px 0 8px' : '0 20px 0 0'; }}; ${emptyStateAnimation}; ${_ref3 => { let { $cardSize } = _ref3; return !isLarge($cardSize) && ` height: 15px; `; }}; `; const DescriptionEmpty = styled('span')` opacity: 0.8; height: 14px; width: 95%; display: block; position: relative; ${emptyStateAnimation}; animation-delay: 0.125s; `; const FooterEmpty = styled('span')` opacity: 0.8; height: 12px; width: 30%; display: block; ${emptyStateAnimation} animation-delay: .25s; ${_ref4 => { let { $cardSize } = _ref4; return !isLarge($cardSize) && ` height: 10px; `; }}; `; const CardEmptyState = () => { const { props: { size } } = useContext(GlobalContext); const isSmallCard = isSmall(size); return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MediaEmpty, { $cardSize: size }), /*#__PURE__*/React.createElement(Content, { $cardSize: size }, /*#__PURE__*/React.createElement(HeaderEmpty, { $cardSize: size }), !isSmallCard ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DescriptionEmpty, { $cardSize: size }), /*#__PURE__*/React.createElement(DescriptionEmpty, { $cardSize: size, style: { marginBottom: '12px' } })) : null, _FooterEmpty || (_FooterEmpty = /*#__PURE__*/React.createElement(FooterEmpty, null)))); }; const MediaButton = styled('div')` backface-visibility: hidden; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); transition: ${transition.short('transform')}; will-change: transform; > svg { display: block; } &:active:not(:focus) { transform: scale(0.9); } `; var _path$2, _path2$2; const VolumeMute = props => /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 14 14" }, props), _path$2 || (_path$2 = /*#__PURE__*/React.createElement("path", { fill: "#FFF", fillRule: "evenodd", stroke: "none", strokeWidth: "1", d: "M15.5 6.205l-.705-.705L13 7.295 11.205 5.5l-.705.705L12.295 8 10.5 9.795l.705.705L13 8.705l1.795 1.795.705-.705L13.705 8 15.5 6.205zM9 15a.5.5 0 01-.355-.15L4.835 11H1.5a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h3.335l3.81-3.85a.5.5 0 01.705 0 .5.5 0 01.15.35v13a.5.5 0 01-.5.5z", transform: "translate(-1 -1)" }))); const VolumeUp = props => /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 14 14" }, props), _path2$2 || (_path2$2 = /*#__PURE__*/React.createElement("path", { fill: "#FFF", fillRule: "evenodd", stroke: "none", strokeWidth: "1", d: "M13.58 4.04l-.765.645a5 5 0 01-.145 6.615l.735.7a6 6 0 00.175-7.94v-.02zM10.79 6a3 3 0 01-.09 3.97l.735.68a4 4 0 00.115-5.295L10.79 6zM9 15a.5.5 0 01-.355-.15L4.835 11H1.5a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h3.335l3.81-3.85a.5.5 0 01.705 0 .5.5 0 01.15.35v13a.5.5 0 01-.5.5z", transform: "translate(-1 -1)" }))); const BottomControls = styled('div')` z-index: 2; position: absolute; bottom: ${_ref => { let { $cardSize } = _ref; return isLarge($cardSize) ? 18 : 14; }}px; left: 0; right: 0; display: flex; justify-content: center; align-items: center; transition: ${transition.medium('opacity')}; will-change: opacity; `; const VolumeIcon = styled('svg')` stroke: #fff; `; const VolumeButton = styled(MediaButton).attrs({ className: classNames.volumeControl })` ${VolumeIcon} { width: ${_ref2 => { let { $cardSize } = _ref2; return isLarge($cardSize) ? 16 : 14; }}px; height: ${_ref3 => { let { $cardSize } = _ref3; return isLarge($cardSize) ? 16 : 14; }}px; ${_ref4 => { let { $cardSize } = _ref4; return !isLarge($cardSize) && media.mobile` width: 12px; height: 12px; `; }} } `; const PlaybackRateButton = styled(MediaButton).attrs({ className: classNames.rateControl })` font-size: ${_ref5 => { let { $cardSize } = _ref5; return isLarge($cardSize) ? 12 : 10; }}px; min-width: ${_ref6 => { let { $cardSize } = _ref6; return isLarge($cardSize) ? 33 : 28; }}px; line-height: 1; font-weight: bold; border: 1.5px solid #fff; border-radius: 9999px; padding: 1px 5px; text-align: center; color: #fff; margin-left: 10px; ${_ref7 => { let { $cardSize } = _ref7; return !isLarge($cardSize) && media.mobile` font-size: 8px; margin-left: 8px; min-width: 23px; `; }} `; const TimeLabel = styled('span').attrs({ className: classNames.progressTime })` margin: ${_ref8 => { let { $right } = _ref8; return !$right ? '0 auto 0 0' : '0 0 0 auto'; }}; font-family: ${font.mono}; font-size: 12px; padding: 0 16px; color: #fff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); `; const FooterControls = _ref9 => { let { $cardSize, currentTime, endTime, isMuted, onMuteClick, onPlaybackRateClick, playbackRate } = _ref9; const VolumeComponent = useMemo(() => isMuted ? VolumeMute : VolumeUp, [isMuted]); const isLargeCard = useMemo(() => isLarge($cardSize), [$cardSize]); return /*#__PURE__*/React.createElement(BottomControls, { $cardSize: $cardSize }, isLargeCard && /*#__PURE__*/React.createElement(TimeLabel, null, currentTime), /*#__PURE__*/React.createElement(VolumeButton, { title: isMuted ? 'Unmute' : 'Mute', $cardSize: $cardSize, onClick: onMuteClick }, /*#__PURE__*/React.createElement(VolumeIcon, { as: VolumeComponent })), /*#__PURE__*/React.createElement(PlaybackRateButton, { title: "Playback Rate", $cardSize: $cardSize, onClick: onPlaybackRateClick }, /*#__PURE__*/React.createElement("span", null, playbackRate, "x")), isLargeCard && /*#__PURE__*/React.createElement(TimeLabel, { $right: true }, endTime)); }; const _excluded$5 = ["$isPlaying"]; var _path$1, _path2$1; const Pause = props => /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 20" }, props), _path$1 || (_path$1 = /*#__PURE__*/React.createElement("path", { fill: "#FFF", fillRule: "evenodd", stroke: "none", strokeWidth: "1", d: "M12 6h-2a2 2 0 00-2 2v16a2 2 0 002 2h2a2 2 0 002-2V8a2 2 0 00-2-2zm10 0h-2a2 2 0 00-2 2v16a2 2 0 002 2h2a2 2 0 002-2V8a2 2 0 00-2-2z", transform: "translate(-8 -6)" }))); const Play = props => /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 21 24" }, props), _path2$1 || (_path2$1 = /*#__PURE__*/React.createElement("path", { fill: "#FFF", fillRule: "evenodd", stroke: "none", strokeWidth: "1", d: "M7 28a1 1 0 01-1-1V5a1 1 0 011.501-.865l19 11a1 1 0 010 1.73l-19 11A.998.998 0 017 28z", transform: "translate(-6 -4)" }))); const iconSizes = { large: '50px', normal: '35px', small: '20px' }; const PlaybackIcon = styled('svg')` stroke: #fff; `; const PlaybackButtonWrap = styled(MediaButton).attrs({ className: classNames.playbackControl })` ${PlaybackIcon} { ${_ref => { let { $cardSize } = _ref; return css` width: ${iconSizes[$cardSize]}; height: ${iconSizes[$cardSize]}; padding: ${isLarge($cardSize) ? 0 : '8px'}; ${!isLarge($cardSize) && !isSmall($cardSize) && media.mobile` width: calc(${iconSizes.small} * 1.2); height: calc(${iconSizes.small} * 1.2); `} `; }} } `; const PlaybackButton = _ref2 => { let { $isPlaying } = _ref2, props = _objectWithoutProperties(_ref2, _excluded$5); const PlaybackComponent = useMemo(() => $isPlaying ? Pause : Play, [$isPlaying]); return /*#__PURE__*/React.createElement(PlaybackButtonWrap, _extends({ title: $isPlaying ? 'Pause' : 'Play' }, props), /*#__PURE__*/React.createElement(PlaybackIcon, { as: PlaybackComponent })); }; const SCRUBBER_SIZE = 12; const scrubberSizeScales = { normal: 0.8, small: 0.9 }; const getScrubberSize = size => Math.floor(SCRUBBER_SIZE * (scrubberSizeScales[size] || 1)); const Scrubber = styled('div').attrs(_ref => { let { $isVisible, $positionX } = _ref; return { style: { left: $positionX, transform: `scale(${$isVisible ? 1 : 0.5}) translate(-50%, -50%)`, opacity: $isVisible ? 1 : 0, visibility: $isVisible ? '$visible' : 'hidden' } }; })` position: absolute; top: 50%; background: #ffffff; border-radius: 50%; transform-origin: center center; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); transition: ${transition.short('transform', 'opacity', 'visibility')}; will-change: left, transform, opacity, visibility; backface-visibility: hidden; z-index: 3; ${_ref2 => { let { $cardSize } = _ref2; const scrubberSize = getScrubberSize($cardSize); return css` height: ${scrubberSize}px; width: ${scrubberSize}px; `; }} `; const _excluded$4 = ["$isDragging", "$isVisible", "label", "$positionX", "size"]; const BASE_FONT_SIZE = 11; const sizeScales$1 = { normal: 0.8 }; const getMarkerFontSize = size => BASE_FONT_SIZE * (sizeScales$1[size] || 1); const TooltipBase = styled('span').attrs(_ref => { let { $position, $isDragging, $visible } = _ref; return { style: { left: `${$position}px`, top: $visible ? '-4px' : '0px', visibility: $visible ? '$visible' : 'hidden', opacity: $visible ? 1 : 0, transform: `translate(-50%, ${!$isDragging ? -100 : -110}%)` } }; })` position: absolute; background: rgba(24, 25, 25, 0.75); color: #fff; text-shadow: 0 1px 2px rgba(24, 25, 25, 0.15); padding: 2px 3px; border-radius: 4px; font-family: ${font.mono}; font-size: ${_ref2 => { let { $cardSize } = _ref2; return getMarkerFontSize($cardSize); }}px; line-height: 1; transition: ${transition.medium('opacity', 'visibility', 'transform')}, ${transition.long('top')}; will-change: top, left, visibility, opacity, transform; backface-visibility: hidden; `; const Tooltip = /*#__PURE__*/forwardRef((_ref3, ref) => { let { $isDragging, $isVisible, label, $positionX, size } = _ref3, props = _objectWithoutProperties(_ref3, _excluded$4); return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TooltipBase, _extends({ $visible: $isVisible, $position: $positionX, $cardSize: size, ref: ref, $isDragging: $isDragging }, props), label)); }); Tooltip.displayName = 'Tooltip'; const _excluded$3 = ["key"]; const HEIGHT$1 = 6; const PADDING = 6; const heightScales = { normal: 0.7, small: 0.6 }; const activeHeightScales = { small: 0.9, large: 1.4 }; const getProgressBarHeight = size => Math.floor(HEIGHT$1 * (heightScales[size] || 1)); const getProgressBarActiveHeight = size => Math.floor(HEIGHT$1 * (activeHeightScales[size] || 1)); const OuterWrap$1 = styled('div').attrs(() => ({ className: classNames.progressBar }))` position: relative; padding: ${PADDING}px ${PADDING / 2}px ${PADDING / 2}px; z-index: 2; backface-visibility: hidden; `; const BarsWrap = styled('div').attrs(_ref => { let { $cardSize, $isDragging } = _ref; if ($isDragging) { const activeHeight = getProgressBarActiveHeight($cardSize); return { style: { height: `${activeHeight}px` } }; } return {}; })` background: transparent; border-radius: 9999px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); background: rgba(255, 255, 255, 0.15); transition: ${transition.short('height')}; will-change: height; pointer-events: none; position: relative; ${_ref2 => { let { $cardSize } = _ref2; const height = getProgressBarHeight($cardSize); const activeHeight = getProgressBarActiveHeight($cardSize); return css` height: ${height}px; ${OuterWrap$1}:hover & { height: ${activeHeight}px; } `; }} `; const ProgressLine = styled('div')` border-radius: inherit; height: 100%; position: relative; overflow: hidden; `; const ProgressMask = styled('div').attrs(_ref3 => { let { $maskScale } = _ref3; return { style: { transform: `scaleX(${$maskScale})` } }; })` position: absolute; left: 0; top: -50%; height: 200%; width: 100%; background: #ffffff; transform-origin: left center; will-change: transform; `; const ProgressHover = styled('div').attrs(_ref4 => { let { $cursorRatio, $isHovering, $progressPercent } = _ref4; return { style: { left: $progressPercent, transform: `scaleX(${$cursorRatio})`, opacity: $isHovering ? 1 : 0, visibility: $isHovering ? '$visible' : 'hidden' } }; })` position: absolute; top: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.4); transform-origin: left center; transition: ${transition.short('opacity', 'visibility')}; will-change: left, transform, opacity, $visible; `; const BufferedChunk = styled('div').attrs(_ref5 => { let { start, end } = _ref5; return { style: { left: `${start}px`, right: `${end}px` } }; })` background: rgba(255, 255, 255, 0.35); position: absolute; top: 0; bottom: 0; `; const ProgressBar = _ref6 => { let { bufferedMedia, cursorX, duration, hoveredTime, $isDragging, $isHovering, onClick, onMouseDown, onMouseOver, progress, showTooltip } = _ref6; const { props: { size } } = useContext(GlobalContext); const wrapRef = useRef(); const tooltipRef = useRef(); const isSmallCard = useMemo(() => isSmall(size), [size]); const getWrapWidth = useCallback(() => { if (wrapRef.current) { return wrapRef.current.getBoundingClientRect().width - PADDING; } return 0; }, []); const progressRatio = useMemo(() => clampNumber(progress / duration, 0, 1), [duration, progress]); const $progressPercent = useMemo(() => `${clampNumber(progressRatio * 100, 1, 99)}%`, [progressRatio]); const $cursorRatio = useMemo(() => { if (wrapRef.current) { const wrapWidth = getWrapWidth(); const startPoint = progressRatio * wrapWidth; const cursorPosition = cursorX - startPoint; const width = wrapWidth - startPoint; if (cursorPosition > 0) { return clampNumber((cursorPosition / width).toFixed(3), 0, 0.99); } } return 0; }, [cursorX, getWrapWidth, progressRatio]); const bufferedMediaChunks = useMemo(() => { const wrapWidth = getWrapWidth(); return bufferedMedia.map((chunk, key) => { const start = chunk.start * wrapWidth; const end = wrapWidth - chunk.end * wrapWidth; return { key, start, end }; }); }, [bufferedMedia, getWrapWidth]); const tooltipLabel = useMemo(() => formatSeconds(hoveredTime), [hoveredTime]); const tooltipPositionX = useMemo(() => { if (wrapRef.current && tooltipRef.current) { const wrapWidth = getWrapWidth(); const tooltipWidth = tooltipRef.current.getBoundingClientRect().width; const tooltipHalf = tooltipWidth / 2; return clampNumber(cursorX, tooltipHalf, wrapWidth - tooltipHalf); } return 0; }, [cursorX, getWrapWidth]); const mouseEvents = useMemo(() => ({ onClick, onMouseDown, onMouseOver }), [onClick, onMouseDown, onMouseOver]); const showAccessories = useMemo(() => $isDragging || $isHovering, [$isDragging, $isHovering]); return /*#__PURE__*/React.createElement(OuterWrap$1, _extends({ $cardSize: size, ref: wrapRef }, mouseEvents), /*#__PURE__*/React.createElement(BarsWrap, { $cardSize: size, $isDragging: $isDragging }, /*#__PURE__*/React.createElement(ProgressLine, null, /*#__PURE__*/React.createElement(ProgressHover, { $cursorRatio: $cursorRatio, $isHovering: $isHovering, $progressPercent: $progressPercent }), bufferedMediaChunks.map(_ref7 => { let { key } = _ref7, chunk = _objectWithoutProperties(_ref7, _excluded$3); return /*#__PURE__*/React.createElement(BufferedChunk, _extends({ key: key }, chunk)); }), /*#__PURE__*/React.createElement(ProgressMask, { $maskScale: progressRatio })), /*#__PURE__*/React.createElement(Scrubber, { $cardSize: size, $isVisible: showAccessories, $positionX: $progressPercent }), !isSmallCard && /*#__PURE__*/React.createElement(Tooltip, { $isDragging: $isDragging, $isVisible: showAccessories, label: tooltipLabel, $positionX: tooltipPositionX, ref: tooltipRef, size: size }))); }; var _path, _path2; const _excluded$2 = ["$cardSize"], _excluded2$2 = ["$cardSize"], _excluded3 = ["type", "$cardSize"]; const Backward = _ref => { let { $cardSize } = _ref, props = _objectWithoutProperties(_ref, _excluded$2); return /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 29" }, props), _path || (_path = /*#__PURE__*/React.createElement("path", { fill: "#FFF", fillRule: "evenodd", stroke: "none", strokeWidth: "1", d: "M4 18c0 6.627 5.373 12 12 12s12-5.373 12-12S22.627 6 16 6h-4V1L6 7l6 6V8h4c5.523 0 10 4.477 10 10s-4.477 10-10 10S6 23.523 6 18H4zm15.63 4.13a2.84 2.84 0 01-1.28-.27 2.44 2.44 0 01-.89-.77 3.57 3.57 0 01-.52-1.25 7.69 7.69 0 01-.17-1.68 7.83 7.83 0 01.17-1.68c.094-.445.27-.87.52-1.25.23-.325.535-.59.89-.77.4-.188.838-.28 1.28-.27a2.44 2.44 0 012.16 1 5.23 5.23 0 01.7 2.93 5.23 5.23 0 01-.7 2.93 2.44 2.44 0 01-2.16 1.08zm0-1.22c.411.025.8-.19 1-.55a3.38 3.38 0 00.37-1.51v-1.38a3.31 3.31 0 00-.29-1.5 1.23 1.23 0 00-2.06 0 3.31 3.31 0 00-.29 1.5v1.38a3.38 3.38 0 00.29 1.51c.195.356.575.57.98.55zm-9 1.09v-1.18h2v-5.19l-1.86 1-.55-1.06 2.32-1.3H14v6.5h1.78V22h-5.15z", transform: "translate(-4 -1)" }))); }; const Forward = _ref2 => { let { $cardSize } = _ref2, props = _objectWithoutProperties(_ref2, _excluded2$2); return /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 29" }, props), _path2 || (_path2 = /*#__PURE__*/React.createElement("path", { fill: "#FFF", fillRule: "evenodd", stroke: "none", strokeWidth: "1", d: "M26 18c0 5.523-4.477 10-10 10S6 23.523 6 18 10.477 8 16 8h4v5l6-6-6-6v5h-4C9.373 6 4 11.373 4 18s5.373 12 12 12 12-5.373 12-12h-2zm-6.36 4.13a2.81 2.81 0 01-1.28-.27 2.36 2.36 0 01-.89-.77 3.39 3.39 0 01-.47-1.25 7.12 7.12 0 01-.17-1.68 7.24 7.24 0 01.17-1.68 3.46 3.46 0 01.52-1.25 2.36 2.36 0 01.89-.77c.4-.19.838-.282 1.28-.27a2.44 2.44 0 012.16 1 5.31 5.31 0 01.7 2.93 5.31 5.31 0 01-.7 2.93 2.44 2.44 0 01-2.21 1.08zm0-1.22a1 1 0 001-.55c.22-.472.323-.99.3-1.51v-1.38a3.17 3.17 0 00-.3-1.5 1.22 1.22 0 00-2.05 0 3.18 3.18 0 00-.29 1.5v1.38a3.25 3.25 0 00.29 1.51 1 1 0 001.05.55zm-7.02-3.49c.355.035.71-.06 1-.27a.84.84 0 00.31-.68v-.08a.94.94 0 00-.3-.74 1.2 1.2 0 00-.83-.27 1.65 1.65 0 00-.89.24 2.1 2.1 0 00-.68.68l-.93-.83a5.37 5.37 0 01.44-.51 2.7 2.7 0 01.54-.4 2.55 2.55 0 01.7-.27 3.25 3.25 0 01.87-.1 3.94 3.94 0 011.06.14c.297.078.576.214.82.4.224.168.408.383.54.63.123.26.184.543.18.83a2 2 0 01-.11.67 1.82 1.82 0 01-.32.52 1.79 1.79 0 01-.47.36 2.27 2.27 0 01-.57.2V18c.219.04.431.11.63.21a1.7 1.7 0 01.85.93c.084.234.124.481.12.73a2 2 0 01-.2.92 2 2 0 01-.58.72 2.66 2.66 0 01-.89.45 3.76 3.76 0 01-1.15.16 4.1 4.1 0 01-1-.11 3.1 3.1 0 01-.76-.31 2.76 2.76 0 01-.56-.45 4.22 4.22 0 01-.44-.55l1.07-.81c.082.147.175.288.28.42.105.128.226.243.36.34.137.097.29.171.45.22a2 2 0 00.57.07 1.45 1.45 0 001-.3 1.12 1.12 0 00.34-.85v-.08a1 1 0 00-.37-.8 1.78 1.78 0 00-1.06-.28h-.76v-1.21h.74z", transform: "translate(-4 -1)" }))); }; const SeekIcon = styled('svg')` stroke: #fff; width: ${_ref3 => { let { $cardSize } = _ref3; return isLarge($cardSize) ? 30 : 24; }}px; height: ${_ref4 => { let { $cardSize } = _ref4; return isLarge($cardSize) ? 30 : 24; }}px; ${_ref5 => { let { $cardSize } = _ref5; return !isLarge($cardSize) && media.mobile` width: 0; height: 0; `; }} `; const SeekButtonWrap = styled(MediaButton)` margin: 0 ${_ref6 => { let { $cardSize } = _ref6; return isLarge($cardSize) ? '28px' : '3px'; }}; `; const SeekButton = _ref7 => { let { type = 'rewind', $cardSize } = _ref7, props = _objectWithoutProperties(_ref7, _excluded3); const IconComponent = useMemo(() => type === 'rewind' ? Backward : Forward, [type]); return /*#__PURE__*/React.createElement(SeekButtonWrap, _extends({ title: type === 'rewind' ? 'Rewind' : 'Forward', $cardSize: $cardSize }, props), /*#__PURE__*/React.createElement(SeekIcon, { as: IconComponent, $cardSize: $cardSize })); }; var _Svg; const BASE_SIZE = 12; const BASE_OFFSET = 6; const offsetScales = { normal: 0.8, small: 0.6 }; const sizeScales = { normal: 0.9, small: 0.8 }; const getSpinnerOffset = size => Math.floor(BASE_OFFSET * (offsetScales[size] || 1)); const getSpinnerSize = size => Math.floor(BASE_SIZE * (sizeScales[size] || 1)); const rotate = keyframes` 100% { transform: rotate(360deg); } `; const dash = keyframes` 0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; } 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; } 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; } `; const Wrap = styled(MediaButton).attrs(_ref => { let { $isVisible } = _ref; return { style: { opacity: $isVisible ? 1 : 0, visibility: $isVisible ? '$visible' : 'hidden' } }; })(_ref2 => { let { $cardSize } = _ref2; const size = `${getSpinnerSize($cardSize)}px`; const offset = `${getSpinnerOffset($cardSize)}px`; return css` position: absolute; width: ${size}; right: ${offset}; top: ${offset}; transition: ${transition.medium('opacity', 'visibility')}; will-change: opacity, visibility; pointer-events: none; `; }); const Svg = styled('svg')` width: 100%; animation: ${rotate} 2s linear infinite; will-change: transform; `; const Circle = styled('circle')` stroke: #fff; stroke-linecap: round; stroke-width: 7; fill: none; animation: ${dash} 1.5s ease-in-out infinite; will-change: stroke-dasharray, stroke-dashoffset; `; const Spinner = _ref3 => { let { size, $isVisible } = _ref3; return /*#__PURE__*/React.createElement(Wrap, { $cardSize: size, className: classNames.spinner, $isVisible: $isVisible }, _Svg || (_Svg = /*#__PURE__*/React.createElement(Svg, { viewBox: "0 0 50 50" }, /*#__PURE__*/React.createElement(Circle, { cx: "25", cy: "25", r: "20" })))); }; const SPACE_KEY = 32; const L_ARROW_KEY = 37; const R_ARROW_KEY = 39; const M_KEY = 77; const OuterWrap = styled('div').attrs({ className: classNames.mediaControls })` position: absolute; left: 0; top: 0; right: 0; bottom: 0; transition: ${transition.long('background')}, ${transition.medium('opacity')}; will-change: background; display: flex; flex-direction: column; pointer-events: auto; ${_ref => { let { $hasInteracted, $isDragging, $isPlaying } = _ref; const bg = 'rgba(0, 0, 0, 0.35)'; const dragBg = 'rgba(0, 0, 0, 0.2)'; const isPaused = $hasInteracted && !$isPlaying; return css` .${classNames.main}:hover & { background: ${!$isDragging ? bg : dragBg}; } .${classNames.main}:not(:hover) & { opacity: ${!$hasInteracted || isPaused ? 1 : 0}; ${isPaused && `background: ${bg}`}; } `; }} `; const InnerWrap = styled('div')` position: absolute; left: 0; top: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; z-index: 2; `; const ControlsTop = styled('div')` flex: 1; ${_ref2 => { let { $isVisible } = _ref2; return !$isVisible && css` *[class*='${classNames.mediaControls}']:not(.${classNames.progressTime}) { transition: ${transition.medium('opacity', 'visibility')}; opacity: 0; visibility: hidden; } `; }} `; const getNextPlaybackRate = rate => { switch (rate) { case 1: return 1.25; case 1.25: return 1.5; case 1.5: return 2; default: return 1; } }; const Controls = _ref3 => { let { MediaComponent, mediaProps } = _ref3; const { props: { autoPlay, controls, mediaRef: propRef, muted, loop, size } } = useContext(GlobalContext); const [duration, setDuration] = useState(0); const [progress, setProgress] = useState(0); const [buffered, setBuffered] = useState([]); const [cursorX, setCursorX] = useState(0); const [hoveredTime, setHoveredTime] = useState(0); const [$isPlaying, setIsPlaying] = useState(autoPlay); const [isMuted, setIsMuted] = useState(muted); const [isBuffering, setIsBuffering] = useState(false); const [$isHovering, setIsHovering] = useState(false); const [$isDragging, setIsDragging] = useState(false); const [playbackRate, setPlaybackRate] = useState(1); const [$hasInteracted, setHasInteracted] = useState(autoPlay); const [pausedByDrag, setPausedByDrag] = useState(false); const mediaRef = useRef(); const setRefs = useCallback(node => { mediaRef.current = node; if (propRef) { if (isFunction(propRef)) { propRef(node); } else { propRef.current = node; } } }, [propRef]); const isNotSmall = useMemo(() => !isSmall(size), [size]); const mediaEvents = useMemo(() => ({ onCanPlay: () => setIsBuffering(false), onLoadedMetadata: e => setDuration(e.currentTarget.duration), onPause: () => setIsPlaying(false), onPlay: () => setIsPlaying(true), onPlaying: () => setIsBuffering(false), onProgress: e => setBuffered(e.currentTarget.buffered), onRateChange: e => setPlaybackRate(e.currentTarget.playbackRate), onTimeUpdate: e => setProgress(e.currentTarget.currentTime), onVolumeChange: e => setIsMuted(e.currentTarget.muted), onWaiting: e => setIsBuffering(true) }), []); const evaluateCursorPosition = useCallback(event => { if (mediaRef.current) { const bounds = event.currentTarget.getBoundingClientRect(); const cursor = clampNumber(Math.floor(event.clientX - bounds.left), 0, bounds.width); const time = cursor / bounds.width * mediaRef.current.duration; return { cursor, time }; } return { cursor: 0, time: 0 }; }, []); const togglePlayback = useCallback(() => { if (mediaRef.current) { if (mediaRef.current.paused) { if (!$hasInteracted) { setHasInteracted(true); } mediaRef.current.play(); } else { mediaRef.current.pause(); } } }, [$hasInteracted]); const jumpTo = useCallback(time => { if (mediaRef.current) { const t = clampNumber(time, 0, mediaRef.current.duration); mediaRef.current.currentTime = t; setProgress(t); } }, []); const onSeekClick = useCallback((event, type) => { event.preventDefault(); event.stopPropagation(); if (mediaRef.current) { const { currentTime } = mediaRef.current; jumpTo(type === 'rewind' ? currentTime - 10 : currentTime + 30); } }, [jumpTo]); const onMuteClick = useCallback(event => { event.preventDefault(); event.stopPropagation(); if (mediaRef.current) { mediaRef.current.muted = !mediaRef.current.muted; } }, []); const onPlaybackRateClick = useCallback(event => { event.preventDefault(); event.stopPropagation(); if (mediaRef.current) { mediaRef.current.playbackRate = getNextPlaybackRate(mediaRef.current.playbackRate); } }, []); const onProgressBarClick = useCallback(event => { event.preventDefault(); event.stopPropagation(); setIsDragging(false); }, []); const onProgressBarMouseDown = useCallback(event => { event.preventDefault(); event.stopPropagation(); setIsDragging(true); const { time } = evaluateCursorPosition(event); jumpTo(time); }, [evaluateCursorPosition, jumpTo]); const onProgressBarMouseOver = useCallback(() => setIsHovering(true), []); const onWrapClick = useCallback(event => { event.preventDefault(); event.stopPropagation(); if ($isDragging) { setIsDragging(false); } else { togglePlayback(); } }, [$isDragging, togglePlayback]); const onWrapMouseMove = useCallback(event => { if (($isDragging || $isHovering) && mediaRef.current) { event.preventDefault(); const { cursor, time } = evaluateCursorPosition(event); setHoveredTime(time); setCursorX(cursor); if ($isDragging) { if (!mediaRef.current.paused) { m