Fix player UI: sports Highlights pane, scrubber knob, volume knob, description + save-to-playlist
Player/UI fixes:
- Sports (match) Highlights pane: move the toggle click-handler script out of
the always-false @if(false ...) legacy block so the "Highlights" button opens,
and emit videoId/isOwner with @json instead of {{ json_encode() }} (escaped
quotes crashed the data/tab/CRUD block once route keys became hashid strings).
- Progress scrubber knob (video + audio players): center on the track line with
translate(-50%, -50%); drop the margin-top hack that floated it above the line.
- Music player volume: give the slider wrap height so overflow:hidden no longer
crops the knob; add a subtle thumb shadow for visibility.
- Music language menu: cap height to available space above the header, sticky
header, internal scroll; keep controls from auto-hiding while the menu is open.
- Description box: re-check "Show more" overflow at every settle point (fonts,
images, rAF, tab reopen) so SPA autonext keeps the expand button.
Also bundles pending work: save-to-playlist button component, playlist/channel
views, PlaylistController + NasSyncService, and component-usage tracker updates.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
80948efff7
commit
6b43a7060a
14 files changed
+236
-136
No files matched your search
@@ -384,15 +384,17 @@
|
||||
background: #f00; border-radius: 2px; width: 0; pointer-events: none;
|
||||
}
|
||||
.ytp-scrubber-container {
|
||||
position: absolute; top: 50%; transform: translateY(-50%);
|
||||
position: absolute; top: 50%;
|
||||
width: 0; pointer-events: none;
|
||||
}
|
||||
.ytp-scrubber-button {
|
||||
width: 13px; height: 13px; border-radius: 50%; background: #f00;
|
||||
transform: translate(-50%, 0) scale(0); transition: transform .1s; margin-top: -4px;
|
||||
/* Center the knob on the play head: -50% X on the track position, -50% Y so the
|
||||
track line passes exactly through the knob's center at any bar height. */
|
||||
transform: translate(-50%, -50%) scale(0); transition: transform .1s;
|
||||
}
|
||||
.ytp-progress-bar-container:hover .ytp-scrubber-button,
|
||||
.ytp-progress-bar.dragging .ytp-scrubber-button { transform: translate(-50%, 0) scale(1); }
|
||||
.ytp-progress-bar.dragging .ytp-scrubber-button { transform: translate(-50%, -50%) scale(1); }
|
||||
.ytp-hover-time {
|
||||
position: absolute; bottom: 16px;
|
||||
background: rgba(28,28,28,.9); color: #fff;
|
||||
@@ -422,6 +424,9 @@
|
||||
.ytp-volume-slider-wrap {
|
||||
overflow: hidden; width: 0; transition: width .2s;
|
||||
display: flex; align-items: center;
|
||||
/* Height must clear the 13px thumb — the 3px track alone leaves the wrap ~3px
|
||||
tall, and overflow:hidden (kept for the width collapse) would crop the knob. */
|
||||
height: 16px;
|
||||
}
|
||||
.ytp-volume-area:hover .ytp-volume-slider-wrap,
|
||||
.ytp-volume-area:focus-within .ytp-volume-slider-wrap { width: 60px; }
|
||||
@@ -431,8 +436,8 @@
|
||||
background: linear-gradient(to right, #fff var(--vol,50%), rgba(255,255,255,.3) var(--vol,50%));
|
||||
outline: none; cursor: pointer; margin: 0 4px;
|
||||
}
|
||||
.ytp-volume-range::-webkit-slider-thumb { -webkit-appearance: none; width: 13px; height: 13px; border-radius: 50%; background: #fff; cursor: pointer; }
|
||||
.ytp-volume-range::-moz-range-thumb { width: 13px; height: 13px; border-radius: 50%; background: #fff; border: none; cursor: pointer; }
|
||||
.ytp-volume-range::-webkit-slider-thumb { -webkit-appearance: none; width: 13px; height: 13px; border-radius: 50%; background: #fff; cursor: pointer; box-shadow: 0 0 2px rgba(0,0,0,.6); }
|
||||
.ytp-volume-range::-moz-range-thumb { width: 13px; height: 13px; border-radius: 50%; background: #fff; border: none; cursor: pointer; box-shadow: 0 0 2px rgba(0,0,0,.6); }
|
||||
|
||||
.ytp-time-display { font-size: 13px; color: #fff; white-space: nowrap; padding: 0 6px; line-height: 36px; }
|
||||
.ytp-time-sep { opacity: .6; margin: 0 2px; }
|
||||
@@ -444,11 +449,14 @@
|
||||
.ytp-lang-popup {
|
||||
display: none; position: absolute; bottom: 48px; right: 0;
|
||||
background: rgba(28,28,28,.97); border-radius: 12px;
|
||||
min-width: 180px; overflow: hidden;
|
||||
min-width: 180px; overflow-x: hidden; overflow-y: auto;
|
||||
max-height: 320px; /* JS narrows this on open to fit above the header/player top */
|
||||
-webkit-overflow-scrolling: touch; overscroll-behavior: contain;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,.7); z-index: 100;
|
||||
}
|
||||
.ytp-lang-popup.open { display: block; }
|
||||
.ytp-lang-popup-hdr {
|
||||
position: sticky; top: 0; z-index: 1; background: rgba(28,28,28,.97);
|
||||
padding: 10px 14px 8px; font-size: 11px; font-weight: 700; letter-spacing: .5px;
|
||||
text-transform: uppercase; color: rgba(255,255,255,.45);
|
||||
border-bottom: 1px solid rgba(255,255,255,.1);
|
||||
@@ -682,10 +690,15 @@ function updateProgress() {
|
||||
}
|
||||
|
||||
// ── Controls visibility ──────────────────────────────────────
|
||||
// True while any control menu is open — the chrome must not auto-hide underneath
|
||||
// an open menu (it would fade the language/settings popup out mid-selection).
|
||||
function isMenuOpen() {
|
||||
return document.querySelector('.ytp-lang-popup.open, .ytp-settings-panel.open') !== null;
|
||||
}
|
||||
function showControls() {
|
||||
player.classList.remove('controls-hidden');
|
||||
clearTimeout(hideTimer);
|
||||
if (!audio.paused) hideTimer = setTimeout(() => player.classList.add('controls-hidden'), 3000);
|
||||
if (!audio.paused && !isMenuOpen()) hideTimer = setTimeout(() => player.classList.add('controls-hidden'), 3000);
|
||||
}
|
||||
|
||||
// ── Play / Pause ─────────────────────────────────────────────
|
||||
@@ -797,14 +810,34 @@ const langOpts = document.querySelectorAll('.ytp-lang-option');
|
||||
window._ytpTrackId = 0;
|
||||
|
||||
if (langBtn && langPopup) {
|
||||
// Cap the popup so it can never grow past the top of the video player or under
|
||||
// the fixed site header — whichever edge is lower. It opens upward from the
|
||||
// control bar, so without this a long track list slides its first rows off-screen.
|
||||
const sizeLangPopup = () => {
|
||||
const btnRect = langBtn.getBoundingClientRect();
|
||||
// 48px = the popup's `bottom` offset above the control-bar button row.
|
||||
const popupBottomY = btnRect.top - 8;
|
||||
// Never rise above the player's own top edge, nor above the fixed header (~56px).
|
||||
const playerTop = wrap.getBoundingClientRect().top;
|
||||
const topBoundary = Math.max(playerTop, 56) + 8;
|
||||
const avail = Math.max(120, popupBottomY - topBoundary);
|
||||
langPopup.style.maxHeight = avail + 'px';
|
||||
};
|
||||
langBtn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
// Only open popup when there are multiple language options
|
||||
if (langPopup.querySelectorAll('.ytp-lang-option').length < 2) return;
|
||||
settingsPanel.classList.remove('open');
|
||||
const willOpen = !langPopup.classList.contains('open');
|
||||
if (willOpen) { sizeLangPopup(); clearTimeout(hideTimer); }
|
||||
langPopup.classList.toggle('open');
|
||||
if (!willOpen) showControls(); // menu closed → resume normal auto-hide
|
||||
});
|
||||
document.addEventListener('click', () => {
|
||||
if (!langPopup.classList.contains('open')) return;
|
||||
langPopup.classList.remove('open');
|
||||
showControls(); // menu closed → resume normal auto-hide
|
||||
});
|
||||
document.addEventListener('click', () => langPopup.classList.remove('open'));
|
||||
langPopup.addEventListener('click', e => e.stopPropagation());
|
||||
|
||||
langOpts.forEach(opt => {
|
||||
@@ -830,6 +863,7 @@ if (langBtn && langPopup) {
|
||||
? `<span class="fi fi-${flag}" style="width:22px;height:16px;border-radius:2px;display:inline-block;"></span>`
|
||||
: langBtn.innerHTML;
|
||||
langPopup.classList.remove('open');
|
||||
showControls(); // menu closed → resume normal auto-hide
|
||||
|
||||
// Update title flag
|
||||
const titleFlagEl = document.getElementById('videoTitleFlag');
|
||||
@@ -1401,6 +1435,7 @@ window._audioPlayerUpdate = function(d) {
|
||||
langPopup.querySelectorAll('.ytp-lang-option').forEach(function(o) { o.classList.remove('active'); });
|
||||
opt.classList.add('active');
|
||||
langPopup.classList.remove('open');
|
||||
showControls(); // menu closed → resume normal auto-hide
|
||||
var _vol = audio.volume, _muted = audio.muted;
|
||||
audio.src = url; audio.load();
|
||||
audio.volume = _vol; audio.muted = _muted;
|
||||
|
||||
@@ -95,6 +95,9 @@ function switchVdbTab(panelId, btn) {
|
||||
document.querySelectorAll('.vdb-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById(panelId).classList.add('active');
|
||||
// About just became visible — re-measure now that its panel is laid out, since
|
||||
// a measurement taken while it was display:none would have read 0.
|
||||
if (panelId === 'vdb-about' && window._vdbScheduleOverflowCheck) window._vdbScheduleOverflowCheck();
|
||||
if (panelId === 'vdb-insights') {
|
||||
const panel = document.getElementById('vdb-insights');
|
||||
const currentUrl = panel && panel.dataset.insightsBase;
|
||||
@@ -128,7 +131,13 @@ function _vdbApplyActiveTab() {
|
||||
(function _vdbWatchSwaps() {
|
||||
const wrap = document.getElementById('vdbWrap');
|
||||
if (!wrap || wrap._vdbTabObserver) return;
|
||||
const obs = new MutationObserver(() => _vdbApplyActiveTab());
|
||||
const obs = new MutationObserver(() => {
|
||||
_vdbApplyActiveTab();
|
||||
// The swap replaced the description markup — re-evaluate the "Show more"
|
||||
// button once layout/fonts/images settle. Central here so every SPA swap
|
||||
// path is covered, not just the ones that remember to call it themselves.
|
||||
if (window._vdbScheduleOverflowCheck) window._vdbScheduleOverflowCheck();
|
||||
});
|
||||
obs.observe(wrap, { childList: true, subtree: false });
|
||||
wrap._vdbTabObserver = obs;
|
||||
})();
|
||||
@@ -147,9 +156,25 @@ function _vdbCheckOverflow() {
|
||||
const d = document.getElementById('vdbDescShort'), b = document.getElementById('vdbShowMore');
|
||||
if (!d || !b) return;
|
||||
if (d.classList.contains('vdb-expanded')) { b.style.display = 'flex'; return; }
|
||||
// If the About panel isn't laid out yet (e.g. Insights tab active, or mid-swap),
|
||||
// scrollHeight reads 0 — don't hide the button on a bogus zero measurement.
|
||||
if (d.scrollHeight === 0) return;
|
||||
b.style.display = (d.scrollHeight > 138) ? 'flex' : 'none';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', _vdbCheckOverflow);
|
||||
window.addEventListener('load', _vdbCheckOverflow);
|
||||
// A single measurement right after an SPA swap is unreliable: web-fonts, images
|
||||
// inside the description, and panel layout can all change the height a beat later,
|
||||
// each of which flips whether the clamp overflows. Re-check at every settling point.
|
||||
window._vdbScheduleOverflowCheck = window._vdbScheduleOverflowCheck || function () {
|
||||
requestAnimationFrame(_vdbCheckOverflow);
|
||||
setTimeout(_vdbCheckOverflow, 120);
|
||||
setTimeout(_vdbCheckOverflow, 400);
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(_vdbCheckOverflow);
|
||||
const d = document.getElementById('vdbDescShort');
|
||||
if (d) d.querySelectorAll('img').forEach(function (img) {
|
||||
if (!img.complete) img.addEventListener('load', _vdbCheckOverflow, { once: true });
|
||||
});
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', window._vdbScheduleOverflowCheck);
|
||||
window.addEventListener('load', window._vdbScheduleOverflowCheck);
|
||||
</script>
|
||||
@endif
|
||||
Reference in new issue
Block a user