mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added custom video player controls
refs https://github.com/TryGhost/Team/issues/1229 - adds custom video player controls, similar to audio player
This commit is contained in:
parent
baf6db2a48
commit
3df7737de7
2 changed files with 423 additions and 0 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
146
core/frontend/src/cards/js/video.js
Normal file
146
core/frontend/src/cards/js/video.js
Normal file
|
@ -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]);
|
||||
}
|
||||
})();
|
Loading…
Add table
Reference in a new issue