diff --git a/core/frontend/src/cards/css/video.css b/core/frontend/src/cards/css/video.css index 5132fcf357..9bf6d45bd9 100644 --- a/core/frontend/src/cards/css/video.css +++ b/core/frontend/src/cards/css/video.css @@ -2,3 +2,280 @@ max-width: 100%; height: auto; } + +.kg-video-thumbnail { + display: flex; + justify-content: center; + align-items: center; + width: 80px; + min-width: 80px; + margin: 8px; + background: var(--ghost-accent-color); + object-fit: cover; + aspect-ratio: 1/1; + border-radius: 3px; +} + +.kg-video-thumbnail svg { + width: 24px; + height: 24px; + fill: white; +} + +.kg-video-card { + position: relative; + --seek-before-width: 0%; + --volume-before-width: 100%; + --buffered-width: 0%; +} + +.kg-video-title { + width: 100%; + margin: 8px 0 0 0; + padding: 8px 12px 0; + border: none; + font-family: inherit; + font-size: 1em; + font-weight: 700; + background: transparent; +} + +.kg-player { + display: flex; + flex-grow: 1; + align-items: center; + padding: 8px 12px; +} + +.kg-video-current-time { + min-width: 38px; + padding: 0 4px; + font-family: inherit; + font-size: .85em; + font-weight: 500; + line-height: 1.4em; + white-space: nowrap; +} + +.kg-video-time { + color: #ababab; + font-family: inherit; + font-size: .85em; + font-weight: 500; + line-height: 1.4em; + white-space: nowrap; +} + +.kg-video-duration { + padding: 0 4px; +} + +.kg-video-play-icon, +.kg-video-pause-icon { + position: relative; + bottom: 1px; + padding: 0px 4px 0 0; + font-size: 0; + background: transparent; +} + +.kg-video-hide { + display: none !important; +} + +.kg-video-play-icon svg, +.kg-video-pause-icon svg { + width: 14px; + height: 14px; + fill: currentColor; +} + +.kg-video-seek-slider { + flex-grow: 1; + margin: 0 4px; +} + +.kg-video-playback-rate { + min-width: 37px; + padding: 0 4px; + font-family: inherit; + font-size: .85em; + font-weight: 600; + line-height: 1.4em; + text-align: left; + background: transparent; + white-space: nowrap; +} + +.kg-video-mute-icon, +.kg-video-unmute-icon { + position: relative; + bottom: 1px; + padding: 0 4px; + font-size: 0; + background: transparent; +} + +.kg-video-mute-icon svg, +.kg-video-unmute-icon svg { + width: 16px; + height: 16px; + fill: currentColor; +} + +.kg-video-volume-slider { + width: 80px; +} + +.kg-video-seek-slider::before { + content: ""; + position: absolute; + left: 0; + width: var(--seek-before-width) !important; + height: 4px; + cursor: pointer; + background-color: currentColor; + border-radius: 2px; +} + +.kg-video-volume-slider::before { + content: ""; + position: absolute; + left: 0; + width: var(--volume-before-width) !important; + height: 4px; + cursor: pointer; + background-color: currentColor; + border-radius: 2px; +} + +/* Resetting browser styles +/* --------------------------------------------------------------- */ + +.kg-video-card input[type=range] { + position: relative; + -webkit-appearance: none; + background: transparent; +} + +.kg-video-card input[type=range]:focus { + outline: none; +} + +.kg-video-card input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; +} + +.kg-video-card input[type=range]::-ms-track { + cursor: pointer; + border-color: transparent; + color: transparent; + background: transparent; +} + +.kg-video-card button { + display: flex; + align-items: center; + border: 0; +} + +.kg-video-card input[type="range"] { + height: auto; + padding: 0; + border: 0; +} + +/* Chrome & Safari styles +/* --------------------------------------------------------------- */ + +.kg-video-card input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: rgba(124, 139, 154, 0.25); + border-radius: 2px; +} + +.kg-video-card input[type="range"]::-webkit-slider-thumb { + position: relative; + box-sizing: content-box; + width: 12px; + height: 12px; + margin: -5px 0 0 0; + border: 0; + cursor: pointer; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); +} + +.kg-video-card input[type="range"]:active::-webkit-slider-thumb { + transform: scale(1.2); +} + +/* Firefox styles +/* --------------------------------------------------------------- */ + +.kg-video-card input[type="range"]::-moz-range-track { + width: 100%; + height: 4px; + cursor: pointer; + background: rgba(124, 139, 154, 0.25); + border-radius: 2px; +} + +.kg-video-card input[type="range"]::-moz-range-progress { + background: currentColor; + border-radius: 2px; +} + +.kg-video-card input[type="range"]::-moz-range-thumb { + box-sizing: content-box; + width: 12px; + height: 12px; + border: 0; + cursor: pointer; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); +} + +.kg-video-card input[type="range"]:active::-moz-range-thumb { + transform: scale(1.2); +} + +/* Edge & IE styles +/* --------------------------------------------------------------- */ + +.kg-video-card input[type="range"]::-ms-track { + width: 100%; + height: 3px; + border: solid transparent; + color: transparent; + cursor: pointer; + background: transparent; +} + +.kg-video-card input[type="range"]::-ms-fill-lower { + background: #fff; +} + +.kg-video-card input[type="range"]::-ms-fill-upper { + background: currentColor; +} + +.kg-video-card input[type="range"]::-ms-thumb { + box-sizing: content-box; + width: 12px; + height: 12px; + border: 0; + cursor: pointer; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); +} + +.kg-video-card input[type="range"]:active::-ms-thumb { + transform: scale(1.2); +} + diff --git a/core/frontend/src/cards/js/video.js b/core/frontend/src/cards/js/video.js new file mode 100644 index 0000000000..261d904c69 --- /dev/null +++ b/core/frontend/src/cards/js/video.js @@ -0,0 +1,146 @@ +(function() { + const handleVideoPlayer = function (videoElementContainer) { + const videoPlayerContainer = videoElementContainer.querySelector('.kg-player'); + const playIconContainer = videoElementContainer.querySelector('.kg-video-play-icon'); + const pauseIconContainer = videoElementContainer.querySelector('.kg-video-pause-icon'); + const seekSlider = videoElementContainer.querySelector('.kg-video-seek-slider'); + const playbackRateContainer = videoElementContainer.querySelector('.kg-video-playback-rate'); + const muteIconContainer = videoElementContainer.querySelector('.kg-video-mute-icon'); + const unmuteIconContainer = videoElementContainer.querySelector('.kg-video-unmute-icon'); + const volumeSlider = videoElementContainer.querySelector('.kg-video-volume-slider'); + const videoEl = videoElementContainer.querySelector('video'); + const durationContainer = videoElementContainer.querySelector('.kg-video-duration'); + const currentTimeContainer = videoElementContainer.querySelector('.kg-video-current-time'); + let playbackRates = [{ + rate: 0.75, + label: '0.7×' + }, { + rate: 1.0, + label: '1×' + }, { + rate: 1.25, + label: '1.2×' + }, { + rate: 1.75, + label: '1.7×' + }, { + rate: 2.0, + label: '2×' + }]; + + let raf = null; + let currentPlaybackRateIdx = 1; + + const whilePlaying = () => { + seekSlider.value = Math.floor(videoEl.currentTime); + currentTimeContainer.textContent = calculateTime(seekSlider.value); + videoPlayerContainer.style.setProperty('--seek-before-width', `${seekSlider.value / seekSlider.max * 100}%`); + raf = requestAnimationFrame(whilePlaying); + } + + const showRangeProgress = (rangeInput) => { + if (rangeInput === seekSlider) { + videoPlayerContainer.style.setProperty('--seek-before-width', rangeInput.value / rangeInput.max * 100 + '%'); + } + else { + videoPlayerContainer.style.setProperty('--volume-before-width', rangeInput.value / rangeInput.max * 100 + '%'); + } + } + + const calculateTime = (secs) => { + const minutes = Math.floor(secs / 60); + const seconds = Math.floor(secs % 60); + const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`; + return `${minutes}:${returnedSeconds}`; + } + + const displayDuration = () => { + durationContainer.textContent = calculateTime(videoEl.duration); + } + + const setSliderMax = () => { + seekSlider.max = Math.floor(videoEl.duration); + } + + const displayBufferedAmount = () => { + if (videoEl.buffered.length > 0) { + const bufferedAmount = Math.floor(videoEl.buffered.end(videoEl.buffered.length - 1)); + videoPlayerContainer.style.setProperty('--buffered-width', `${(bufferedAmount / seekSlider.max) * 100}%`); + } + } + + if (videoEl.readyState > 0) { + displayDuration(); + setSliderMax(); + displayBufferedAmount(); + } else { + videoEl.addEventListener('loadedmetadata', () => { + displayDuration(); + setSliderMax(); + displayBufferedAmount(); + }); + } + + playIconContainer.addEventListener('click', () => { + playIconContainer.classList.add("kg-video-hide"); + pauseIconContainer.classList.remove("kg-video-hide"); + videoEl.play(); + requestAnimationFrame(whilePlaying); + }); + + pauseIconContainer.addEventListener('click', () => { + pauseIconContainer.classList.add("kg-video-hide"); + playIconContainer.classList.remove("kg-video-hide"); + videoEl.pause(); + cancelAnimationFrame(raf); + }); + + muteIconContainer.addEventListener('click', () => { + muteIconContainer.classList.add("kg-video-hide"); + unmuteIconContainer.classList.remove("kg-video-hide"); + videoEl.muted = false; + }); + + unmuteIconContainer.addEventListener('click', () => { + unmuteIconContainer.classList.add("kg-video-hide"); + muteIconContainer.classList.remove("kg-video-hide"); + videoEl.muted = true; + }); + + playbackRateContainer.addEventListener('click', () => { + let nextPlaybackRate = playbackRates[(currentPlaybackRateIdx + 1) % 5]; + currentPlaybackRateIdx = currentPlaybackRateIdx + 1; + videoEl.playbackRate = nextPlaybackRate.rate; + playbackRateContainer.textContent = nextPlaybackRate.label; + }); + + videoEl.addEventListener('progress', displayBufferedAmount); + + seekSlider.addEventListener('input', (e) => { + showRangeProgress(e.target); + currentTimeContainer.textContent = calculateTime(seekSlider.value); + if (!videoEl.paused) { + cancelAnimationFrame(raf); + } + }); + + seekSlider.addEventListener('change', () => { + videoEl.currentTime = seekSlider.value; + if (!videoEl.paused) { + requestAnimationFrame(whilePlaying); + } + }); + + volumeSlider.addEventListener('input', (e) => { + const value = e.target.value; + showRangeProgress(e.target); + videoEl.volume = value / 100; + }); + } + + const videoCardElements = document.querySelectorAll('.kg-video-card'); + + for (let i = 0; i < videoCardElements.length; i++) { + handleVideoPlayer(videoCardElements[i]); + } +})();