Compare commits

..

2 Commits

Author SHA1 Message Date
cottongin
d7a834a992 feat: add file import and prompt() methods for API key entry in OBS
OBS Browser Source lacks clipboard support, making it impractical to
enter long API keys. Add two workarounds: "Import from File" reads a
key from a .txt file via the native file picker, and "Paste Key" uses
window.prompt() where OS-level paste should work.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 01:30:56 -04:00
cottongin
3bd5752cb5 feat: enhance full-room glow with configurable controls and prevent text wrapping
Add white-space: nowrap to header, footer, and code-part elements to
prevent line breaks in narrow viewports. Replace static CSS keyframes
with dynamic generation so glow color, intensity, opacity, outline
thickness, and pulse duration are all configurable from the controls
panel. Double the default outline thickness from 4px to 8px.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 01:24:47 -04:00
3 changed files with 148 additions and 14 deletions

49
js/controls.js vendored
View File

@@ -194,6 +194,42 @@ function initControls(manager, wsClient, components) {
localStorage.setItem(STORAGE_API_KEY, apiKeyInput.value.trim()); localStorage.setItem(STORAGE_API_KEY, apiKeyInput.value.trim());
}); });
const importKeyBtn = document.getElementById('import-key-btn');
const importKeyFile = /** @type {HTMLInputElement | null} */ (
document.getElementById('import-key-file')
);
const promptKeyBtn = document.getElementById('prompt-key-btn');
function applyImportedKey(key) {
if (!apiKeyInput) return;
apiKeyInput.value = key;
localStorage.setItem(STORAGE_API_KEY, key);
}
if (importKeyBtn && importKeyFile) {
importKeyBtn.addEventListener('click', () => {
importKeyFile.value = '';
importKeyFile.click();
});
importKeyFile.addEventListener('change', () => {
const file = importKeyFile.files && importKeyFile.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function () {
var text = typeof reader.result === 'string' ? reader.result.trim() : '';
if (text) applyImportedKey(text);
};
reader.readAsText(file);
});
}
if (promptKeyBtn) {
promptKeyBtn.addEventListener('click', () => {
var key = window.prompt('Paste or type your API key:');
if (key && key.trim()) applyImportedKey(key.trim());
});
}
const volumeSlider = document.getElementById('volume-slider'); const volumeSlider = document.getElementById('volume-slider');
const volumeValue = document.getElementById('volume-value'); const volumeValue = document.getElementById('volume-value');
const themeSound = /** @type {HTMLAudioElement | null} */ ( const themeSound = /** @type {HTMLAudioElement | null} */ (
@@ -340,22 +376,25 @@ function initConnectionStatusHandler() {
const wsDisconnectRow = document.getElementById('ws-disconnect-row'); const wsDisconnectRow = document.getElementById('ws-disconnect-row');
const apiUrlInput = document.getElementById('api-url-input'); const apiUrlInput = document.getElementById('api-url-input');
const apiKeyInput = document.getElementById('api-key-input'); const apiKeyInput = document.getElementById('api-key-input');
const importKeyBtn = document.getElementById('import-key-btn');
const promptKeyBtn = document.getElementById('prompt-key-btn');
return (state, message) => { return (state, message) => {
if (wsStatusDot) wsStatusDot.className = `status-dot ${state}`; if (wsStatusDot) wsStatusDot.className = `status-dot ${state}`;
if (wsStatusText) wsStatusText.textContent = message ?? String(state); if (wsStatusText) wsStatusText.textContent = message ?? String(state);
if (state === 'connected') { var isConnected = state === 'connected';
if (isConnected) {
if (wsConnectBtn) wsConnectBtn.style.display = 'none'; if (wsConnectBtn) wsConnectBtn.style.display = 'none';
if (wsDisconnectRow) wsDisconnectRow.style.display = 'flex'; if (wsDisconnectRow) wsDisconnectRow.style.display = 'flex';
if (apiUrlInput) apiUrlInput.disabled = true;
if (apiKeyInput) apiKeyInput.disabled = true;
} else { } else {
if (wsConnectBtn) wsConnectBtn.style.display = 'block'; if (wsConnectBtn) wsConnectBtn.style.display = 'block';
if (wsDisconnectRow) wsDisconnectRow.style.display = 'none'; if (wsDisconnectRow) wsDisconnectRow.style.display = 'none';
if (apiUrlInput) apiUrlInput.disabled = false;
if (apiKeyInput) apiKeyInput.disabled = false;
} }
if (apiUrlInput) apiUrlInput.disabled = isConnected;
if (apiKeyInput) apiKeyInput.disabled = isConnected;
if (importKeyBtn) importKeyBtn.disabled = isConnected;
if (promptKeyBtn) promptKeyBtn.disabled = isConnected;
}; };
} }

View File

@@ -41,6 +41,12 @@
* @property {HTMLInputElement} line2AppearDuration * @property {HTMLInputElement} line2AppearDuration
* @property {HTMLInputElement} line2HideTime * @property {HTMLInputElement} line2HideTime
* @property {HTMLInputElement} line2HideDuration * @property {HTMLInputElement} line2HideDuration
* @property {HTMLInputElement} glowColor
* @property {HTMLInputElement} glowIntensity
* @property {HTMLInputElement} glowOpacity
* @property {HTMLInputElement} glowWhiteIntensity
* @property {HTMLInputElement} glowOutline
* @property {HTMLInputElement} glowPulseDuration
*/ */
class RoomCodeDisplay { class RoomCodeDisplay {
@@ -65,6 +71,8 @@ class RoomCodeDisplay {
/** @type {number | null} */ /** @type {number | null} */
#meterRafId = null; #meterRafId = null;
/** @type {HTMLStyleElement | null} */
#glowStyleEl = null;
/** /**
* @param {RoomCodeDisplayElements} elements * @param {RoomCodeDisplayElements} elements
@@ -73,6 +81,12 @@ class RoomCodeDisplay {
init(elements, inputs) { init(elements, inputs) {
this.#elements = elements; this.#elements = elements;
this.#inputs = inputs; this.#inputs = inputs;
if (!this.#glowStyleEl) {
this.#glowStyleEl = document.createElement('style');
this.#glowStyleEl.id = 'glow-keyframes';
document.head.appendChild(this.#glowStyleEl);
}
} }
/** /**
@@ -217,6 +231,9 @@ class RoomCodeDisplay {
footer.style.color = inputs.footerColor.value; footer.style.color = inputs.footerColor.value;
footer.style.fontSize = `${inputs.footerSize.value}px`; footer.style.fontSize = `${inputs.footerSize.value}px`;
footer.style.transform = `translateY(${inputs.footerOffset.value}px)`; footer.style.transform = `translateY(${inputs.footerOffset.value}px)`;
this.#rebuildGlowKeyframes();
this.#checkFullPulse();
} }
#startAnimation() { #startAnimation() {
@@ -370,14 +387,52 @@ class RoomCodeDisplay {
this.#meterRafId = requestAnimationFrame(step); this.#meterRafId = requestAnimationFrame(step);
} }
#rebuildGlowKeyframes() {
const inputs = this.#inputs;
const style = this.#glowStyleEl;
if (!inputs || !style) return;
const outline = Number(inputs.glowOutline?.value ?? 8);
const whiteGlow = Number(inputs.glowWhiteIntensity?.value ?? 40);
const colorGlow = Number(inputs.glowIntensity?.value ?? 80);
const opacity = Number(inputs.glowOpacity?.value ?? 0.6);
const duration = Number(inputs.glowPulseDuration?.value ?? 1.2);
const glowHex = inputs.glowColor?.value ?? '#f35dcb';
const r = parseInt(glowHex.slice(1, 3), 16);
const g = parseInt(glowHex.slice(3, 5), 16);
const b = parseInt(glowHex.slice(5, 7), 16);
const outlineShadow =
`drop-shadow(0 0 ${outline}px black) drop-shadow(0 0 ${outline}px black)`;
style.textContent = `
@keyframes meter-full-pulse {
0%, 100% {
filter: ${outlineShadow} drop-shadow(0 0 0 rgba(255,255,255,0));
}
50% {
filter: ${outlineShadow}
drop-shadow(0 0 ${whiteGlow}px rgba(255,255,255,0.95))
drop-shadow(0 0 ${colorGlow}px rgba(${r},${g},${b},${opacity}));
}
}
#header.meter-full-pulse {
animation: meter-full-pulse ${duration}s ease-in-out infinite;
}
`;
}
#checkFullPulse() { #checkFullPulse() {
const header = this.#elements?.header; const header = this.#elements?.header;
if (!header) return; if (!header) return;
if (this.#meterFill >= 1) { if (this.#meterFill >= 1) {
this.#rebuildGlowKeyframes();
header.classList.add('meter-full-pulse'); header.classList.add('meter-full-pulse');
} else { } else {
header.classList.remove('meter-full-pulse'); header.classList.remove('meter-full-pulse');
header.style.animation = '';
} }
} }
} }

View File

@@ -30,6 +30,7 @@
#header { #header {
position: absolute; position: absolute;
width: fit-content; width: fit-content;
white-space: nowrap;
left: 50%; left: 50%;
transform: translateX(-50%) translateY(-220px); transform: translateX(-50%) translateY(-220px);
color: #f35dcb; color: #f35dcb;
@@ -45,19 +46,12 @@
filter: drop-shadow(3px 3px 8px rgba(0, 0, 0, 0.8)); filter: drop-shadow(3px 3px 8px rgba(0, 0, 0, 0.8));
} }
@keyframes meter-full-pulse { /* meter-full-pulse keyframes are generated dynamically by RoomCodeDisplay */
0% { filter: drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); }
50% { filter: drop-shadow(0 0 20px rgba(255,255,255,0.6)) drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); }
100% { filter: drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); }
}
#header.meter-full-pulse {
animation: meter-full-pulse 1.2s ease-in-out infinite;
}
#footer { #footer {
position: absolute; position: absolute;
width: 100%; width: 100%;
white-space: nowrap;
text-align: center; text-align: center;
transform: translateY(160px); transform: translateY(160px);
color: #f35dcb; color: #f35dcb;
@@ -77,6 +71,7 @@
letter-spacing: 8px; letter-spacing: 8px;
position: absolute; position: absolute;
width: 100%; width: 100%;
white-space: nowrap;
text-align: center; text-align: center;
opacity: 0; opacity: 0;
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
@@ -542,6 +537,40 @@
</div> </div>
</div> </div>
<!-- Glow Effect Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Glow Effect Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-row">
<label>Glow Color:</label>
<input type="color" id="glow-color-input" value="#f35dcb">
</div>
<div class="control-row">
<label>Glow Intensity (px):</label>
<input type="number" id="glow-intensity-input" value="80" min="10" max="200" step="5">
</div>
<div class="control-row">
<label>Glow Opacity:</label>
<input type="number" id="glow-opacity-input" value="0.6" min="0" max="1" step="0.05">
</div>
<div class="control-row">
<label>White Glow (px):</label>
<input type="number" id="glow-white-intensity-input" value="40" min="0" max="200" step="5">
</div>
<div class="control-row">
<label>Outline Thickness (px):</label>
<input type="number" id="glow-outline-input" value="8" min="0" max="30" step="1">
</div>
<div class="control-row">
<label>Pulse Duration (sec):</label>
<input type="number" id="glow-pulse-duration-input" value="1.2" min="0.2" max="5" step="0.1">
</div>
</div>
</div>
<!-- Footer Settings Section --> <!-- Footer Settings Section -->
<div class="control-section"> <div class="control-section">
<div class="section-header"> <div class="section-header">
@@ -735,6 +764,11 @@
<div class="control-row"> <div class="control-row">
<input type="password" id="api-key-input" placeholder="Enter API key"> <input type="password" id="api-key-input" placeholder="Enter API key">
</div> </div>
<div class="control-row" style="gap: 6px;">
<button id="import-key-btn" title="Import API key from a .txt file">Import from File</button>
<button id="prompt-key-btn" title="Enter API key via dialog (paste works)">Paste Key</button>
<input type="file" id="import-key-file" accept=".txt,.key" style="display: none;">
</div>
<div class="control-row"> <div class="control-row">
<button id="ws-connect-btn">Connect</button> <button id="ws-connect-btn">Connect</button>
</div> </div>
@@ -855,6 +889,12 @@
line2AppearDuration: document.getElementById('line2-appear-duration'), line2AppearDuration: document.getElementById('line2-appear-duration'),
line2HideTime: document.getElementById('line2-hide-time'), line2HideTime: document.getElementById('line2-hide-time'),
line2HideDuration: document.getElementById('line2-hide-duration'), line2HideDuration: document.getElementById('line2-hide-duration'),
glowColor: document.getElementById('glow-color-input'),
glowIntensity: document.getElementById('glow-intensity-input'),
glowOpacity: document.getElementById('glow-opacity-input'),
glowWhiteIntensity: document.getElementById('glow-white-intensity-input'),
glowOutline: document.getElementById('glow-outline-input'),
glowPulseDuration: document.getElementById('glow-pulse-duration-input'),
} }
); );
manager.registerComponent('roomCode', roomCodeDisplay); manager.registerComponent('roomCode', roomCodeDisplay);