diff --git a/README.md b/README.md index 149bc19e..29813980 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,73 @@ Looking for an app for Android TVs? Check out [TizenTube Cobalt](https://github. - [DeArrow](https://dearrow.ajay.app/) Support - Customizable Themes (Custom Coloring) - More to come, if you [request](https://github.com/reisxd/TizenTube/issues/new) it! + +# Tampermonkey local debugging helpers (Windows + Chrome) + +Use this when you want to test TizenTube locally in Chrome instead of on-device. + +Tampermonkey runs the userscripts, while a User-Agent switcher extension makes Chrome present itself as a TV browser so `https://www.youtube.com/tv` stays on the TV UI. + +## Scripts in this repository + +Tampermonkey scripts are stored in: + +- `scripts/tampermonkey/tizentube-loader.user.js` (loads `dist/userScript.js` via `@require`) +- `scripts/tampermonkey/tizentube-log-button.user.js` (adds floating **TT Logs** button that calls `window.downloadTizenTubeLogs()`) + +## Full setup steps + +1. Install **Tampermonkey** in Chrome. +2. Open `chrome://extensions/`: + - Enable **Developer mode** (top-right). + - Open Tampermonkey details and enable **Allow in Incognito**. + - If you use a User-Agent extension, also enable **Allow in Incognito** for it. +3. Install a User-Agent switching extension (for TV UA testing). +4. Set a TV-like User-Agent for YouTube. Example that usually works (can be in Incognito Mode): + + `Mozilla/5.0 (SMART-TV; Linux; Tizen 6.5) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.0 TV Safari/537.36` + + + Real TV User-Agent examples: + + - Tizen 5.0 TV: + `Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 TV Safari/537.36` + - Tizen 6.5 TV: + `Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5) AppleWebKit/537.36 (KHTML, like Gecko) 108.0.5359.1/6.5 TV Safari/537.36 UWE/0.2.27108` + +5. In Tampermonkey, create/import `scripts/tampermonkey/tizentube-loader.user.js` (configured for `youtube.com/tv*` only). +6. In Tampermonkey, create/import `scripts/tampermonkey/tizentube-log-button.user.js` (configured for `youtube.com/tv*` only). +7. In Tampermonkey script settings, set **Sandbox mode = ALL** for these scripts. +8. Open `https://www.youtube.com/tv` and sign in if needed. +9. Verify TizenTube loaded: + - Open DevTools Console (F12) and run `typeof window.toggleDebugConsole` (should return `"function"`). +10. Click **TT Logs** (bottom-right corner of the page, floating above the YouTube TV UI) to download logs without typing console commands. + + +### Keyboard shortcuts for Windows testing (no TV remote) + +When testing in Chrome on desktop, TizenTube maps TV color-button actions to normal keys: + +- **GREEN** (open TizenTube settings): `G`, `F2`, or `2` +- **RED** (open theme settings): `R`, `F1`, or `1` +- **YELLOW** (toggle debug console): `Y`, `F3`, or `3` +- **BLUE**: `B`, `F4`, or `4` + +### Where should I see the button? + +After both userscripts are enabled and `/tv` has loaded, the **TT Logs** button appears in the **bottom-right corner** as a small black/green floating button. + +If you do not see it: +- confirm `tizentube-log-button.user.js` is enabled in Tampermonkey, +- refresh the page once, +- and verify Tampermonkey script sandbox is set to **ALL**. + +## Notes + +- These helper scripts are intentionally limited to `/tv` URLs so they do not affect normal desktop YouTube pages. +- In Tampermonkey, you can force-refresh `@require` files via **TizenTube Loader → Externals → Requires → Update**. +- If Tampermonkey seems stale in Incognito, open script dashboard and use **Utilities → Check for userscript updates**, then hard-refresh YouTube TV. +- Tampermonkey only refreshes `@require` when it checks script updates; bumping loader `@version` and running update check forces newest bundle. +- If `/tv` redirects back to desktop YouTube, re-check User-Agent override and extension scope. +- Keep both scripts enabled: loader + log-button. +- The log button is external (Tampermonkey UI helper), not an in-app visual-console button. \ No newline at end of file diff --git a/mods/config.js b/mods/config.js index acf67848..6d595264 100644 --- a/mods/config.js +++ b/mods/config.js @@ -56,9 +56,14 @@ const defaultConfig = { autoFrameRatePauseVideoFor: 0, enableSigninReminder: false, sortSubscriptionsByAlphabet: false, + enableDebugConsole: false, + enableDebugLogging: false, + debugConsolePosition: 'top-left', + debugConsoleHeight: 500 }; let localConfig; +const populatedConfigWarnings = new Set(); try { localConfig = JSON.parse(window.localStorage[CONFIG_KEY]); @@ -67,10 +72,18 @@ try { localConfig = defaultConfig; } +if (!localConfig || typeof localConfig !== 'object') { + localConfig = { ...defaultConfig }; +} + export function configRead(key) { if (localConfig[key] === undefined) { - console.warn('Populating key', key, 'with default value', defaultConfig[key]); - localConfig[key] = defaultConfig[key]; + const hasDefault = Object.prototype.hasOwnProperty.call(defaultConfig, key); + localConfig[key] = hasDefault ? defaultConfig[key] : undefined; + if (hasDefault && !populatedConfigWarnings.has(key)) { + populatedConfigWarnings.add(key); + console.warn('Populating key', key, 'with default value', defaultConfig[key]); + } } return localConfig[key]; diff --git a/mods/features/adblock.js b/mods/features/adblock.js index d9959264..95b1b0e1 100644 --- a/mods/features/adblock.js +++ b/mods/features/adblock.js @@ -341,7 +341,6 @@ function deArrowify(items) { } } - function hqify(items) { for (const item of items) { if (!item.tileRenderer) continue; @@ -411,4 +410,4 @@ function hideVideo(items) { const percentWatched = (progressBar.percentDurationWatched || 0); return percentWatched <= configRead('hideWatchedVideosThreshold'); }); -} \ No newline at end of file +} diff --git a/mods/features/moreSubtitles.js b/mods/features/moreSubtitles.js index 01eeb34d..ca20604d 100644 --- a/mods/features/moreSubtitles.js +++ b/mods/features/moreSubtitles.js @@ -358,11 +358,12 @@ function patchSubtitleMenu() { isPatched = true; } -// Wait for the YouTube TV app to be ready +// Wait for the YouTube TV app to be ready, keep retrying until patch succeeds const interval = setInterval(() => { if (window._yttv && Object.keys(window._yttv).length > 0) { patchSubtitleMenu(); - clearInterval(interval); + // Only clear the interval if patching actually succeeded + if (isPatched) clearInterval(interval); } }, 1000); diff --git a/mods/features/sponsorblock.js b/mods/features/sponsorblock.js index 07dc5b44..0e4a4103 100644 --- a/mods/features/sponsorblock.js +++ b/mods/features/sponsorblock.js @@ -240,7 +240,7 @@ class SponsorBlockHandler { this.sliderInterval = setInterval(() => { this.slider = document.querySelector('ytlr-redux-connect-ytlr-progress-bar'); - if (this.slider) { + if (this.slider && this.slider.isConnected) { // add isConnected check clearInterval(this.sliderInterval); this.sliderInterval = null; this.observer.observe(this.slider, { diff --git a/mods/features/visualConsole.js b/mods/features/visualConsole.js new file mode 100644 index 00000000..b64dd43c --- /dev/null +++ b/mods/features/visualConsole.js @@ -0,0 +1,227 @@ +import { configRead, configWrite, configChangeEmitter } from '../config.js'; +import resolveCommand from '../resolveCommand.js'; +import rootPkg from '../../package.json'; + +const APP_VERSION_LABEL = 'TizenTube'; +const APP_VERSION = rootPkg.version; + +function detectTvModel() { + let modelName = null; + let modelCode = null; + + try { + const h5vccModel = window?.h5vcc?.system?.getDeviceInfo?.()?.modelName; + if (h5vccModel) modelName = String(h5vccModel); + } catch (_) { } + + try { + const webapisModel = window?.webapis?.productinfo?.getModel?.(); + if (webapisModel) modelName = String(webapisModel); + } catch (_) { } + + try { + const webapisRealModel = window?.webapis?.productinfo?.getRealModel?.(); + if (webapisRealModel) modelName = String(webapisRealModel); + } catch (_) { } + + try { + const maybeCode = window?.webapis?.productinfo?.getModelCode?.(); + if (maybeCode) modelCode = String(maybeCode); + } catch (_) { } + + try { + const ua = String(navigator.userAgent || ''); + const match = ua.match(/\(([^)]*?TV[^)]*?)\)/i); + if (!modelName && match?.[1]) modelName = match[1]; + } catch (_) { } + + if (!modelName && !modelCode) return 'unknown'; + if (modelName && modelCode) return `${modelName} (${modelCode})`; + return modelName || modelCode; +} + +function initVisualConsole() { + const positions = { + 'top-left': { top: '0', left: '0', right: '', bottom: '', transform: '' }, + 'top-right': { top: '0', right: '0', left: '', bottom: '', transform: '' }, + 'bottom-left': { bottom: '0', left: '0', right: '', top: '', transform: '' }, + 'bottom-right': { bottom: '0', right: '0', left: '', top: '', transform: '' }, + center: { top: '50%', left: '50%', right: '', bottom: '', transform: 'translate(-50%, -50%)' } + }; + + const consoleDiv = document.createElement('div'); + consoleDiv.id = 'tv-debug-console'; + + const applyPosition = () => { + const pos = configRead('debugConsolePosition') || 'bottom-right'; + Object.assign(consoleDiv.style, positions[pos] || positions['bottom-right']); + }; + + const applyHeight = () => { + const h = Number(configRead('debugConsoleHeight') || 500); + consoleDiv.style.height = `${h}px`; + }; + + consoleDiv.style.cssText = ` + position: fixed; + width: 1054px; + background: rgba(0, 0, 0, 0.95); + color: #0f0; + font-family: monospace; + font-size: 13px; + padding: 10px; + overflow-y: auto; + overflow-x: hidden; + z-index: 999999; + border: 3px solid #0f0; + display: none; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.5); + pointer-events: none; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + `; + + applyPosition(); + applyHeight(); + + const mount = () => { + if (document.body && !document.getElementById('tv-debug-console')) document.body.appendChild(consoleDiv); + }; + mount(); + document.addEventListener('DOMContentLoaded', mount); + + let logs = []; + if (!Array.isArray(window.__ttFileOnlyLogs)) window.__ttFileOnlyLogs = []; + const original = { + log: console.log, + info: console.info, + warn: console.warn, + error: console.error, + debug: console.debug + }; + + const renderLogs = () => { + while (consoleDiv.firstChild) { + consoleDiv.removeChild(consoleDiv.firstChild); + } + + for (const entry of logs) { + const row = document.createElement('div'); + row.style.color = entry.color; + row.style.marginBottom = '4px'; + row.style.whiteSpace = 'pre-wrap'; + row.style.wordWrap = 'break-word'; + row.textContent = `[${entry.time}] ${entry.msg}`; + consoleDiv.appendChild(row); + } + }; + + const syncVisible = () => { + const enabled = !!configRead('enableDebugConsole'); + consoleDiv.style.display = enabled ? 'block' : 'none'; + if (enabled) { + applyPosition(); + applyHeight(); + renderLogs(); + consoleDiv.scrollTop = 0; + } + }; + + const addLog = (type, args) => { + if (!configRead('enableDebugConsole') && !configRead('enableDebugLogging')) return; + const color = type === 'error' ? '#f55' : type === 'warn' ? '#ff0' : '#0f0'; + const msg = args.map((a) => { + if (typeof a === 'string') return a; + try { return JSON.stringify(a); } catch (_) { return String(a); } + }).join(' '); + logs.unshift({ + color, + msg, + time: new Date().toLocaleTimeString() + }); + if (logs.length > 600) logs.pop(); + if (consoleDiv.style.display !== 'none') { + renderLogs(); + consoleDiv.scrollTop = 0; + } + }; + + console.log = (...args) => { original.log.apply(console, args); addLog('log', args); }; + console.info = (...args) => { original.info.apply(console, args); addLog('info', args); }; + console.warn = (...args) => { original.warn.apply(console, args); addLog('warn', args); }; + console.error = (...args) => { original.error.apply(console, args); addLog('error', args); }; + console.debug = (...args) => { original.debug.apply(console, args); addLog('debug', args); }; + + const downloadLogs = () => { + try { + const plainTextLogs = logs + .map((entry) => `[${entry.time}] ${entry.msg}`) + .join('\n'); + const fileOnlyLogs = Array.isArray(window.__ttFileOnlyLogs) ? window.__ttFileOnlyLogs.join('\n') : ''; + const combinedLogs = `${plainTextLogs}\n\n===== FILE-ONLY DEBUG LOGS =====\n${fileOnlyLogs}`; + const blob = new Blob([combinedLogs], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `tizentube-logs-${Date.now()}.txt`; + a.click(); + URL.revokeObjectURL(url); + } catch (err) { + original.error('Failed to download logs', err); + } + }; + + window.downloadTizenTubeLogs = downloadLogs; + + const toggleDebugConsole = function () { + configWrite('enableDebugConsole', !configRead('enableDebugConsole')); + syncVisible(); + }; + + window.toggleDebugConsole = toggleDebugConsole; + + configChangeEmitter.addEventListener('configChange', (event) => { + if (event?.detail?.key === 'enableDebugConsole' || event?.detail?.key === 'debugConsolePosition' || event?.detail?.key === 'debugConsoleHeight') { + syncVisible(); + } + }); + + syncVisible(); + + console.log('[Console] ========================================'); + console.log('[Console] Use TizenTube settings to configure position/height'); + console.log(`[Console] Visual Console ${APP_VERSION_LABEL} v${APP_VERSION}`); + console.log(`[Console] TV Model: ${detectTvModel()}`); + console.log(`[Console] User-Agent: ${navigator.userAgent}`); + console.log('[Console] ========================================'); + + const versionToastCmd = { + openPopupAction: { + popupType: 'TOAST', + popupDurationSeconds: 5, + popup: { + overlayToastRenderer: { + title: { simpleText: 'TizenTube started' }, + subtitle: { simpleText: 'Version ' + APP_VERSION } + } + } + } + }; + + setTimeout(() => { + try { resolveCommand(versionToastCmd); } catch (_) { } + }, 1200); +} + +if (typeof window.toggleDebugConsole !== 'function') { + window.toggleDebugConsole = function () { + configWrite('enableDebugConsole', !configRead('enableDebugConsole')); + }; +} + +const interval = setInterval(() => { + if (document.querySelector('video')) { + initVisualConsole(); + clearInterval(interval); + } +}, 500); \ No newline at end of file diff --git a/mods/resolveCommand.js b/mods/resolveCommand.js index 1d580700..17011a9f 100644 --- a/mods/resolveCommand.js +++ b/mods/resolveCommand.js @@ -27,8 +27,15 @@ export function findFunction(funcName) { // Patch resolveCommand to be able to change TizenTube settings export function patchResolveCommand() { + let patched = false; + for (const key in window._yttv) { if (window._yttv[key] && window._yttv[key].instance && window._yttv[key].instance.resolveCommand) { + // Prevent double-patching + if (window._yttv[key].instance.resolveCommand.__ttPatched) { + patched = true; + continue; + } const ogResolve = window._yttv[key].instance.resolveCommand; window._yttv[key].instance.resolveCommand = function (cmd, _) { @@ -128,7 +135,7 @@ export function patchResolveCommand() { return true; } - if (cmd?.requestAccountSelectorCommand + if (cmd?.requestAccountSelectorCommand && cmd.requestAccountSelectorCommand?.identityActionContext?.eventTrigger === 'ACCOUNT_EVENT_TRIGGER_ON_EXIT') { if (!configRead('enableWhosWatchingMenuOnAppExit')) { ogResolve.call(this, { @@ -142,8 +149,16 @@ export function patchResolveCommand() { return ogResolve.call(this, cmd, _); } + + window._yttv[key].instance.resolveCommand.__ttPatched = true; + patched = true; } } + + // If nothing was patchable yet, retry + if (!patched) { + setTimeout(patchResolveCommand, 500); + } } function customAction(action, parameters) { @@ -198,4 +213,4 @@ function customAction(action, parameters) { checkForUpdates(true); break; } -} \ No newline at end of file +} diff --git a/mods/rollup.config.js b/mods/rollup.config.js index 99ee7b2b..7e3d17a0 100644 --- a/mods/rollup.config.js +++ b/mods/rollup.config.js @@ -38,4 +38,4 @@ export default { '\uFFFF': '\u0000', }) ] -}; \ No newline at end of file +}; diff --git a/mods/ui/customYTSettings.js b/mods/ui/customYTSettings.js index 7d043f3e..4587bc68 100644 --- a/mods/ui/customYTSettings.js +++ b/mods/ui/customYTSettings.js @@ -26,4 +26,4 @@ function PatchSettings(settingsObject) { export { PatchSettings -} \ No newline at end of file +} diff --git a/mods/ui/settings.js b/mods/ui/settings.js index dfab4504..16f6c844 100644 --- a/mods/ui/settings.js +++ b/mods/ui/settings.js @@ -305,6 +305,48 @@ export default function modernUI(update, parameters) { { name: t('settings.options.misc.options.reloadHomeOnStartup'), value: 'reloadHomeOnStartup' + }, + { + name: 'Debug Console', + icon: 'BUG_REPORT', + value: null, + menuId: 'tt-debug-console-settings', + options: [ + { + name: 'Enable Debug Console', + icon: 'BUG_REPORT', + value: 'enableDebugConsole' + }, + { + name: 'Enable Background Debug Logging', + icon: 'ARTICLE', + value: 'enableDebugLogging' + }, + { + name: 'Console Position', + value: null, + menuId: 'tt-debug-console-position', + options: [ + { name: 'Top Left', key: 'debugConsolePosition', value: 'top-left' }, + { name: 'Top Right', key: 'debugConsolePosition', value: 'top-right' }, + { name: 'Bottom Left', key: 'debugConsolePosition', value: 'bottom-left' }, + { name: 'Bottom Right', key: 'debugConsolePosition', value: 'bottom-right' }, + { name: 'Center', key: 'debugConsolePosition', value: 'center' } + ] + }, + { + name: 'Console Height', + value: null, + menuId: 'tt-debug-console-height', + options: [300, 400, 500, 600, 700, 800, 1054].map((height) => { + return { + name: `${height}px`, + key: 'debugConsoleHeight', + value: height + } + }) + } + ] } ] }, diff --git a/mods/ui/ui.js b/mods/ui/ui.js index f2b5b3c5..269c8fa7 100644 --- a/mods/ui/ui.js +++ b/mods/ui/ui.js @@ -11,9 +11,11 @@ import getCommandExecutor from './customCommandExecution.js'; import { t } from 'i18next'; // It just works, okay? +// Wait for both a video element AND _yttv to be ready before initialising const interval = setInterval(() => { const videoElement = document.querySelector('video'); - if (videoElement) { + const yttvReady = window._yttv && Object.keys(window._yttv).length > 0; + if (videoElement && yttvReady) { execute_once_dom_loaded(); patchResolveCommand(); clearInterval(interval); @@ -22,6 +24,20 @@ const interval = setInterval(() => { let keyTimeout = null; + +function mapDesktopColorKey(evt) { + const code = evt.code || ''; + const key = (evt.key || '').toLowerCase(); + + // Desktop fallbacks for TV remote color keys when testing on Windows. + if (code === 'KeyR' || key === 'r' || code === 'F1' || code === 'Digit1') return 403; // RED + if (code === 'KeyG' || key === 'g' || code === 'F2' || code === 'Digit2') return 404; // GREEN + if (code === 'KeyY' || key === 'y' || code === 'F3' || code === 'Digit3') return 405; // YELLOW + if (code === 'KeyB' || key === 'b' || code === 'F4' || code === 'Digit4') return 406; // BLUE + + return evt.keyCode; +} + function execute_once_dom_loaded() { // Add CSS to head. @@ -94,7 +110,6 @@ function execute_once_dom_loaded() { focusedElement.value = focusedElement.value.slice(0, -1); } - if (evt.key === 'Enter' || evt.Uc?.key === 'Enter') { // If the focused element is a text input, emit a change event. if (document.querySelector(':focus').type === 'text') { @@ -107,12 +122,35 @@ function execute_once_dom_loaded() { ); try { - uiContainer.innerHTML = ` -

TizenTube Theme Configuration

- - -
Sponsor segments skipping - https://sponsor.ajay.app
-`; + // Build theme UI without innerHTML to avoid TrustedHTML CSP warnings + const h1 = document.createElement('h1'); + h1.textContent = 'TizenTube Theme Configuration'; + uiContainer.appendChild(h1); + + const label1 = document.createElement('label'); + label1.setAttribute('for', '__barColor'); + label1.textContent = 'Navigation Bar Color: '; + const input1 = document.createElement('input'); + input1.type = 'text'; + input1.id = '__barColor'; + label1.appendChild(input1); + uiContainer.appendChild(label1); + + const label2 = document.createElement('label'); + label2.setAttribute('for', '__routeColor'); + label2.textContent = 'Main Content Color: '; + const input2 = document.createElement('input'); + input2.type = 'text'; + input2.id = '__routeColor'; + label2.appendChild(input2); + uiContainer.appendChild(label2); + + const infoDiv = document.createElement('div'); + const small = document.createElement('small'); + small.textContent = 'Sponsor segments skipping - https://sponsor.ajay.app'; + infoDiv.appendChild(small); + uiContainer.appendChild(infoDiv); + document.querySelector('body').appendChild(uiContainer); uiContainer.querySelector('#__barColor').value = configRead('focusContainerColor'); @@ -129,14 +167,9 @@ function execute_once_dom_loaded() { } catch (e) { } var eventHandler = (evt) => { + const mappedKeyCode = mapDesktopColorKey(evt); + // We handle key events ourselves. - console.info( - 'Key event:', - evt.type, - evt.keyCode, - evt.keyCode, - evt.defaultPrevented - ); if (configRead('enableScreenDimming')) { if (keyTimeout) { clearTimeout(keyTimeout); @@ -149,7 +182,7 @@ function execute_once_dom_loaded() { document.getElementById('container').style.setProperty('opacity', (1 - configRead('dimmingOpacity')).toString(), 'important'); }, configRead('dimmingTimeout') * 1000); } - if (evt.keyCode == 403) { + if (mappedKeyCode == 403) { console.info('Taking over!'); evt.preventDefault(); evt.stopPropagation(); @@ -167,10 +200,16 @@ function execute_once_dom_loaded() { } catch (e) { } } return false; - } else if (evt.keyCode == 404) { + } else if (mappedKeyCode == 404) { if (evt.type === 'keydown') { modernUI(); } + } else if (mappedKeyCode == 405 || mappedKeyCode == 170) { + if (evt.type === 'keydown' && typeof window.toggleDebugConsole === 'function') { + evt.preventDefault(); + evt.stopPropagation(); + window.toggleDebugConsole(); + } } else if (evt.keyCode == 39) { // Right key, for PiP if (evt.type === 'keydown') { @@ -182,14 +221,13 @@ function execute_once_dom_loaded() { } }; return true; - } + }; // Red, Green, Yellow, Blue - // 403, 404, 405, 406 - // ---, 172, 170, 191 + // TV key codes: 403, 404, 405, 406 + // Alternative key codes: ---, 172, 170, 191 + // Desktop test fallbacks: R/G/Y/B, F1/F2/F3/F4, 1/2/3/4 document.addEventListener('keydown', eventHandler, true); - document.addEventListener('keypress', eventHandler, true); - document.addEventListener('keyup', eventHandler, true); if (configRead('showWelcomeToast')) { setTimeout(() => { showToast(t('welcomeMsg.title'), t('welcomeMsg.subtitle')); @@ -214,7 +252,6 @@ function execute_once_dom_loaded() { } // Fix UI issues, again. Love, Googol. - if (configRead('enableFixedUI')) { try { const observer = new MutationObserver((_, _2) => { @@ -226,4 +263,4 @@ function execute_once_dom_loaded() { observer.observe(document.body, { attributes: true, childList: false, subtree: false }); } catch (e) { } } -} \ No newline at end of file +} diff --git a/mods/userScript.js b/mods/userScript.js index fd906f02..0b7cec91 100644 --- a/mods/userScript.js +++ b/mods/userScript.js @@ -19,4 +19,5 @@ import "./features/videoQueuing.js"; import "./features/enableFeatures.js"; import "./ui/customUI.js"; import "./ui/customGuideAction.js"; -import "./features/autoFrameRate.js"; \ No newline at end of file +import "./features/autoFrameRate.js"; +import "./features/visualConsole.js"; \ No newline at end of file diff --git a/scripts/tampermonkey/tizentube-loader.user.js b/scripts/tampermonkey/tizentube-loader.user.js new file mode 100644 index 00000000..d51d8824 --- /dev/null +++ b/scripts/tampermonkey/tizentube-loader.user.js @@ -0,0 +1,18 @@ +// ==UserScript== +// @name TizenTube Loader +// @namespace https://github.com/reisxd/TizenTube +// @version 0.4 +// @description Load TizenTube userscript bundle for local /tv debugging +// @match https://www.youtube.com/tv* +// @match https://youtube.com/tv* +// @run-at document-start +// @grant none +// @updateURL https://raw.githubusercontent.com/reisxd/TizenTube/main/scripts/tampermonkey/tizentube-loader.user.js +// @downloadURL https://raw.githubusercontent.com/reisxd/TizenTube/main/scripts/tampermonkey/tizentube-loader.user.js +// @require https://raw.githubusercontent.com/reisxd/TizenTube/main/dist/userScript.js +// ==/UserScript== + +(function () { + // The actual TizenTube code is loaded via @require. + // Tampermonkey updates this script by @version and update checks. +})(); \ No newline at end of file diff --git a/scripts/tampermonkey/tizentube-log-button.user.js b/scripts/tampermonkey/tizentube-log-button.user.js new file mode 100644 index 00000000..206f3b70 --- /dev/null +++ b/scripts/tampermonkey/tizentube-log-button.user.js @@ -0,0 +1,54 @@ +// ==UserScript== +// @name TizenTube Log Download Button +// @namespace https://github.com/reisxd/TizenTube +// @version 0.1 +// @description Inject a floating button to trigger window.downloadTizenTubeLogs() +// @match https://www.youtube.com/tv* +// @match https://youtube.com/tv* +// @run-at document-idle +// @grant none +// @updateURL https://raw.githubusercontent.com/reisxd/TizenTube/main/scripts/tampermonkey/tizentube-log-button.user.js +// @downloadURL https://raw.githubusercontent.com/reisxd/TizenTube/main/scripts/tampermonkey/tizentube-log-button.user.js +// ==/UserScript== + +(function () { + const buttonId = 'tt-log-download-btn'; + + const ensureButton = () => { + if (document.getElementById(buttonId)) return; + + const btn = document.createElement('button'); + btn.id = buttonId; + btn.textContent = 'TT Logs'; + btn.style.cssText = [ + 'position:fixed', + 'right:14px', + 'bottom:14px', + 'z-index:2147483647', + 'padding:8px 10px', + 'font-size:12px', + 'font-family:monospace', + 'color:#0f0', + 'background:#111', + 'border:1px solid #0f0', + 'border-radius:4px', + 'cursor:pointer', + 'opacity:0.9' + ].join(';'); + + btn.addEventListener('click', () => { + if (typeof window.downloadTizenTubeLogs === 'function') { + window.downloadTizenTubeLogs(); + } else { + console.warn('[TT_LOG_BUTTON] window.downloadTizenTubeLogs is not available yet'); + } + }); + + document.body.appendChild(btn); + }; + + const interval = setInterval(() => { + if (document.body) ensureButton(); + if (document.getElementById(buttonId)) clearInterval(interval); + }, 500); +})(); \ No newline at end of file