diff --git a/_sass/_player.scss b/_sass/_player.scss index 94da97a..5079447 100644 --- a/_sass/_player.scss +++ b/_sass/_player.scss @@ -1,9 +1,9 @@ // -------------------------------------------------------------------------------- // Media Player // -------------------------------------------------------------------------------- -$media-player-button-size: 1.5rem; - player { + --button-size: 2.0rem; + overlay { display: block; width: 100%; @@ -14,6 +14,48 @@ player { background: transparent; + .variants { + position: absolute; + bottom: calc(var(--button-size) + 1.0rem); + right: calc(var(--button-size) + 1.0rem); + z-index: 2; + line-height: 1.1rem; + font-size: 1.1rem; + padding: 0.1em; + margin: 0; + max-height: 50%; + height: calc(10 * 1em); + + appearance: none; + outline: none; + background: rgba(0, 0, 0, 0.9); + color: $theme-menu-color; + border: none; + + transition: height ease-in-out 100ms, bottom ease-in-out 500ms, opacity ease-in-out 100ms; + + option { + padding: 0.1em 0.25em; + margin: 0; + } + + option:hover, + option:focus { + background: hsla($theme-hue, 75%, 30%, 0.5); + color: $theme-menu-color-active; + } + + option:active { + background: hsla($theme-hue, 100%, 50%, 0.5); + color: $theme-menu-color-active; + } + } + + .variants.hide { + height: 0px; + opacity: 0.0; + } + controls { transition: bottom ease-in-out 100ms, opacity ease-in-out 100ms; display: flex; @@ -31,99 +73,95 @@ player { left: 0; right: 0; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 80%); + background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.9) 0.5rem); opacity: 1.0; overflow: hidden; user-select: none; - .play { + div { display: block; - flex-grow: 0; margin: 0; padding: 0; - width: $media-player-button-size; - height: $media-player-button-size; - font-size: $media-player-button-size; - line-height: $media-player-button-size; + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + text-align: center; user-select: none; } - .time { + input { display: block; margin: 0; padding: 0; + + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + + text-align: center; + user-select: none; + } + + .play { + width: var(--button-size); + height: var(--button-size); + + font-size: var(--button-size); + line-height: var(--button-size); + } + + .time { width: auto; - height: $media-player-button-size; + height: var(--button-size); flex-grow: 0; flex-shrink: 0; flex-basis: auto; text-align: right; - line-height: $media-player-button-size; + line-height: var(--button-size); user-select: auto; } .progress { - margin: 0; - padding: 0; width: auto; min-width: 5rem; - height: 100%; + height: var(--button-size); flex-grow: 5; flex-shrink: 1; - - display: block; - user-select: none; } .mute { - display: block; - margin: 0; - padding: 0; - width: $media-player-button-size; - height: $media-player-button-size; - flex-grow: 0; - flex-shrink: 0; - flex-basis: auto; - - font-size: $media-player-button-size; - line-height: $media-player-button-size; - text-align: center; - user-select: none; + width: var(--button-size); + height: var(--button-size); + font-size: var(--button-size); + line-height: var(--button-size); } .volume { - display: block; - margin: 0; - padding: 0; width: auto; min-width: 3rem; max-width: 6rem; - height: 100%; + height: var(--button-size); flex-grow: 1; flex-shrink: 5; flex-basis: auto; + } - user-select: none; + .variant { + width: var(--button-size); + height: var(--button-size); + font-size: var(--button-size); + line-height: var(--button-size); } .fullscreen { - display: block; - margin: 0; - padding: 0; - width: $media-player-button-size; - height: $media-player-button-size; - flex-grow: 0; - flex-shrink: 0; - flex-basis: auto; - - font-size: $media-player-button-size; - line-height: $media-player-button-size; - text-align: center; - user-select: none; + width: var(--button-size); + height: var(--button-size); + font-size: var(--button-size); + line-height: var(--button-size); } } } @@ -133,6 +171,12 @@ player.hide { cursor: none; overlay { + .variants { + bottom: -100%; + opacity: 0.0; + transition: bottom ease-in-out 500ms, opacity ease-in-out 500ms; + } + controls { bottom: -100%; opacity: 0.0; diff --git a/assets/site.js b/assets/site.js index d0e2608..9196c1c 100644 --- a/assets/site.js +++ b/assets/site.js @@ -24,14 +24,6 @@ async function xmr_media_lazyload_initialize() { class XMediaPlayer { constructor(element) { this._media = element; - this._variants = new Map(); - for (let source of this._media.querySelectorAll("source")) { - if (source.title) { - this._variants.set(source.url, source.title); - } else { - this._variants.set(source.url, (new URL(source.url)).pathname); - } - } // Replace the original video element with a player. this._container = document.createElement("player"); @@ -44,11 +36,31 @@ class XMediaPlayer { this._overlay = document.createElement("overlay"); this._container.appendChild(this._overlay); + // Build the variant menu. + this._variants = document.createElement("select"); + this._variants.size = 2; + this._variants.classList.add("variants", "hide"); + this._overlay.appendChild(this._variants); + console.log(this._media.currentSrc); + for (let source of this._media.querySelectorAll("source")) { + let variant = document.createElement("option") + if (source.src == this._media.currentSrc) { + variant.selected = true; + } + variant.value = source.src; + if (source.title) { + variant.innerText = source.title; + } else { + variant.innerText = (new URL(source.src)).pathname; + } + this._variants.appendChild(variant); + } + // Build the control overlay. this._controls = document.createElement("controls"); this._overlay.appendChild(this._controls); // - Play Button - this._controlPlay = document.createElement("span"); + this._controlPlay = document.createElement("div"); this._controlPlay.classList.add("play", "symbol") this._controlPlay.tabIndex = 0; this._controlPlay.dataset.symbol = "⏵"; @@ -71,7 +83,7 @@ class XMediaPlayer { } this._controls.appendChild(this._controlPlay); // - Time Display - this._controlTime = document.createElement("span"); + this._controlTime = document.createElement("div"); this._controlTime.classList.add("time"); this._controlTime.innerHTML = "00:00 / 00:00"; this._controlTime.update = () => { @@ -129,7 +141,7 @@ class XMediaPlayer { } this._controls.appendChild(this._controlProgress); // - Mute Button - this._controlMute = document.createElement("span"); + this._controlMute = document.createElement("div"); this._controlMute.classList.add("mute", "symbol"); this._controlMute.tabIndex = 0; this._controlMute.ariaLabel = "Mute"; @@ -174,15 +186,25 @@ class XMediaPlayer { } } this._controls.appendChild(this._controlVolume); + // - Variant Button + this._controlVariant = document.createElement("div"); + this._controlVariant.classList.add("variant", "symbol"); + this._controlVariant.tabIndex = 0; + this._controlVariant.ariaLabel = "Change Variant"; + this._controlVariant.dataset.symbol = "⚙"; + this._controlVariant.update = () => { + this._controlVariant.value = this._media.currentSrc; + }; + this._controls.appendChild(this._controlVariant); // - Fullscreen Button - this._controlFullscreen = document.createElement("span"); + this._controlFullscreen = document.createElement("div"); this._controlFullscreen.classList.add("fullscreen", "symbol"); this._controlFullscreen.tabIndex = 0; this._controlFullscreen.ariaLabel = "Toggle Fullscreen"; this._controlFullscreen.dataset.symbol = "⛶"; this._controlFullscreen.update = () => { - if (document.fullscreenElement) { - this._controlFullscreen.dataset.symbol = "\u200F⛶"; + if (document.fullscreenElement == this._container) { + this._controlFullscreen.dataset.symbol = "\u200F⬚"; } else { this._controlFullscreen.dataset.symbol = "⛶"; } @@ -209,6 +231,12 @@ class XMediaPlayer { for (let event of ["Input", "KeyDown", "Wheel"]) { this._controlVolume.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); } + for (let event of ["Input"]) { + this._variants.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["Click", "KeyDown"]) { + this._controlVariant.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } for (let event of ["Click", "KeyDown"]) { this._controlFullscreen.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); } @@ -219,6 +247,7 @@ class XMediaPlayer { this._controlProgress.update(); this._controlMute.update(); this._controlVolume.update(); + this._controlVariant.update(); this._controlFullscreen.update(); // And tell the media element to load. @@ -320,6 +349,8 @@ class XMediaPlayer { this.toggleMute(); } else if (ev.target == this._controlFullscreen) { this.toggleFullscreen(); + } else if (ev.target == this._controlVariant) { + this.toggleVariants(); } else { // If we can't handle this, let it fall to the next handler. return @@ -350,6 +381,12 @@ class XMediaPlayer { if (["Space", "Enter"].includes(ev.code)) { // Most likely an attempt to simulate a click. ev.target.click(); + } else if (["Escape"].includes(ev.key)) { + if (document.fullscreenElement) { + this.toggleFullscreen(false); + } else { + this.toggleTheater(false); + } } else if (["f", "F"].includes(ev.key)) { this._controlFullscreen.click(); } else if (["m", "M"].includes(ev.key)) { @@ -389,6 +426,7 @@ class XMediaPlayer { ev.stopPropagation(); } _onInput(ev) { + this.toggleVariants(false); if (ev.target == this._controlProgress) { ev.preventDefault(); if (isFinite(this._media.duration)) { @@ -407,9 +445,54 @@ class XMediaPlayer { let vol = (val - min) / dlt; this._media.volume = vol; this._media.muted = (this._media.volume < 0.01); + } else if (ev.target == this._variants) { + // Switch out the video file. + // Store current state and pause. + let paused = this._media.paused; + let muted = this._media.muted; + let volume = this._media.volume; + let time = this._media.currentTime; + this._media.pause(); + this._controlPlay.update(); + + // Tell the this._media source to begin loading. + this._media.src = this._variants.value; + this._media.load(); + this._media.currentTime = time; + + this._media.addEventListener("loadeddata", () => { + this._media.currentTime = time; + this._media.volume = volume; + this._media.muted = muted; + + ev.preventDefault(); + ev.stopPropagation(); + }, { + "once": true, + "capture": true + }) + this._media.addEventListener("seeked", () => { + if (paused) { + this._media.pause(); + } else { + this._media.play(); + } + this._controlPlay.update(); + + ev.preventDefault(); + ev.stopPropagation(); + }, { + "once": true, + "capture": true + }); } } - _onChange(ev) { } + _onChange(ev) {} + + // Visibility + _onFullscreenChange(ev) { + this._controlFullscreen.update(); + } // Control Functions togglePlayPause() { @@ -424,9 +507,19 @@ class XMediaPlayer { this._media.muted = !this._media.muted; } - toggleFullscreen() { - if (document.fullscreenElement) { - document.exitFullscreen(); + toggleVariants(state) { + if ((state == false) || (!this._variants.classList.contains("hide"))) { + this._variants.classList.add("hide"); + } else { + this._variants.classList.remove("hide"); + } + } + + toggleFullscreen(state) { + if ((state == false) || (document.fullscreenElement == this._container)) { + if (document.fullscreenElement) { + document.exitFullscreen(); + } } else { this._container.requestFullscreen(); } @@ -500,42 +593,6 @@ async function xmr_initialize_player(el) { } variant.value = this._media.currentSrc; } - - { // Hide/Show controls - el.showOverlay = function () { - el.cancelHideOverlay(); - el.classList.remove("hide"); - } - el.hideOverlay = function () { - } - el.delayHideOverlay = function () { - el.cancelHideOverlay(); - el.timer = setTimeout(() => { el.hideOverlay(); }, 2500); - } - el.cancelHideOverlay = function () { - if (el.timer) { - clearTimeout(el.timer); - } - } - el.addEventListener("mousemove", () => { - el.showOverlay(); - if (!this._media.paused && !this._media.ended && !this._media.error) { - el.delayHideOverlay(); - } - }); - el.hideOverlay(); - } - - { // Show/Hide Audio controls without audio. - function checkAudioPresence() { - } - this._media.addEventListener("loadedmetadata", () => { checkAudioPresence() }); - this._media.addEventListener("loadeddata", () => { checkAudioPresence() }); - checkAudioPresence(); - } - - // Signal the browser to try and load some information. - this._media.load(); } */ diff --git a/assets/site.scss b/assets/site.scss index df6b451..c9d713e 100644 --- a/assets/site.scss +++ b/assets/site.scss @@ -5,6 +5,7 @@ symbols: - code: 0023F5 - code: 0023F8 - code: 0023F9 + - code: 002699 - code: 0026F6 combos: - code: 00200F diff --git a/assets/symbols/u002699.svg b/assets/symbols/u002699.svg new file mode 100644 index 0000000..0e47e3a --- /dev/null +++ b/assets/symbols/u002699.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + +