From fac9b5ba2ba222b1d0015af1c20510efa8ae0425 Mon Sep 17 00:00:00 2001 From: Daniel Mejia Date: Fri, 13 Mar 2026 20:49:47 -0400 Subject: [PATCH 1/2] Improvement to hubpresenter --- .../site/assets/css/hubpresenter.css | 841 ++++++++++++------ .../com_resources/site/assets/css/video.css | 193 ++-- .../site/assets/js/hubpresenter.js | 345 ++++++- .../site/views/view/tmpl/watch.php | 174 +++- 4 files changed, 1113 insertions(+), 440 deletions(-) diff --git a/core/components/com_resources/site/assets/css/hubpresenter.css b/core/components/com_resources/site/assets/css/hubpresenter.css index 76b80a1d072..b8bd0ca9e5c 100644 --- a/core/components/com_resources/site/assets/css/hubpresenter.css +++ b/core/components/com_resources/site/assets/css/hubpresenter.css @@ -15,7 +15,21 @@ a:hover { border-bottom: 0; } a:focus { - outline: none; + outline: 2px solid #2074cb; + outline-offset: 2px; +} + +/* Screen-reader only utility class */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } #watch-section { @@ -93,7 +107,7 @@ a:focus { margin: 20px auto; border-radius: 12px; background: #1a1a1a; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6); + border: 1px solid rgba(255,255,255,0.06); overflow: hidden; } @@ -270,6 +284,7 @@ video { #list ul li.active, #list ul li.active:hover { background: #cddce4; + border-left: 3px solid #2074cb; } #list ul li:hover { cursor: pointer; @@ -299,125 +314,223 @@ video { #presenter-left { box-sizing: border-box; } -#progress-bar:before { - content: ''; - position: absolute; - top: -5px; - left: 0; - right: 0; + +/* Progress bar track */ +#progress-bar { + height: 4px; + background: rgba(255,255,255,0.2); + border-radius: 0; + border: none; + cursor: pointer; + transition: height 0.15s; +} +#presenter-content:hover #progress-bar, +#presenter-content.paused #progress-bar { height: 5px; } -#progress-bar, #progress-bar .ui-slider-range { + background: #2074cb; border-radius: 0; border: none; -} -#progress-bar { - background: #888; + height: 100%; } #progress-bar .ui-slider-handle { + width: 12px; + height: 12px; + top: -4px; + margin-left: -6px; + border-radius: 50%; + background: #fff; + border: none; + box-shadow: 0 1px 4px rgba(0,0,0,0.5); opacity: 0; - -webkit-transition: opacity 0.5s; - -moz-transition: opacity 0.5s; - transition: opacity 0.5s; + transition: opacity 0.2s; + outline: none; +} +#presenter-content.paused #progress-bar .ui-slider-handle, +#presenter-content:hover #progress-bar .ui-slider-handle { + opacity: 1; } + +/* Control box */ #control-box { - position: absolute; - bottom: -45px; - left: 0; - right: 1px; - height: 60px; - -webkit-transition: bottom 0.4s; - -moz-transition: bottom 0.4s; - transition: bottom 0.4s; - background: linear-gradient(to top, rgba(20, 20, 20, 0.95), rgba(20, 20, 20, 0.7)); - border-top: 1px solid rgba(255, 255, 255, 0.1); - backdrop-filter: blur(6px); - padding: 0 15px; - z-index: 9997; + position: absolute; + bottom: -52px; + left: 0; + right: 0; + height: 52px; + display: flex; + flex-direction: column; + background: linear-gradient(to top, rgba(10,10,10,0.96), rgba(20,20,20,0.75)); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-top: 1px solid rgba(255,255,255,0.08); + transition: bottom 0.3s ease; + z-index: 9997; } #presenter-content.paused #control-box, #presenter-content:hover #control-box { - -webkit-transition-delay: 0s; bottom: 0; + transition-delay: 0s; } -#presenter-content.paused #progress-bar .ui-slider-handle, -#presenter-content:hover #progress-bar .ui-slider-handle { - opacity: 1 -} + +/* Button row */ #control-buttons { - position: relative; - z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + flex: 1; + min-height: 0; } #control-buttons-left { - float: left; + display: flex; + align-items: center; + gap: 2px; } #control-buttons-right { - float: right; -} -#control-buttons-right .control:last-child { - border-right: none; + display: flex; + align-items: center; + gap: 2px; } + +/* Individual control buttons */ .control { - float: left; position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0; + border: none; + border-radius: 6px; + background: transparent; + color: rgba(255,255,255,0.6); + cursor: pointer; + /* hide legacy text */ + font-size: 0; + text-indent: 0; + transition: background 0.15s, color 0.15s; +} +.control svg { + width: 18px; + height: 18px; + flex-shrink: 0; +} +.control:hover { + background: rgba(255,255,255,0.12); + color: #fff; +} +.control:focus { + outline: none; +} +.control:focus-visible { + outline: 2px solid #2074cb; + outline-offset: 1px; +} +/* Remove old :before/:after pseudo icons — SVG replaces them */ +.control:before, +.control:after { + content: none !important; +} + +/* Play/pause is slightly larger */ +#play-pause { width: 40px; height: 40px; - color: transparent; - text-indent: -9999px; - border-bottom: none; - border-right: 1px solid #333; + color: #fff; } -.control:before { - position: absolute; - top: 9px; - left: 0; - right: 0; - text-align: center; - color: #777; - text-indent: 0px; - font-size: 20px; - line-height: 24px; - font-family: 'Fontcons'; - -webkit-font-smoothing: antialiased; +#play-pause svg { + width: 22px; + height: 22px; } -.control:hover .control-container { - bottom: 40px; - opacity: 1; - -webkit-transition-delay: 0.35s; + +/* Time readout */ +#media-progress { + font-size: 12px; + color: rgba(255,255,255,0.6); + padding: 0 10px; + font-variant-numeric: tabular-nums; + white-space: nowrap; + line-height: 1; } -.control:hover:before { - color: #fff; + +/* Volume slider popup — horizontal layout */ +.volume-controls { + min-width: 180px; + width: 180px; + height: auto; + padding: 14px 16px; + display: flex; + align-items: center; + gap: 10px; +} +.volume-controls #volume-bar { + flex: 1; + width: auto; + left: auto; + top: auto; + height: 4px; + position: relative; + border-radius: 2px; +} +.volume-controls #volume-bar .ui-slider-handle { + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.5); + margin-left: -6px; + margin-top: -4px; + display: block; + position: absolute; + outline: none; + cursor: pointer; } + +/* Control popups (settings, CC, link panels) */ .control-container { display: block; position: absolute; - min-width: 250px; - bottom: -200px; + min-width: 260px; + bottom: -300px; right: 0; - text-indent: 0px; - background: #222; - padding: 20px; - color: #efefef; + background: rgba(24,24,28,0.92); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 12px; + padding: 16px; + color: #ddd; z-index: 922; opacity: 0; - -webkit-transition: opacity 0.5s; + pointer-events: none; + transition: opacity 0.2s, bottom 0s 0.2s; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); } +.control.open .control-container, .control-container.fixed { - opacity: 1 !important; - bottom: 40px; + bottom: 46px; + opacity: 1; + pointer-events: auto; + transition: opacity 0.2s; } .control-container h3 { - margin: 0 0 10px 0; - padding: 0 0 10px 0; - font-size: 14px; - color: #eee; - border-bottom: 1px solid #333; + margin: 0 0 12px; + padding: 0 0 10px; + font-size: 13px; + font-weight: 600; + color: #fff; + border-bottom: 1px solid rgba(255,255,255,0.07); } .control-container h3 span { - color: #aaa; + font-weight: 400; + color: #888; } .control-container .grid { + display: flex; + align-items: center; + gap: 8px; margin-bottom: 10px; } .control-container .grid:last-child { @@ -425,150 +538,52 @@ video { } .control-container .label { font-size: 12px; - text-align: left; - color: #aaa; + color: #bbb; + flex-shrink: 0; } .control-container .input { - text-align: right; + flex: 1; } .control-container .input input, .control-container .input select { width: 100%; + background: #2a2a2a; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 4px; + color: #eee; + padding: 4px 6px; + font-size: 12px; } .control-container .hint { font-size: 11px; - color: #666; -} -#play-pause:before { - content: '\f04c'; - font-size: 24px; -} -#play-pause.paused:before { - content: '\25b6'; -} -#previous, -#next { - width: 30px; -} -#previous:before { - content: '\21E4'; - font-size: 14px; -} -#next:before { - content: '\21E5'; - font-size: 14px; -} -#media-progress { - float: left; - padding: 10px 20px; - color: #999; -} -#volume { - border-left: 1px solid #333; -} -#volume:before, -#volume.mute:before { - content: ''; - position: absolute; - top: 8px; - left: 8px; - width: 24px; - height: 24px; - content: '\f026'; -} -#volume.low:before { - content: '\f026'; -} -#volume.medium:before { - content: '\f027'; -} -#volume.high:before { - content: '\f028'; -} -.volume-controls { - min-width: inherit; - width: 40px; - height: 110px; - padding: 0; + color: #aaa; + margin-top: 4px; } -.volume-controls #volume-bar { - width: 10px; - left: 15px; - top: 15px; - height: 80px; - position: absolute; + +/* CC/subtitle button */ +#subtitle { + display: none; } -.volume-controls #volume-bar .ui-slider-handle { - margin-left: -2px; - margin-bottom: -7px; +#subtitle svg { display: block; - bottom: 80%; - background: #fff; - height: 14px; - width: 14px; - border-radius: 7px; - outline: none; - position: absolute; + width: 20px; + height: 20px; + margin: auto; } -#settings:before { - content: '\2699'; +#subtitle.on { + color: #fff; + background: rgba(32,116,203,0.3); + border-radius: 6px; + outline: 2px solid #2074cb; + outline-offset: -2px; } + +/* Link input */ #link .link-controls input { - background: #FFF; + background: #2a2a2a; + color: #eee; width: 100%; } -#link:before { - content: '\26D3'; -} -#switch:before { - content: '\f0a1'; -} -#switch:after { - content: '\f0a1'; - position: absolute; - top: 6px; - left: 0; - right: 8px; - text-align: right; - color: #777; - text-indent: 0px; - font-size: 16px; - line-height: 18px; - font-family: 'Fontcons'; - -webkit-font-smoothing: antialiased; -} -#switch:hover:after { - color: #fff; -} -#subtitle { - display: none; - width: 50px; - margin-right: -1px; - border-left: 1px solid #333; -} -#subtitle:before { - content: 'CC'; - position: absolute; - top: 12px; - left: 12px; - right: 12px; - height: 14px; - color: #555; - border: 2px solid #555; - border-radius: 2px; - font-size: 11px; - line-height: 14px; - font-weight: bold; -} -#subtitle:hover:before { - border-color: #ddd; - color: #ddd; -} -#subtitle.on:before { - background: #ddd; - color: #333; - border: 2px solid #ddd; -} .subtitle-controls .options-toggle { display: block; font-size: 11px; @@ -621,7 +636,7 @@ video { } /*--------------------------------------------------------------- - Subtitles + Subtitles / Closed Captions ---------------------------------------------------------------*/ #media { position: relative; @@ -637,7 +652,9 @@ video { right: 0; text-align: center; z-index: 888; + pointer-events: none; -webkit-transition: bottom 0.4s; + transition: bottom 0.4s; } #video-subtitles div { display: inline-block !important; @@ -645,16 +662,19 @@ video { padding: 0; font-size: 18px; font-weight: 400; - line-height: 1.2em; - color: #FFF; + line-height: 1.4em; + /* WCAG AA: white on black = 21:1 contrast */ + color: #FFFFFF; margin: 0 auto; min-width: 100px; - max-width: 700px; - background: rgba(0,0,0,1); + max-width: 75%; + background: rgba(0, 0, 0, 0.85); border-radius: 4px; + /* Prevent captions from bleeding to screen edge */ + word-break: break-word; } #video-subtitles div.showing { - padding: 5px 10px; + padding: 6px 12px; } /*--------------------------------------------------------------- @@ -1105,103 +1125,238 @@ a.ui-slider-handle { Transcript Viewer ---------------------------------------------------------------*/ #transcript-container { - padding: 0px; - margin: 0px; + padding: 0; + margin: 0; display: none; - border-bottom: 1px solid #ccc; - background: #FFF; + border-top: 3px solid #2074cb; + border-bottom: 1px solid #d0d0d0; + background: #fff; } #transcript-toolbar { - position: relative; - background: #efefef; - padding: 0; + display: flex; + align-items: center; + gap: 10px; + background: #f7f8fa; + padding: 0 14px; margin: 0; - border-bottom: 1px solid #ccc; - height: 36px; + border-bottom: 1px solid #e0e0e0; + height: 50px; + flex-wrap: wrap; } #transcript-select { - position: absolute; - top: 8px; - left: 10px; + flex: 1; + font-size: 13px; + font-weight: 600; + color: #444; + white-space: nowrap; } #transcript-select:before { - content: 'Language:'; - font-weight: bold; - font-size: 12px; + content: 'Language: '; + font-weight: 600; + font-size: 13px; } -#transcript-search { + +/* Search wrapper */ +#transcript-search-wrap { + position: relative; + display: flex; + align-items: center; + gap: 4px; +} +.transcript-search-icon { position: absolute; - top: 4px; - right: 10px; - background: #FFF; - font-size: 11px; - width: 150px; + left: 9px; + width: 16px; + height: 16px; + color: #888; + pointer-events: none; + flex-shrink: 0; } -#font-smaller, -#font-bigger { +#transcript-search { + padding: 6px 30px 6px 32px; + font-size: 14px; + width: 200px; + background: #fff; + border: 1px solid #c5c8cc; + border-radius: 6px; + color: #222; + line-height: 1.4; + -webkit-appearance: none; + appearance: none; +} +#transcript-search::placeholder { + color: #aaa; + font-size: 13px; +} +#transcript-search:focus { + outline: 2px solid #2074cb; + outline-offset: 0; + border-color: #2074cb; +} +/* Hide browser's native clear button on type=search */ +#transcript-search::-webkit-search-cancel-button { + -webkit-appearance: none; +} +#transcript-search-clear { position: absolute; - top: 10px; - right: 190px; + right: 6px; + display: flex; + align-items: center; + justify-content: center; width: 20px; height: 20px; + padding: 0; border: none; + background: transparent; + color: #888; + cursor: pointer; + border-radius: 50%; } -#font-bigger { - right: 170px; +#transcript-search-clear:hover { + background: #e5e5e5; + color: #333; } -#font-smaller:before, -#font-bigger:before { - content: '\f031'; - position: absolute; - top: 0; - left: 0; - font-family: 'Fontcons'; - font-size: 14px; - color: #555; +#transcript-search-clear:focus { + outline: 2px solid #2074cb; } -#font-bigger:before { - font-size: 24px; +#transcript-search-clear svg { + width: 14px; + height: 14px; } -#font-smaller:hover:before, -#font-bigger:hover:before { - color: #000; +#transcript-search-count { + display: none; + font-size: 12px; + color: #666; + white-space: nowrap; + flex-shrink: 0; } -#font-smaller.inactive:before, -#font-bigger.inactive:before { - color: #aaa; + +/* Font size controls */ +#transcript-font-controls { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} +#font-smaller, +#font-bigger { + /* Reset any inherited positioning from prior rules */ + position: static; + top: auto; + right: auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid #c5c8cc; + border-radius: 6px; + background: #fff; + color: #444; + cursor: pointer; + flex-shrink: 0; + font-family: inherit; +} +#font-smaller svg, +#font-bigger svg { + width: 18px; + height: 18px; +} +#font-smaller:hover, +#font-bigger:hover { + background: #e8f0fb; + border-color: #2074cb; + color: #1a5fac; +} +#font-smaller:focus, +#font-bigger:focus { + outline: 2px solid #2074cb; + outline-offset: 1px; +} +/* Remove old :before pseudo-icon — SVG is used instead */ +#font-smaller:before, +#font-bigger:before { + content: none; +} +#font-smaller.inactive, +#font-bigger.inactive { + opacity: 0.35; + pointer-events: none; } + +/* Transcript scroll area */ #transcripts { - overflow-y: scroll; - height: 150px; + overflow-y: auto; + height: calc(100vh - 420px); + min-height: 150px; + scroll-behavior: smooth; +} +#transcripts:focus { + outline: 2px solid #2074cb; + outline-offset: -2px; } .transcript { - padding: 0 10px; - margin: 10px 0; + padding: 4px 14px 10px; display: none; } .transcript-line { - overflow: hidden; - padding: 0 3px; - font-size: 12px; - line-height: 16px; - border-radius: 3px; + display: flex; + align-items: baseline; + gap: 10px; + padding: 7px 8px; + font-size: 15px; + line-height: 1.55; + border-radius: 5px; + border-left: 3px solid transparent; + cursor: pointer; + transition: background 0.12s, border-color 0.12s; + min-height: 36px; } .transcript-line:hover { - background: #efeffe; - cursor: pointer; + background: #edf2fb; +} +.transcript-line:focus { + outline: 2px solid #2074cb; + outline-offset: 0; + background: #edf2fb; } +/* Active: blue left-border + background — distinct from colour alone (WCAG 1.4.11) */ .transcript-line.active, .transcript-line.active:hover { - background: #f0f0f0; - font-weight: bold; + background: #e8f0fb; + border-left-color: #2074cb; + font-weight: 600; } .transcript-line-time { - float: left; - margin-right: 10px; - color: #aaa; + flex-shrink: 0; + font-size: 12px; + color: #888; + font-variant-numeric: tabular-nums; + min-width: 42px; +} +.transcript-line.active .transcript-line-time { + color: #1558a0; + font-weight: 600; } .transcript-line-text { - color: #444; + color: #2a2a2a; + flex: 1; +} +.transcript-line.active .transcript-line-text { + color: #111; +} +/* Highlighted search terms */ +.transcript-line-text .highlight { + background: #ffeb3b; + color: #111; + border-radius: 2px; + padding: 0 2px; + font-weight: inherit; +} +/* Lines hidden during active search */ +.transcript-line.search-hidden { + display: none; } /*--------------------------------------------------------------- @@ -1229,3 +1384,145 @@ a.ui-slider-handle { display: none; } +/*--------------------------------------------------------------- + Fullscreen +---------------------------------------------------------------*/ +#presenter-container:fullscreen, +#presenter-container:-webkit-full-screen { + width: 100vw; + max-width: 100vw; + height: 100vh; + margin: 0; + border-radius: 0; + display: flex; + flex-direction: row; + background: #000; + overflow: hidden; +} + +/* Hide header and bottom chrome in fullscreen */ +#presenter-container:fullscreen #presenter-header, +#presenter-container:fullscreen .bottom-controls, +#presenter-container:-webkit-full-screen #presenter-header, +#presenter-container:-webkit-full-screen .bottom-controls { + display: none; +} + +/* Content (slides + video) takes 75% width */ +#presenter-container:fullscreen #presenter-content, +#presenter-container:-webkit-full-screen #presenter-content { + flex: 3; + min-width: 0; + height: 100%; + display: flex; + flex-direction: row; +} + +/* Left panel (slides) fills ~67% of content */ +#presenter-container:fullscreen #presenter-left, +#presenter-container:-webkit-full-screen #presenter-left { + flex: 2; + min-width: 0; + height: 100%; + float: none; + width: auto; +} + +/* Slides fill the left column */ +#presenter-container:fullscreen #presenter-left #slides, +#presenter-container:-webkit-full-screen #presenter-left #slides { + width: 100%; + height: calc(100% - 60px); + border: none; +} + +#presenter-container:fullscreen #presenter-left #slides ul li, +#presenter-container:-webkit-full-screen #presenter-left #slides ul li { + height: 100%; +} + +#presenter-container:fullscreen #presenter-left #slides ul li img, +#presenter-container:-webkit-full-screen #presenter-left #slides ul li img { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* Right panel (video + slide list) fills ~33% of content */ +#presenter-container:fullscreen #presenter-right, +#presenter-container:-webkit-full-screen #presenter-right { + flex: 1; + min-width: 0; + height: 100%; + float: none; + width: auto; + display: flex; + flex-direction: column; +} + +/* Video fills right panel */ +#presenter-container:fullscreen #presenter-right #media, +#presenter-container:-webkit-full-screen #presenter-right #media { + flex: 1; + height: auto; + min-height: 0; + border: none; + background: #000; +} + +#presenter-container:fullscreen #presenter-right #media video, +#presenter-container:-webkit-full-screen #presenter-right #media video { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* Slide list fills remaining right-panel space */ +#presenter-container:fullscreen #list, +#presenter-container:-webkit-full-screen #list { + flex: 1; + min-height: 0; + overflow: hidden; +} + +#presenter-container:fullscreen #list_items, +#presenter-container:-webkit-full-screen #list_items { + height: 100%; +} + +/* Control box stays pinned at bottom of left panel */ +#presenter-container:fullscreen #control-box, +#presenter-container:-webkit-full-screen #control-box { + bottom: 0; +} + +/* Transcript panel: 25% width on the right, full height, visible in fullscreen */ +#presenter-container:fullscreen #transcript-container, +#presenter-container:-webkit-full-screen #transcript-container { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + height: 100%; + max-width: 25%; + background: #1a1a1a; + border-left: 1px solid #333; + overflow: hidden; +} + +#presenter-container:fullscreen #transcript-container #transcript-toolbar, +#presenter-container:-webkit-full-screen #transcript-container #transcript-toolbar { + flex-shrink: 0; + background: #222; + border-bottom: 1px solid #333; +} + +#presenter-container:fullscreen #transcript-container #transcripts, +#presenter-container:-webkit-full-screen #transcript-container #transcripts { + flex: 1; + height: auto; + min-height: 0; + overflow-y: auto; + padding: 8px; +} + diff --git a/core/components/com_resources/site/assets/css/video.css b/core/components/com_resources/site/assets/css/video.css index 9f2c3faafd5..1e0763b81fc 100644 --- a/core/components/com_resources/site/assets/css/video.css +++ b/core/components/com_resources/site/assets/css/video.css @@ -108,7 +108,7 @@ font-family: 'Fontcons'; -webkit-font-smoothing: antialiased; } - .control:hover .control-container { + .control.open .control-container { bottom: 40px; opacity: 1; -webkit-transition-delay: 0.35s; @@ -214,21 +214,25 @@ content: '\f028'; } .volume-controls { - min-width: inherit; - width: 40px; - height: 110px; - padding: 0; + min-width: 180px; + width: 180px; + height: auto; + padding: 14px 16px; + display: flex; + align-items: center; + gap: 10px; } .volume-controls #volume-bar { - width: 10px; - left: 15px; - top: 15px; - height: 80px; - position: absolute; + flex: 1; + width: auto; + left: auto; + top: auto; + height: 4px; + position: relative; } .volume-controls #volume-bar .ui-slider-handle { - margin-left: -2px; - margin-bottom: -8px; + margin-left: -6px; + margin-top: -4px; } #settings:before { content: '\2699'; @@ -639,106 +643,145 @@ } /*--------------------------------------------------------------- - Transcript Viewer + Transcript Viewer (video.css — defer to hubpresenter.css) ---------------------------------------------------------------*/ #transcript-container { - padding: 0px; - margin: 0px; + padding: 0; + margin: 0; display: none; - border-bottom: 1px solid #ccc; - background: #FFF; + border-top: 3px solid #2074cb; + border-bottom: 1px solid #d0d0d0; + background: #fff; } #transcript-toolbar { - position: relative; - background: #efefef; - padding: 0; + position: static; + display: flex; + align-items: center; + gap: 10px; + background: #f7f8fa; + padding: 0 14px; margin: 0; - border-bottom: 1px solid #ccc; - height: 36px; + border-bottom: 1px solid #e0e0e0; + height: 50px; + flex-wrap: wrap; } #transcript-select { - position: absolute; - top: 8px; - left: 10px; + position: static; + top: auto; + left: auto; + flex: 1; + font-size: 13px; + font-weight: 600; + color: #444; + white-space: nowrap; } #transcript-select:before { content: 'Language: '; - font-weight: bold; - font-size: 12px; + font-weight: 600; + font-size: 13px; + } + #transcript-search-wrap { + position: relative; + display: flex; + align-items: center; } #transcript-search { - position: absolute; - top: 4px; - right: 10px; - background: #FFF; - font-size: 11px; - width: 150px; + position: static; + top: auto; + right: auto; + padding: 6px 30px 6px 32px; + font-size: 14px; + width: 200px; + background: #fff; + border: 1px solid #c5c8cc; + border-radius: 6px; + color: #222; } #font-smaller, #font-bigger { - position: absolute; - top: 10px; - right: 190px; - width: 20px; - height: 20px; - border: none; - } - #font-bigger { - right: 170px; + position: static; + top: auto; + right: auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid #c5c8cc; + border-radius: 6px; + background: #fff; + color: #444; } #font-smaller:before, #font-bigger:before { - content: '\f031'; - position: absolute; - top: 0; - left: 0; - font-family: 'Fontcons'; - font-size: 14px; - color: #555; + content: none; } - #font-bigger:before { - font-size: 24px; - } - #font-smaller:hover:before, - #font-bigger:hover:before { - color: #000; - } - #font-smaller.inactive:before, - #font-bigger.inactive:before { - color: #aaa; + #font-smaller svg, + #font-bigger svg { + width: 18px; + height: 18px; } #transcripts { - overflow-y: scroll; - height: 150px; + overflow-y: auto; + height: calc(100vh - 420px); + min-height: 150px; + scroll-behavior: smooth; } .transcript { - padding: 0 10px; - margin: 10px 0; + padding: 4px 14px 10px; + margin: 0; display: none; } .transcript-line { - overflow: hidden; - padding: 0 3px; - font-size: 12px; - line-height: 16px; - border-radius: 3px; + display: flex; + align-items: baseline; + gap: 10px; + padding: 7px 8px; + font-size: 15px; + line-height: 1.55; + border-radius: 5px; + border-left: 3px solid transparent; + overflow: visible; } .transcript-line:hover { - background: #efeffe; + background: #edf2fb; cursor: pointer; } .transcript-line.active, .transcript-line.active:hover { - background: #f0f0f0; - font-weight: bold; + background: #e8f0fb; + border-left-color: #2074cb; + font-weight: 600; } .transcript-line-time { - float: left; - margin-right: 10px; - color: #aaa; + float: none; + flex-shrink: 0; + font-size: 12px; + color: #888; + font-variant-numeric: tabular-nums; + min-width: 42px; + margin-right: 0; + } + .transcript-line.active .transcript-line-time { + color: #1a5fac; } .transcript-line-text { - color: #444; + color: #2a2a2a; + flex: 1; + } + .transcript-line.active .transcript-line-text { + color: #111; + } + .transcript-line-text .highlight { + background: #ffeb3b; + color: #111; + border-radius: 2px; + padding: 0 2px; + } + /* Lines hidden during search that don't match */ + .transcript-line.search-hidden { + display: none; } /*--------------------------------------------------------------- diff --git a/core/components/com_resources/site/assets/js/hubpresenter.js b/core/components/com_resources/site/assets/js/hubpresenter.js index e47bcf149e3..769a0c92661 100644 --- a/core/components/com_resources/site/assets/js/hubpresenter.js +++ b/core/components/com_resources/site/assets/js/hubpresenter.js @@ -19,10 +19,12 @@ HUB.Presenter = (() => { subtitles: null, transcriptLineActive: 0, transcriptBoxScrolling: false, + _transcriptScrollTimer: null, canSendTracking: true, sendingTracking: false, detailedTrackingId: null, doneLoading: false, + suppressAutoplay: false, audio: false, current: 0, duration: 0 @@ -311,6 +313,43 @@ HUB.Presenter = (() => { // Control bar const controls = { init() { + // Toggle popups on click; close when clicking outside + $(document).on('click', '.control', function (e) { + const btn = $(this); + // Buttons with no popup (no .control-container child) do nothing here + if (!btn.find('.control-container').length) return; + e.stopPropagation(); + const isOpen = btn.hasClass('open'); + // Close all other open popups + $('.control.open').not(btn).removeClass('open').attr('aria-expanded', 'false'); + // Toggle this one + if (isOpen) { + btn.removeClass('open').attr('aria-expanded', 'false'); + } else { + btn.addClass('open').attr('aria-expanded', 'true'); + } + }); + // Prevent clicks inside popup from closing it + $(document).on('click', '.control-container', function (e) { + e.stopPropagation(); + }); + // Close all popups when clicking outside + $(document).on('click', function () { + $('.control.open').removeClass('open').attr('aria-expanded', 'false'); + }); + // Close all popups on Escape key + $(document).on('keydown', function (e) { + if (e.key === 'Escape') { + $('.control.open').removeClass('open').attr('aria-expanded', 'false'); + } + }); + // Close all popups when the toolbar hides (mouse leaves presenter while playing) + $('#presenter-content').on('mouseleave', function () { + if (!$(this).hasClass('paused')) { + $('.control.open').removeClass('open').attr('aria-expanded', 'false'); + } + }); + $('#play-pause').on('click', (e) => { e.preventDefault(); this.playPause(true); @@ -335,8 +374,7 @@ HUB.Presenter = (() => { this.switchVideo(); }); - $('#link').on('mouseover', (e) => { - e.preventDefault(); + $('#link').on('click', () => { this.linkVideo(); }); @@ -351,17 +389,24 @@ HUB.Presenter = (() => { progressBar.init(); volumeBar.init(); + fullscreen.init(); }, playPause(clicking) { const paused = player.isPaused(); if (paused) { - $('#play-pause').removeClass('playing').addClass('paused'); + $('#play-pause').removeClass('playing').addClass('paused') + .attr('aria-label', 'Play presentation').attr('aria-pressed', 'false'); + $('#play-pause .icon-pause').hide(); + $('#play-pause .icon-play').show(); $('#presenter-content').addClass('paused'); if (clicking) player.get().play(); } else { - $('#play-pause').removeClass('paused').addClass('playing'); + $('#play-pause').removeClass('paused').addClass('playing') + .attr('aria-label', 'Pause presentation').attr('aria-pressed', 'true'); + $('#play-pause .icon-play').hide(); + $('#play-pause .icon-pause').show(); $('#presenter-content').removeClass('paused'); if (clicking) player.get().pause(); } @@ -402,8 +447,9 @@ HUB.Presenter = (() => { linkVideo() { const url = this.getTimestampUrl(); - $('.link-controls input') + $('#timestamp-link') .val(url) + .off('click') .on('click', function () { $(this).select(); }); @@ -451,7 +497,7 @@ HUB.Presenter = (() => { step: 0.1, min: 0, max: 1, - orientation: 'vertical', + orientation: 'horizontal', slide: (e, ui) => { this.updateIcon(ui.value * 100); player.setVolume(ui.value); @@ -461,13 +507,11 @@ HUB.Presenter = (() => { }, updateIcon(volume) { - const icon = $('#volume'); - icon.removeClass('mute low medium high'); - - if (volume === 0) icon.addClass('mute'); - else if (volume <= 33) icon.addClass('low'); - else if (volume <= 66) icon.addClass('medium'); - else icon.addClass('high'); + $('#volume .icon-vol-high, #volume .icon-vol-medium, #volume .icon-vol-low, #volume .icon-vol-mute').hide(); + if (volume === 0) $('#volume .icon-vol-mute').show(); + else if (volume <= 33) $('#volume .icon-vol-low').show(); + else if (volume <= 66) $('#volume .icon-vol-medium').show(); + else $('#volume .icon-vol-high').show(); } }; @@ -527,6 +571,68 @@ HUB.Presenter = (() => { } }; + // Fullscreen + const fullscreen = { + get isFullscreen() { + return !!(document.fullscreenElement || document.webkitFullscreenElement); + }, + + enter() { + const el = document.getElementById('presenter-container'); + if (el.requestFullscreen) el.requestFullscreen(); + else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen(); + }, + + exit() { + if (document.exitFullscreen) document.exitFullscreen(); + else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); + }, + + toggle() { + this.isFullscreen ? this.exit() : this.enter(); + }, + + syncButton() { + const btn = $('#fullscreen'); + if (this.isFullscreen) { + btn.addClass('is-fullscreen').attr('aria-label', 'Exit fullscreen').attr('title', 'Exit fullscreen'); + btn.find('.icon-enter-fs').hide(); + btn.find('.icon-exit-fs').show(); + } else { + btn.removeClass('is-fullscreen').attr('aria-label', 'Enter fullscreen').attr('title', 'Fullscreen'); + btn.find('.icon-exit-fs').hide(); + btn.find('.icon-enter-fs').show(); + } + }, + + init() { + $('#fullscreen').on('click', (e) => { + e.preventDefault(); + this.toggle(); + // Remove focus so outline doesn't persist after fullscreen toggle + e.currentTarget.blur(); + }); + + $(document).on('fullscreenchange webkitfullscreenchange', () => { + this.syncButton(); + // Resize slides to fill available space when entering/exiting + slides.sync(); + }); + + // Keyboard shortcut: F key + $(document).on('keydown', (e) => { + if (e.key === 'f' || e.key === 'F') { + // Don't trigger when typing in an input + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return; + this.toggle(); + } + if (e.key === 'Escape' && this.isFullscreen) { + this.exit(); + } + }); + } + }; + // Initialization const init = { loading() { @@ -572,8 +678,8 @@ HUB.Presenter = (() => { }, volumechange: () => player.syncVolume(), canplay: () => { - this.doneLoading(); this.locationHash(); + this.doneLoading(); }, seeked: () => { state.seeking = true; @@ -588,8 +694,25 @@ HUB.Presenter = (() => { doneLoading() { if (state.doneLoading) return; + state.doneLoading = true; $('#overlayer').remove(); - controls.playPause(false); + if (state.suppressAutoplay) { + // Loaded at a specific timestamp — show paused state, don't autoplay + player.get().pause(); + controls.playPause(false); + } else { + // Attempt autoplay; fall back to showing play button if browser blocks it + const playPromise = player.get().play(); + if (playPromise !== undefined) { + playPromise.then(() => { + controls.playPause(false); // sync UI to playing state + }).catch(() => { + controls.playPause(false); // browser blocked autoplay; show play button + }); + } else { + controls.playPause(false); + } + } this.previews(); }, @@ -663,8 +786,8 @@ HUB.Presenter = (() => { this.resume(utils.formatTime(time)); player.seek(time); progressBar.setProgress(time); - player.get().pause(); - state.doneLoading = true; + // Suppress autoplay when loading at a specific timestamp + state.suppressAutoplay = true; } }, @@ -868,7 +991,7 @@ HUB.Presenter = (() => { setupPicker(subTitles) { let auto = false; - $('#subtitle').show(); + $('#subtitle').css('display', 'inline-flex'); subTitles.forEach(sub => { const subLang = sub.lang.toLowerCase(); @@ -879,16 +1002,63 @@ HUB.Presenter = (() => { state.track = subLang; } - $('#video-subtitles').append(`
`); + $('#video-subtitles').append(`
`); $('#subtitle-selector').append(``); }); - if (auto) $('#subtitle').addClass('on'); + if (auto) { + $('#subtitle').addClass('on').attr('aria-pressed', 'true'); + } $('#subtitle-selector').on('change', function () { state.track = $(this).val(); - $('#subtitle').toggleClass('on', state.track !== ''); + const isOn = state.track !== ''; + $('#subtitle').toggleClass('on', isOn).attr('aria-pressed', String(isOn)); + }); + + // Options toggle + $('.options-toggle').on('click', function () { + const $settings = $('.subtitle-settings'); + const isVisible = $settings.is(':visible'); + $settings.toggle(); + $(this).text(isVisible ? 'Options' : 'Hide Options') + .attr('aria-expanded', String(!isVisible)); + }); + + // Subtitle settings — font, size, color + $('#font-selector').on('change', function () { + $('#video-subtitles div').css('font-family', $(this).val()); + $('.subtitle-settings-preview .test').css('font-family', $(this).val()); }); + + $('#font-size-selector').on('change', function () { + $('#video-subtitles div').css('font-size', $(this).val() + 'px'); + $('.subtitle-settings-preview .test').css('font-size', $(this).val() + 'px'); + }); + + if ($.fn.colpick) { + $('#font-color').colpick({ + layout: 'hex', + submit: 0, + colorScheme: 'dark', + onChange(_hsb, hex) { + $('#font-color').css('background-color', '#' + hex); + $('#video-subtitles div').css('color', '#' + hex); + $('.subtitle-settings-preview .test').css('color', '#' + hex); + } + }); + + $('#background-color').colpick({ + layout: 'hex', + submit: 0, + colorScheme: 'dark', + onChange(_hsb, hex) { + $('#background-color').css('background-color', '#' + hex); + $('#video-subtitles div').css('background-color', '#' + hex); + $('.subtitle-settings-preview .test').css('background-color', '#' + hex); + } + }); + } }, sync(subTitles) { @@ -924,9 +1094,9 @@ HUB.Presenter = (() => { subs.forEach(sub => { const line = ` -
-
${utils.formatTime(sub.start)}
-
${sub.text}
+
+ + ${sub.text}
`; $(`.transcript-${language}`).append(line); }); @@ -938,6 +1108,18 @@ HUB.Presenter = (() => { this.setupFontChanger(); this.setupSearch(); this.setupJumpTo(); + this.setupScrollSuppression(); + + // Auto-show transcript if any subtitle has autoplay set + const autoSub = subTitles.find(s => parseInt(s.auto)); + if (autoSub) { + const lang = autoSub.lang.toLowerCase(); + $('.transcript-selector').val(lang).trigger('change'); + } else if (subTitles.length > 0) { + // No explicit auto, show first available transcript + const lang = subTitles[0].lang.toLowerCase(); + $('.transcript-selector').val(lang).trigger('change'); + } setInterval(() => this.sync(), 300); }, @@ -947,17 +1129,21 @@ HUB.Presenter = (() => { const language = $(this).val(); if (language) { - $('#transcript-container').slideDown(() => { - if (parent.HUB?.Resources) { - parent.HUB.Resources.resizeInlineHubpresenter($('body').outerHeight() + 20); - } - }); + $('#transcript-container') + .attr('aria-hidden', 'false') + .slideDown(() => { + if (parent.HUB?.Resources) { + parent.HUB.Resources.resizeInlineHubpresenter($('body').outerHeight() + 20); + } + }); } else { - $('#transcript-container').slideUp(() => { - if (parent.HUB?.Resources) { - parent.HUB.Resources.resizeInlineHubpresenter($('body').outerHeight() + 20); - } - }); + $('#transcript-container') + .attr('aria-hidden', 'true') + .slideUp(() => { + if (parent.HUB?.Resources) { + parent.HUB.Resources.resizeInlineHubpresenter($('body').outerHeight() + 20); + } + }); } $('#transcript-select').html($('.transcript-selector option:selected').text()); @@ -969,12 +1155,12 @@ HUB.Presenter = (() => { setupFontChanger() { $('#font-smaller').on('click', (e) => { e.preventDefault(); - this.changeFontSize(-2, 8); + this.changeFontSize(-2, 12); }); $('#font-bigger').on('click', (e) => { e.preventDefault(); - this.changeFontSize(2, 18); + this.changeFontSize(2, 32); }); }, @@ -1003,17 +1189,86 @@ HUB.Presenter = (() => { }, setupSearch() { - $('#transcript-search').on('keyup change', function () { + $('#transcript-search').on('input', function () { + const term = $(this).val().trim(); + // Clear previous state $('.transcript-line-text').removeHighlight(); - $('.transcript-line-text').highlight($(this).val()); + $('.transcript-line').removeClass('search-hidden'); + $('#transcript-search-clear').toggle(term.length > 0); + + if (!term) { + $('#transcript-search-count').hide(); + return; + } + + // Filter lines — hide those that don't contain the term + let count = 0; + let $firstMatch = null; + $('.transcript-line').each(function () { + const text = $(this).find('.transcript-line-text').text(); + if (text.toUpperCase().indexOf(term.toUpperCase()) === -1) { + $(this).addClass('search-hidden'); + } else { + count++; + if (!$firstMatch) $firstMatch = $(this); + } + }); + + // Highlight matching text within visible lines + $('.transcript-line:not(.search-hidden) .transcript-line-text').highlight(term); + + // Show result count + $('#transcript-search-count') + .text(count > 0 ? `${count} result${count !== 1 ? 's' : ''}` : 'No results') + .show(); + + // Scroll to first match + if ($firstMatch) { + const containerTop = $('#transcripts').offset().top; + const matchTop = $firstMatch.offset().top; + const scrollTarget = $('#transcripts').scrollTop() + (matchTop - containerTop) - 20; + $('#transcripts').animate({ scrollTop: scrollTarget }, 200); + } + }); + + $('#transcript-search-clear').on('click', () => { + $('#transcript-search').val('').trigger('input').trigger('focus'); }); }, setupJumpTo() { - $('.transcript-line').on('click', function (e) { + // After clicking a line, suppress auto-scroll for 2s so the + // transcript box doesn't fight the user's intentional scroll position. + const suppressScroll = () => { + state.transcriptBoxScrolling = true; + clearTimeout(state._transcriptScrollTimer); + state._transcriptScrollTimer = setTimeout(() => { + state.transcriptBoxScrolling = false; + }, 2000); + }; + + $(document).on('click', '.transcript-line', function (e) { e.preventDefault(); + suppressScroll(); player.seek($(this).data('time')); }); + $(document).on('keydown', '.transcript-line', function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + suppressScroll(); + player.seek($(this).data('time')); + } + }); + }, + + setupScrollSuppression() { + $('#transcripts').on('scroll', function () { + state.transcriptBoxScrolling = true; + clearTimeout($.data(this, 'scrollTimer')); + $.data(this, 'scrollTimer', setTimeout(() => { + state.transcriptBoxScrolling = false; + }, 250)); + }); }, sync() { @@ -1026,19 +1281,11 @@ HUB.Presenter = (() => { if (!subs) return; - $('#transcripts').on('scroll', function () { - state.transcriptBoxScrolling = true; - clearTimeout($.data(this, 'scrollTimer')); - $.data(this, 'scrollTimer', setTimeout(() => { - state.transcriptBoxScrolling = false; - }, 250)); - }); - - $('.transcript-line').removeClass('active'); + $('.transcript-line').removeClass('active').removeAttr('aria-current'); subs.forEach((sub, i) => { if (currentTime >= sub.start && currentTime <= sub.end) { - $('.transcript-line').eq(i).addClass('active'); + $('.transcript-line').eq(i).addClass('active').attr('aria-current', 'true'); if (!state.transcriptBoxScrolling && state.transcriptLineActive !== i) { const lineHeight = $('.transcript-line').outerHeight(true); diff --git a/core/components/com_resources/site/views/view/tmpl/watch.php b/core/components/com_resources/site/views/view/tmpl/watch.php index 1b59164d03d..efad599587e 100644 --- a/core/components/com_resources/site/views/view/tmpl/watch.php +++ b/core/components/com_resources/site/views/view/tmpl/watch.php @@ -174,7 +174,7 @@
-
- Options -
+ + @@ -455,10 +514,10 @@ placeholder) && $presentation->placeholder) : ?> - + Audio presentation placeholder -
+
-
-
-
- - - +
+ -
+
Pop Out From 7470ae1d7be6a44cec0f59faf96f3f4bc54ad44b Mon Sep 17 00:00:00 2001 From: Daniel Mejia Date: Mon, 16 Mar 2026 13:27:16 -0400 Subject: [PATCH 2/2] fix issue on Chrome fix issue on Chrome --- .../site/assets/css/hubpresenter.css | 129 ------------------ 1 file changed, 129 deletions(-) diff --git a/core/components/com_resources/site/assets/css/hubpresenter.css b/core/components/com_resources/site/assets/css/hubpresenter.css index b8bd0ca9e5c..d56b34de40f 100644 --- a/core/components/com_resources/site/assets/css/hubpresenter.css +++ b/core/components/com_resources/site/assets/css/hubpresenter.css @@ -1394,135 +1394,6 @@ a.ui-slider-handle { height: 100vh; margin: 0; border-radius: 0; - display: flex; - flex-direction: row; - background: #000; - overflow: hidden; -} - -/* Hide header and bottom chrome in fullscreen */ -#presenter-container:fullscreen #presenter-header, -#presenter-container:fullscreen .bottom-controls, -#presenter-container:-webkit-full-screen #presenter-header, -#presenter-container:-webkit-full-screen .bottom-controls { - display: none; -} - -/* Content (slides + video) takes 75% width */ -#presenter-container:fullscreen #presenter-content, -#presenter-container:-webkit-full-screen #presenter-content { - flex: 3; - min-width: 0; - height: 100%; - display: flex; - flex-direction: row; -} - -/* Left panel (slides) fills ~67% of content */ -#presenter-container:fullscreen #presenter-left, -#presenter-container:-webkit-full-screen #presenter-left { - flex: 2; - min-width: 0; - height: 100%; - float: none; - width: auto; -} - -/* Slides fill the left column */ -#presenter-container:fullscreen #presenter-left #slides, -#presenter-container:-webkit-full-screen #presenter-left #slides { - width: 100%; - height: calc(100% - 60px); - border: none; -} - -#presenter-container:fullscreen #presenter-left #slides ul li, -#presenter-container:-webkit-full-screen #presenter-left #slides ul li { - height: 100%; -} - -#presenter-container:fullscreen #presenter-left #slides ul li img, -#presenter-container:-webkit-full-screen #presenter-left #slides ul li img { - width: 100%; - height: 100%; - object-fit: contain; -} - -/* Right panel (video + slide list) fills ~33% of content */ -#presenter-container:fullscreen #presenter-right, -#presenter-container:-webkit-full-screen #presenter-right { - flex: 1; - min-width: 0; - height: 100%; - float: none; - width: auto; - display: flex; - flex-direction: column; -} - -/* Video fills right panel */ -#presenter-container:fullscreen #presenter-right #media, -#presenter-container:-webkit-full-screen #presenter-right #media { - flex: 1; - height: auto; - min-height: 0; - border: none; - background: #000; -} - -#presenter-container:fullscreen #presenter-right #media video, -#presenter-container:-webkit-full-screen #presenter-right #media video { - width: 100%; - height: 100%; - object-fit: contain; -} - -/* Slide list fills remaining right-panel space */ -#presenter-container:fullscreen #list, -#presenter-container:-webkit-full-screen #list { - flex: 1; - min-height: 0; - overflow: hidden; -} - -#presenter-container:fullscreen #list_items, -#presenter-container:-webkit-full-screen #list_items { - height: 100%; -} - -/* Control box stays pinned at bottom of left panel */ -#presenter-container:fullscreen #control-box, -#presenter-container:-webkit-full-screen #control-box { - bottom: 0; -} - -/* Transcript panel: 25% width on the right, full height, visible in fullscreen */ -#presenter-container:fullscreen #transcript-container, -#presenter-container:-webkit-full-screen #transcript-container { - display: flex; - flex: 1; - flex-direction: column; - min-width: 0; - height: 100%; - max-width: 25%; background: #1a1a1a; - border-left: 1px solid #333; overflow: hidden; } - -#presenter-container:fullscreen #transcript-container #transcript-toolbar, -#presenter-container:-webkit-full-screen #transcript-container #transcript-toolbar { - flex-shrink: 0; - background: #222; - border-bottom: 1px solid #333; -} - -#presenter-container:fullscreen #transcript-container #transcripts, -#presenter-container:-webkit-full-screen #transcript-container #transcripts { - flex: 1; - height: auto; - min-height: 0; - overflow-y: auto; - padding: 8px; -} -