|
23 | 23 | maxTextWidthPx: 720, |
24 | 24 | playerPaddingPx: 32, |
25 | 25 | fontSizeRatio: 0.0155, |
26 | | - minFontPx: 20, |
27 | | - maxFontPx: 28, |
| 26 | + minFontPx: 16, |
| 27 | + maxFontPx: 32, |
28 | 28 | lineHeight: 1.4, |
29 | 29 | }; |
30 | 30 |
|
| 31 | + const SETTINGS_STORAGE_KEY = 'ketuviaSettings'; |
| 32 | + const DEFAULT_SETTINGS = { |
| 33 | + targetLines: 2, |
| 34 | + textSize: 'medium', |
| 35 | + background: 'medium', |
| 36 | + position: 'center-low', |
| 37 | + }; |
| 38 | + const TEXT_SIZE_SCALE = { |
| 39 | + small: 0.9, |
| 40 | + medium: 1.3, |
| 41 | + large: 1.7, |
| 42 | + }; |
| 43 | + const BACKGROUND_OPACITY = { |
| 44 | + light: 0.3, |
| 45 | + medium: 0.45, |
| 46 | + dark: 0.78, |
| 47 | + }; |
| 48 | + const OVERLAY_POSITIONS = { |
| 49 | + 'left-top': { x: 'left', y: '8%' }, |
| 50 | + 'center-top': { x: 'center', y: '8%' }, |
| 51 | + 'right-top': { x: 'right', y: '8%' }, |
| 52 | + 'left-high': { x: 'left', y: '20%' }, |
| 53 | + 'center-high': { x: 'center', y: '20%' }, |
| 54 | + 'right-high': { x: 'right', y: '20%' }, |
| 55 | + 'left-highish': { x: 'left', y: '35%' }, |
| 56 | + 'center-highish': { x: 'center', y: '35%' }, |
| 57 | + 'right-highish': { x: 'right', y: '35%' }, |
| 58 | + 'left-middle': { x: 'left', y: '50%' }, |
| 59 | + 'center-middle': { x: 'center', y: '50%' }, |
| 60 | + 'right-middle': { x: 'right', y: '50%' }, |
| 61 | + 'left-lowish': { x: 'left', y: '68%' }, |
| 62 | + 'center-lowish': { x: 'center', y: '68%' }, |
| 63 | + 'right-lowish': { x: 'right', y: '68%' }, |
| 64 | + 'left-low': { x: 'left', y: '84%' }, |
| 65 | + 'center-low': { x: 'center', y: '84%' }, |
| 66 | + 'right-low': { x: 'right', y: '84%' }, |
| 67 | + 'left-bottom': { x: 'left', y: '94%' }, |
| 68 | + 'center-bottom': { x: 'center', y: '94%' }, |
| 69 | + 'right-bottom': { x: 'right', y: '94%' }, |
| 70 | + }; |
| 71 | + |
| 72 | + function readSettings() { |
| 73 | + try { |
| 74 | + const parsed = JSON.parse(window.localStorage.getItem(SETTINGS_STORAGE_KEY) || '{}'); |
| 75 | + return normalizeSettings(parsed); |
| 76 | + } catch { |
| 77 | + return { ...DEFAULT_SETTINGS }; |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + function normalizeSettings(settings) { |
| 82 | + const targetLines = Number(settings?.targetLines); |
| 83 | + const textSize = String(settings?.textSize || DEFAULT_SETTINGS.textSize); |
| 84 | + const background = String(settings?.background || DEFAULT_SETTINGS.background); |
| 85 | + const position = String(settings?.position || DEFAULT_SETTINGS.position); |
| 86 | + |
| 87 | + return { |
| 88 | + targetLines: [1, 2, 3].includes(targetLines) |
| 89 | + ? targetLines |
| 90 | + : DEFAULT_SETTINGS.targetLines, |
| 91 | + textSize: Object.hasOwn(TEXT_SIZE_SCALE, textSize) |
| 92 | + ? textSize |
| 93 | + : DEFAULT_SETTINGS.textSize, |
| 94 | + background: Object.hasOwn(BACKGROUND_OPACITY, background) |
| 95 | + ? background |
| 96 | + : DEFAULT_SETTINGS.background, |
| 97 | + position: Object.hasOwn(OVERLAY_POSITIONS, position) |
| 98 | + ? position |
| 99 | + : DEFAULT_SETTINGS.position, |
| 100 | + }; |
| 101 | + } |
| 102 | + |
31 | 103 | const DEBUG = { |
32 | 104 | enabled: false, |
33 | 105 | maxChunkLogs: 80, |
|
69 | 141 | triggerRetryId: null, |
70 | 142 | triggerAttempts: 0, |
71 | 143 | debugChunks: [], |
| 144 | + settings: readSettings(), |
72 | 145 | }; |
73 | 146 |
|
| 147 | + function applySettings(nextSettings) { |
| 148 | + STATE.settings = normalizeSettings(nextSettings); |
| 149 | + window.__ketuviaSettings = { ...STATE.settings }; |
| 150 | + |
| 151 | + try { |
| 152 | + window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(STATE.settings)); |
| 153 | + } catch {} |
| 154 | + |
| 155 | + rebuildChunksForLayout(); |
| 156 | + renderCurrentCaption(true); |
| 157 | + |
| 158 | + return { ...STATE.settings }; |
| 159 | + } |
| 160 | + |
| 161 | + window.__ketuviaSettings = { ...STATE.settings }; |
| 162 | + window.__ketuviaApplySettings = applySettings; |
| 163 | + |
| 164 | + window.addEventListener('ketuvia-settings-change', event => { |
| 165 | + applySettings(event.detail); |
| 166 | + }); |
| 167 | + |
74 | 168 | const log = (...a) => { |
75 | 169 | if (!DEBUG.enabled) return; |
76 | 170 | console.log( |
|
97 | 191 | return document.querySelector('#movie_player') || document.querySelector('.html5-video-player'); |
98 | 192 | } |
99 | 193 |
|
| 194 | + function getRuntimeConfig() { |
| 195 | + return { |
| 196 | + ...CFG, |
| 197 | + targetLines: STATE.settings.targetLines, |
| 198 | + }; |
| 199 | + } |
| 200 | + |
100 | 201 | function joinWords(words) { |
101 | 202 | let text = ''; |
102 | 203 |
|
|
121 | 222 | function getLayoutMetrics(player) { |
122 | 223 | const playerWidth = Math.max(0, player?.clientWidth || 0); |
123 | 224 | if (!playerWidth) return null; |
| 225 | + const settings = STATE.settings; |
| 226 | + const runtimeConfig = getRuntimeConfig(); |
| 227 | + const textSizeScale = TEXT_SIZE_SCALE[settings.textSize] || TEXT_SIZE_SCALE.medium; |
124 | 228 |
|
125 | 229 | const fontSizePx = clamp( |
126 | | - Math.round(playerWidth * CFG.fontSizeRatio * 10) / 10, |
| 230 | + Math.round(playerWidth * CFG.fontSizeRatio * textSizeScale * 10) / 10, |
127 | 231 | CFG.minFontPx, |
128 | 232 | CFG.maxFontPx |
129 | 233 | ); |
|
140 | 244 | textWidthPx, |
141 | 245 | fontSizePx, |
142 | 246 | lineHeight: CFG.lineHeight, |
143 | | - targetLines: CFG.targetLines, |
| 247 | + targetLines: runtimeConfig.targetLines, |
144 | 248 | }; |
145 | 249 | } |
146 | 250 |
|
|
201 | 305 |
|
202 | 306 | function applyLayout(node, layout) { |
203 | 307 | if (!node || !layout) return; |
| 308 | + const position = OVERLAY_POSITIONS[STATE.settings.position] || OVERLAY_POSITIONS[DEFAULT_SETTINGS.position]; |
| 309 | + |
204 | 310 | node.style.setProperty('--rechunk-text-width', layout.textWidthPx + 'px'); |
205 | 311 | node.style.setProperty('--rechunk-font-size', layout.fontSizePx + 'px'); |
206 | 312 | node.style.setProperty('--rechunk-line-height', String(layout.lineHeight)); |
207 | 313 | node.style.setProperty('--rechunk-target-lines', String(layout.targetLines)); |
| 314 | + node.style.setProperty( |
| 315 | + '--rechunk-bg-opacity', |
| 316 | + String(BACKGROUND_OPACITY[STATE.settings.background] || BACKGROUND_OPACITY.medium) |
| 317 | + ); |
| 318 | + node.style.top = position.y; |
| 319 | + node.style.bottom = 'auto'; |
| 320 | + node.style.left = ''; |
| 321 | + node.style.right = ''; |
| 322 | + |
| 323 | + if (position.x === 'left') { |
| 324 | + node.style.left = '8px'; |
| 325 | + node.style.transform = 'translateY(-50%)'; |
| 326 | + node.style.textAlign = 'left'; |
| 327 | + } else if (position.x === 'right') { |
| 328 | + node.style.right = '8px'; |
| 329 | + node.style.transform = 'translateY(-50%)'; |
| 330 | + node.style.textAlign = 'right'; |
| 331 | + } else { |
| 332 | + node.style.left = '50%'; |
| 333 | + node.style.transform = 'translate(-50%, -50%)'; |
| 334 | + node.style.textAlign = 'center'; |
| 335 | + } |
208 | 336 | } |
209 | 337 |
|
210 | 338 | function mountOverlay() { |
|
283 | 411 | const player = mountOverlay(); |
284 | 412 | if (!player || !STATE.words.length) return; |
285 | 413 |
|
286 | | - const chunkResult = chunkWords(STATE.words, CFG); |
| 414 | + const chunkResult = chunkWords(STATE.words, getRuntimeConfig()); |
287 | 415 | STATE.chunks = chunkResult.chunks; |
288 | 416 | STATE.debugChunks = chunkResult.debugChunks; |
289 | 417 | log('rebuilt ' + STATE.chunks.length + ' chunks for layout width=' + STATE.layout.textWidthPx); |
|
495 | 623 | } |
496 | 624 | STATE.words = words; |
497 | 625 | mountOverlay(); |
498 | | - const chunkResult = chunkWords(words, CFG); |
| 626 | + const chunkResult = chunkWords(words, getRuntimeConfig()); |
499 | 627 | STATE.chunks = chunkResult.chunks; |
500 | 628 | STATE.debugChunks = chunkResult.debugChunks; |
501 | 629 | log('built ' + STATE.chunks.length + ' chunks from ' + words.length + ' words'); |
@@ -888,37 +1016,44 @@ function chunkWords(words, cfg) { |
888 | 1016 | const _captionHideStyle = document.createElement('style'); |
889 | 1017 | _captionHideStyle.textContent = '.ytp-caption-window-container{visibility:hidden!important}'; |
890 | 1018 |
|
| 1019 | + function renderCurrentCaption(force = false) { |
| 1020 | + if (!STATE.overlay || !STATE.enabled) return; |
| 1021 | + const video = document.querySelector('video.html5-main-video') || document.querySelector('video'); |
| 1022 | + if (!video) return; |
| 1023 | + |
| 1024 | + const ms = (video.currentTime || 0) * 1000 + CFG.lookaheadMs; |
| 1025 | + let active = ''; |
| 1026 | + let activeIndex = -1; |
| 1027 | + const N = STATE.chunks.length; |
| 1028 | + |
| 1029 | + for (let i = 0; i < N; i++) { |
| 1030 | + const c = STATE.chunks[i]; |
| 1031 | + const next = STATE.chunks[i + 1]; |
| 1032 | + const winEnd = next ? next.startMs : c.endMs; |
| 1033 | + if (ms >= c.startMs && ms < winEnd) { |
| 1034 | + active = c.text; |
| 1035 | + activeIndex = i; |
| 1036 | + break; |
| 1037 | + } |
| 1038 | + if (ms < c.startMs) break; |
| 1039 | + } |
| 1040 | + |
| 1041 | + if (!force && active === STATE.lastText) return; |
| 1042 | + |
| 1043 | + if (STATE.overlayText) { |
| 1044 | + STATE.overlayText.textContent = active; |
| 1045 | + } |
| 1046 | + STATE.overlay.dataset.empty = active ? '0' : '1'; |
| 1047 | + if (active && activeIndex >= 0) { |
| 1048 | + logRenderedChunk(activeIndex, STATE.chunks[activeIndex]); |
| 1049 | + } |
| 1050 | + STATE.lastText = active; |
| 1051 | + } |
| 1052 | + |
891 | 1053 | function startPolling() { |
892 | 1054 | if (STATE.pollId) return; |
893 | 1055 | const tick = () => { |
894 | | - if (!STATE.overlay || !STATE.enabled) return; |
895 | | - const video = document.querySelector('video.html5-main-video') || document.querySelector('video'); |
896 | | - if (!video) return; |
897 | | - const ms = (video.currentTime || 0) * 1000 + CFG.lookaheadMs; |
898 | | - let active = ''; |
899 | | - let activeIndex = -1; |
900 | | - const N = STATE.chunks.length; |
901 | | - for (let i = 0; i < N; i++) { |
902 | | - const c = STATE.chunks[i]; |
903 | | - const next = STATE.chunks[i + 1]; |
904 | | - const winEnd = next ? next.startMs : c.endMs; |
905 | | - if (ms >= c.startMs && ms < winEnd) { |
906 | | - active = c.text; |
907 | | - activeIndex = i; |
908 | | - break; |
909 | | - } |
910 | | - if (ms < c.startMs) break; |
911 | | - } |
912 | | - if (active !== STATE.lastText) { |
913 | | - if (STATE.overlayText) { |
914 | | - STATE.overlayText.textContent = active; |
915 | | - } |
916 | | - STATE.overlay.dataset.empty = active ? '0' : '1'; |
917 | | - if (active && activeIndex >= 0) { |
918 | | - logRenderedChunk(activeIndex, STATE.chunks[activeIndex]); |
919 | | - } |
920 | | - STATE.lastText = active; |
921 | | - } |
| 1056 | + renderCurrentCaption(); |
922 | 1057 | }; |
923 | 1058 | STATE.pollId = setInterval(tick, CFG.pollMs); |
924 | 1059 | } |
@@ -978,8 +1113,8 @@ function chunkWords(words, cfg) { |
978 | 1113 | btn.id = 'rechunk-toggle'; |
979 | 1114 | btn.className = 'ytp-button'; |
980 | 1115 | btn.type = 'button'; |
981 | | - btn.setAttribute('aria-label', 'Toggle Rechunk Captions'); |
982 | | - btn.title = 'Rechunk Captions'; |
| 1116 | + btn.setAttribute('aria-label', 'Turn Ketuvia captions on or off'); |
| 1117 | + btn.title = 'Turn Ketuvia captions on or off'; |
983 | 1118 | btn.textContent = 'CC+'; |
984 | 1119 | btn.addEventListener('click', () => { |
985 | 1120 | STATE.enabled = !STATE.enabled; |
@@ -1011,11 +1146,11 @@ function chunkWords(words, cfg) { |
1011 | 1146 | b.dataset.status = STATE.statusMode; |
1012 | 1147 | b.dataset.enabled = STATE.enabled ? '1' : '0'; |
1013 | 1148 | const labels = { |
1014 | | - idle: 'Rechunk Captions: waiting for video', |
1015 | | - loading: 'Rechunk Captions: loading', |
1016 | | - active: 'Rechunk Captions: ON (click to disable)', |
1017 | | - unavailable: 'Rechunk Captions: no auto-captions on this video', |
1018 | | - error: 'Rechunk Captions: failed to load', |
| 1149 | + idle: 'Ketuvia: waiting for video', |
| 1150 | + loading: 'Ketuvia: loading captions', |
| 1151 | + active: 'Ketuvia is on (click to turn off)', |
| 1152 | + unavailable: 'Ketuvia: no auto-captions on this video', |
| 1153 | + error: 'Ketuvia: failed to load captions', |
1019 | 1154 | }; |
1020 | 1155 | b.title = (STATE.enabled ? '' : '(disabled) ') + (labels[STATE.statusMode] || ''); |
1021 | 1156 | } |
|
0 commit comments