mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
From the English-parity and breakage review against main:
- English wording back to main: "Configured Source", the "SHARED SUBJECT"
fallback, CCTV calibration "RANGE 120m".
- Voice header and CCTV calibration chip/select tweaks for long labels now
apply to non-English only (html:not([lang='en'])); English matches main.
The scene select font change is scoped the same way.
- New scenes and shots store their default title in English ("Scene 3",
"Shot 2") and translate it when shown, so a title captured in one language
does not leak into projects, storage or shared JSON.
- Choosing a language with storage blocked keeps it in ?lang instead of
silently doing nothing.
- One picked import file shows its name again.
- qa-l9-matrix reads the voice state from data-status, not the label.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
201 lines
5.8 KiB
JavaScript
201 lines
5.8 KiB
JavaScript
import { t, tn } from '../i18n/index.js';
|
|
|
|
/** Small owned modal for scene authoring and import review; text never becomes markup. */
|
|
export function createSceneDialog(title, onClose) {
|
|
const dialog = document.createElement('dialog');
|
|
dialog.dataset.directorDialog = '';
|
|
dialog.className = 'director-sharing-dialog';
|
|
dialog.setAttribute('aria-label', title);
|
|
Object.assign(dialog.style, {
|
|
background: '#08141e',
|
|
color: '#e3faff',
|
|
border: '1px solid #397080',
|
|
borderRadius: '6px',
|
|
padding: '20px',
|
|
width: 'min(720px,calc(100vw - 32px))',
|
|
boxSizing: 'border-box',
|
|
position: 'fixed',
|
|
inset: '0',
|
|
margin: 'auto',
|
|
maxHeight: 'calc(100vh - 32px)',
|
|
overflow: 'auto',
|
|
font: '14px/1.5 sans-serif',
|
|
});
|
|
const heading = document.createElement('h2');
|
|
heading.textContent = title;
|
|
heading.style.marginBottom = '12px';
|
|
dialog.append(heading);
|
|
const body = document.createElement('div'),
|
|
footer = document.createElement('div'),
|
|
status = document.createElement('p');
|
|
status.setAttribute('role', 'status');
|
|
dialog.append(body, status, footer);
|
|
const listeners = [];
|
|
function listen(node, type, fn) {
|
|
node.addEventListener(type, fn);
|
|
listeners.push(() => node.removeEventListener(type, fn));
|
|
}
|
|
function text(value, parent = body) {
|
|
const p = document.createElement('p');
|
|
p.textContent = value;
|
|
p.style.margin = '8px 0';
|
|
parent.append(p);
|
|
return p;
|
|
}
|
|
function button(label, fn, parent = footer) {
|
|
const b = document.createElement('button');
|
|
b.type = 'button';
|
|
b.textContent = label;
|
|
Object.assign(b.style, {
|
|
background: '#10242e',
|
|
color: '#e3faff',
|
|
border: '1px solid #397080',
|
|
borderRadius: '3px',
|
|
padding: '7px 11px',
|
|
margin: '6px 6px 6px 0',
|
|
cursor: 'pointer',
|
|
font: 'inherit',
|
|
});
|
|
listen(b, 'click', fn);
|
|
parent.append(b);
|
|
return b;
|
|
}
|
|
function input(label, value, { type = 'text', multiline = false } = {}) {
|
|
const wrapper = document.createElement('label');
|
|
wrapper.style.display = 'block';
|
|
wrapper.textContent = label;
|
|
const node = document.createElement(multiline ? 'textarea' : 'input');
|
|
if (!multiline) node.type = type;
|
|
node.value = value;
|
|
node.setAttribute('aria-label', label);
|
|
Object.assign(node.style, {
|
|
display: 'block',
|
|
boxSizing: 'border-box',
|
|
width: '100%',
|
|
padding: '7px',
|
|
margin: '6px 0 14px',
|
|
color: '#e3faff',
|
|
background: '#10242e',
|
|
border: '1px solid #397080',
|
|
font: multiline ? '12px monospace' : 'inherit',
|
|
});
|
|
if (multiline) node.rows = 9;
|
|
wrapper.append(node);
|
|
if (type === 'file') fileChooser(wrapper, node);
|
|
body.append(wrapper);
|
|
return node;
|
|
}
|
|
/**
|
|
* The browser draws a file input's button and "no file" text in its own
|
|
* language. Hide it visually (it stays focusable and is still opened by its
|
|
* label) and show a translated button and selection line instead.
|
|
*/
|
|
function fileChooser(wrapper, node) {
|
|
wrapper.style.position = 'relative';
|
|
Object.assign(node.style, {
|
|
position: 'absolute',
|
|
width: '1px',
|
|
height: '1px',
|
|
margin: '0',
|
|
padding: '0',
|
|
overflow: 'hidden',
|
|
clip: 'rect(0 0 0 0)',
|
|
whiteSpace: 'nowrap',
|
|
border: '0',
|
|
});
|
|
const row = document.createElement('span');
|
|
row.setAttribute('aria-hidden', 'true');
|
|
Object.assign(row.style, {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '10px',
|
|
margin: '6px 0 14px',
|
|
padding: '7px',
|
|
background: '#10242e',
|
|
border: '1px solid #397080',
|
|
});
|
|
const chooser = document.createElement('span');
|
|
chooser.textContent = t('Choose files');
|
|
Object.assign(chooser.style, {
|
|
padding: '3px 9px',
|
|
border: '1px solid #397080',
|
|
borderRadius: '3px',
|
|
background: '#08141e',
|
|
cursor: 'pointer',
|
|
});
|
|
const chosen = document.createElement('span');
|
|
const showChosen = () => {
|
|
const count = node.files?.length || 0;
|
|
// One file shows its name, like the browser's own picker did.
|
|
chosen.textContent =
|
|
count === 1
|
|
? node.files[0].name
|
|
: count
|
|
? tn(count, '{count} file selected', '{count} files selected')
|
|
: t('No file chosen');
|
|
};
|
|
showChosen();
|
|
row.append(chooser, chosen);
|
|
wrapper.append(row);
|
|
listen(node, 'change', showChosen);
|
|
listen(node, 'focus', () => {
|
|
chooser.style.outline = '2px solid #e3faff';
|
|
});
|
|
listen(node, 'blur', () => {
|
|
chooser.style.outline = '';
|
|
});
|
|
}
|
|
button(t('Cancel'), onClose);
|
|
listen(dialog, 'cancel', (event) => {
|
|
event.preventDefault();
|
|
onClose();
|
|
});
|
|
document.body.append(dialog);
|
|
dialog.showModal();
|
|
return {
|
|
body,
|
|
footer,
|
|
status,
|
|
text,
|
|
button,
|
|
input,
|
|
listen,
|
|
element: dialog,
|
|
dispose() {
|
|
for (const remove of listeners.splice(0)) remove();
|
|
dialog.close();
|
|
dialog.remove();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Add controls only when their owner is present; dispose removes every listener and element. */
|
|
export function mountSceneSharing(
|
|
{ edit, share },
|
|
panel = document.getElementById('scene-panel'),
|
|
) {
|
|
if (!panel) return () => {};
|
|
const bar = document.createElement('div');
|
|
bar.dataset.directorAuthoring = '';
|
|
bar.className = 'scene-controls';
|
|
const entries = [
|
|
[t('EDIT DETAILS'), edit],
|
|
[t('SHARE SCENE'), share],
|
|
];
|
|
const removers = [];
|
|
for (const [text, fn] of entries) {
|
|
const b = document.createElement('button');
|
|
b.type = 'button';
|
|
b.textContent = text;
|
|
b.className = 'scene-btn';
|
|
b.addEventListener('click', fn);
|
|
removers.push(() => b.removeEventListener('click', fn));
|
|
bar.append(b);
|
|
}
|
|
(panel.querySelector?.('.scene-panel-inner') || panel).appendChild(bar);
|
|
return () => {
|
|
for (const remove of removers) remove();
|
|
bar.remove();
|
|
};
|
|
}
|