mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
i18n: translate the application views with t() as they render
Every data-i18n, data-i18n-title, data-i18n-aria-label and data-i18n-placeholder marker in the Vue templates becomes a t() call with the key the boot pass used to look up, contexts included. The three labels the shell rewrote at boot (Visual Presets, Location and the standby description) render through t() too, and that rewrite is gone. With the locale in vue-i18n, view text now follows a locale change, and markup under v-if or v-for no longer comes back in English when it re-renders. Rendered text and attributes are unchanged in every locale. The boot pass still translates static page markup such as the loading screen and a host's own markup, and npm run i18n rejects data-i18n* in a Vue template.
This commit is contained in:
1 parent
2c72a038d5
commit
ba20bc13d8
33 files changed
+377
-344
No files matched your search
+1
-1
@@ -126,7 +126,7 @@ The highest-leverage places to jump in:
|
|||||||
- ES modules, **2-space indent, single quotes, semicolons.**
|
- ES modules, **2-space indent, single quotes, semicolons.**
|
||||||
- JSDoc on exported/public functions.
|
- JSDoc on exported/public functions.
|
||||||
- Match the surrounding code — comment density, naming, and idiom.
|
- Match the surrounding code — comment density, naming, and idiom.
|
||||||
- User-visible text goes through `t()` or `tn()`, with `msg()` marking English that is stored and translated later. Static page markup that predates the views keeps its `data-i18n*` attributes; components use `t()`. `npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
|
- User-visible text goes through `t()` or `tn()`, with `msg()` marking English that is stored and translated later. Components call `t()` in their templates, so their text follows the locale; `data-i18n*` attributes are only for static page markup such as the loading screen, and `npm run i18n` rejects them in a template. `npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md).
|
||||||
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
|
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
|
||||||
|
|
||||||
## Building UI
|
## Building UI
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
- The application views translate their text with `t()` as they render,
|
||||||
|
so it follows the active locale; they no longer carry `data-i18n*`
|
||||||
|
attributes, and `npm run i18n` rejects those in a Vue template. The boot
|
||||||
|
pass (`applyDocumentTranslations`) still translates static page markup
|
||||||
|
such as the loading screen and a host's own markup (samehkhamis, #0000).
|
||||||
|
|
||||||
- The page's locale lives in a vue-i18n instance installed in every
|
- The page's locale lives in a vue-i18n instance installed in every
|
||||||
application view, and `t()`, `tn()` and the Intl formatters read it from
|
application view, and `t()`, `tn()` and the Intl formatters read it from
|
||||||
there, so a view that translates while rendering renders again when the
|
there, so a view that translates while rendering renders again when the
|
||||||
|
|||||||
+17
-7
@@ -31,13 +31,23 @@ Vue views call the same `t()` and `tn()`. The page's vue-i18n instance (`src/ui/
|
|||||||
|
|
||||||
## Markup
|
## Markup
|
||||||
|
|
||||||
|
Vue views translate in the template, so their text renders in the active locale:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<span>{{ t('Normal') }}</span>
|
||||||
|
<button :title="t('Pin visual presets')">…</button>
|
||||||
|
<option value="all">{{ t('All', { context: 'radio tag' }) }}</option>
|
||||||
|
```
|
||||||
|
|
||||||
|
Static page markup that no view renders (the loading screen in `index.html`, a host's own markup) opts in with attributes instead, and `npm run i18n` rejects them in a Vue template:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<span data-i18n>Normal</span>
|
<span data-i18n>Normal</span>
|
||||||
<button title="Pin visual presets" data-i18n-title>…</button>
|
<button title="Pin visual presets" data-i18n-title>…</button>
|
||||||
<input placeholder="Search any location..." data-i18n-placeholder />
|
<input placeholder="Search any location..." data-i18n-placeholder />
|
||||||
```
|
```
|
||||||
|
|
||||||
`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading.
|
`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Attribute markup is translated once at boot. Switching language still reloads the page: views would follow a new locale, but much of their text is published already translated by code that does not. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
@@ -68,16 +78,16 @@ An empty translation is reported, and so is one left as the English, plural form
|
|||||||
|
|
||||||
`npm run i18n` and `npm test` fail on UI text that is not marked:
|
`npm run i18n` and `npm test` fail on UI text that is not marked:
|
||||||
|
|
||||||
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`.
|
- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. In a Vue template, static text or a static `title`, `aria-label` or `placeholder` (rather than `t()`), and any `data-i18n*` attribute.
|
||||||
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`.
|
- **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`.
|
||||||
|
|
||||||
Fix it by marking the text. When the text must stay as it is, say so where it lives:
|
Fix it by marking the text. When the text must stay as it is, say so where it lives:
|
||||||
|
|
||||||
| Case | How |
|
| Case | How |
|
||||||
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
||||||
| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML |
|
| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML |
|
||||||
| Markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element |
|
| Page markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element (not in a Vue template) |
|
||||||
| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: <reason>` on the line or the line above |
|
| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: <reason>` on the line or the line above |
|
||||||
|
|
||||||
Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page.
|
Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page.
|
||||||
|
|
||||||
|
|||||||
@@ -153,10 +153,9 @@ a feature supplying only its accent.
|
|||||||
Next each view takes over its wiring: markup an owner builds with
|
Next each view takes over its wiring: markup an owner builds with
|
||||||
`createElement` becomes component markup that renders from a Pinia store
|
`createElement` becomes component markup that renders from a Pinia store
|
||||||
the owner publishes to, and user actions come back to the owner through
|
the owner publishes to, and user actions come back to the owner through
|
||||||
the store, so the owner keeps every decision. Static markup that the
|
the store, so the owner keeps every decision. Component text uses
|
||||||
translation pass rewrites (`data-i18n*`) stays outside `v-if` and
|
`t()`, which follows the locale as the view renders; the views carry no
|
||||||
`v-for`, because re-rendering it would bring back the English; component
|
`data-i18n*` markers, which `npm run i18n` rejects in a template. Shared builders become components first and
|
||||||
text uses `t()` instead. Shared builders become components first and
|
|
||||||
keep their imperative entry points until their owners are components:
|
keep their imperative entry points until their owners are components:
|
||||||
the rail cards, card blocks and timeline (`src/ui/rail/`) render through
|
the rail cards, card blocks and timeline (`src/ui/rail/`) render through
|
||||||
Vue's synchronous `render()` behind `createRailCards()`,
|
Vue's synchronous `render()` behind `createRailCards()`,
|
||||||
@@ -344,8 +343,10 @@ a feature supplying only its accent.
|
|||||||
now requires the pattern for new UI. The page-level markup tests read
|
now requires the pattern for new UI. The page-level markup tests read
|
||||||
`renderApplicationHtml()`, the real views rendered on the server, and new
|
`renderApplicationHtml()`, the real views rendered on the server, and new
|
||||||
tests mount components; three tests still read a `.vue` file's source
|
tests mount components; three tests still read a `.vue` file's source
|
||||||
(`cyberTheme`, `panelStorageDocs`, `geoFileImport`). The attribute translation pass goes with the move to
|
(`cyberTheme`, `panelStorageDocs`, `geoFileImport`). The views translate with `t()`
|
||||||
vue-i18n. An explicit API for hosts to register panels and rail items is
|
while rendering, and vue-i18n holds the locale they read; the attribute
|
||||||
|
translation pass is left for static page markup (the loading screen and a
|
||||||
|
host's own markup). An explicit API for hosts to register panels and rail items is
|
||||||
deferred until a host adds one: no host does today (the hosted
|
deferred until a host adds one: no host does today (the hosted
|
||||||
application's account interface is its own markup), and the first such
|
application's account interface is its own markup), and the first such
|
||||||
panel, the hosted timeline, will shape it.
|
panel, the hosted timeline, will shape it.
|
||||||
|
|||||||
+39
-3
@@ -156,7 +156,7 @@ const SKIP_TAGS = new Set(['script', 'style', 'svg', 'code', 'pre', 'title']);
|
|||||||
/**
|
/**
|
||||||
* User-visible markup text without data-i18n*. Skipped, with everything
|
* User-visible markup text without data-i18n*. Skipped, with everything
|
||||||
* inside: translate="no" (text that stays as is, as in HTML),
|
* inside: translate="no" (text that stays as is, as in HTML),
|
||||||
* data-i18n-runtime (code sets this text through t()), and icons
|
* data-i18n-runtime (page markup whose text code sets through t()), and icons
|
||||||
* (aria-hidden, Material Symbols).
|
* (aria-hidden, Material Symbols).
|
||||||
*/
|
*/
|
||||||
export function unmarkedMarkup(file, html) {
|
export function unmarkedMarkup(file, html) {
|
||||||
@@ -404,8 +404,13 @@ export function unmarkedScript(file, original) {
|
|||||||
*/
|
*/
|
||||||
export function findUnmarked() {
|
export function findUnmarked() {
|
||||||
const found = [];
|
const found = [];
|
||||||
for (const file of markupFiles())
|
for (const file of markupFiles()) {
|
||||||
found.push(...unmarkedMarkup(file, readMarkup(file)));
|
found.push(...unmarkedMarkup(file, readMarkup(file)));
|
||||||
|
}
|
||||||
|
for (const file of trackedFiles('src/**/*.vue'))
|
||||||
|
found.push(
|
||||||
|
...markedTemplate(file, readFileSync(path.join(ROOT, file), 'utf8')),
|
||||||
|
);
|
||||||
for (const file of sourceFiles().filter(
|
for (const file of sourceFiles().filter(
|
||||||
(name) => !NOT_UI.some((pattern) => pattern.test(name)),
|
(name) => !NOT_UI.some((pattern) => pattern.test(name)),
|
||||||
))
|
))
|
||||||
@@ -421,7 +426,38 @@ export function findUnmarked() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Static markup opted in with data-i18n*: the page and its views. */
|
/**
|
||||||
|
* data-i18n* in a Vue template: views translate with t() as they render, so
|
||||||
|
* their text follows the locale; the attributes are for static page markup.
|
||||||
|
* Reads the whole single-file component, so lines are the file's own.
|
||||||
|
*/
|
||||||
|
export function markedTemplate(file, source) {
|
||||||
|
const start = source.indexOf('<template>');
|
||||||
|
if (start === -1) return [];
|
||||||
|
const end = source.lastIndexOf('</template>');
|
||||||
|
// Comments and attribute values are blanked, keeping every offset.
|
||||||
|
const blank = (text) => text.replace(/[^\n]/g, ' ');
|
||||||
|
const template = source
|
||||||
|
.slice(0, end === -1 ? undefined : end)
|
||||||
|
.replace(/<!--[\s\S]*?-->/g, blank);
|
||||||
|
const found = [];
|
||||||
|
const TAG = /<[a-zA-Z][\w-]*((?:"[^"]*"|'[^']*'|[^>"'])*)>/g;
|
||||||
|
TAG.lastIndex = start + '<template>'.length;
|
||||||
|
for (let tag; (tag = TAG.exec(template));) {
|
||||||
|
const attrs = tag[1].replace(/"[^"]*"|'[^']*'/g, blank);
|
||||||
|
const offset = tag.index + tag[0].length - 1 - tag[1].length;
|
||||||
|
for (const match of attrs.matchAll(
|
||||||
|
/(?<=\s)(?::|v-bind:)?(data-i18n[\w-]*)(?=[\s=/]|$)/g,
|
||||||
|
))
|
||||||
|
found.push({
|
||||||
|
where: `${file}:${lineAt(source, offset + match.index)}`,
|
||||||
|
text: `${match[1]}: use t() in a Vue template`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Markup checked for unmarked text: the page and its views. */
|
||||||
function markupFiles() {
|
function markupFiles() {
|
||||||
return trackedFiles('index.html', 'src/ui/views/*.vue');
|
return trackedFiles('index.html', 'src/ui/views/*.vue');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ test('markup, startup ordering and accessibility remain pinned', async () => {
|
|||||||
// passing edit — changing it needs the owner, not a nicer-sounding rewrite.
|
// passing edit — changing it needs the owner, not a nicer-sounding rewrite.
|
||||||
assert.ok(
|
assert.ok(
|
||||||
html.includes(
|
html.includes(
|
||||||
'<p id="first-run-description" data-i18n-runtime>It feels like a forbidden cockpit' +
|
'<p id="first-run-description">It feels like a forbidden cockpit' +
|
||||||
'—then you realize the sources are public and the data is real.</p>',
|
'—then you realize the sources are public and the data is real.</p>',
|
||||||
),
|
),
|
||||||
'the owner-authored first-run line must ship exactly as written',
|
'the owner-authored first-run line must ship exactly as written',
|
||||||
@@ -903,8 +903,8 @@ test('the card closes without a keyboard: the ESC hint is a button, kept on phon
|
|||||||
footer,
|
footer,
|
||||||
/<button type="button" class="first-run-dismiss" data-first-run-dismiss>/,
|
/<button type="button" class="first-run-dismiss" data-first-run-dismiss>/,
|
||||||
);
|
);
|
||||||
assert.match(footer, /data-i18n>ESC to dismiss</);
|
assert.match(footer, />ESC to dismiss</);
|
||||||
assert.match(footer, /data-i18n>DISMISS</);
|
assert.match(footer, />DISMISS</);
|
||||||
// It closes the way ESC does, through the one dismiss path, so the session
|
// It closes the way ESC does, through the one dismiss path, so the session
|
||||||
// flag and focus handling are the same: firstRunLauncherView.test.mjs.
|
// flag and focus handling are the same: firstRunLauncherView.test.mjs.
|
||||||
// Phones used to hide the hint, leaving no way out but picking a mission.
|
// Phones used to hide the hint, leaving no way out but picking a mission.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
checkCatalog,
|
checkCatalog,
|
||||||
extractMessages,
|
extractMessages,
|
||||||
findUnmarked,
|
findUnmarked,
|
||||||
|
markedTemplate,
|
||||||
readCatalog,
|
readCatalog,
|
||||||
shippedLocales,
|
shippedLocales,
|
||||||
unmarkedMarkup,
|
unmarkedMarkup,
|
||||||
@@ -111,6 +112,32 @@ test('unmarkedMarkup finds text and attributes without data-i18n*', () => {
|
|||||||
assert.deepEqual(texts('<!-- Note --><span>— 42 —</span>'), []);
|
assert.deepEqual(texts('<!-- Note --><span>— 42 —</span>'), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('markedTemplate rejects data-i18n* attributes in a Vue template', () => {
|
||||||
|
const view = [
|
||||||
|
'<script setup>',
|
||||||
|
"const note = 'data-i18n';",
|
||||||
|
'</script>',
|
||||||
|
'<template>',
|
||||||
|
' <!-- no data-i18n here -->',
|
||||||
|
' <span data-i18n>Normal</span>',
|
||||||
|
' <b title="Explains data-i18n" data-i18n-title>{{ t(\'Go\') }}</b>',
|
||||||
|
' <i :data-i18n="true" />',
|
||||||
|
'</template>',
|
||||||
|
].join('\n');
|
||||||
|
assert.deepEqual(markedTemplate('View.vue', view), [
|
||||||
|
{ where: 'View.vue:6', text: 'data-i18n: use t() in a Vue template' },
|
||||||
|
{ where: 'View.vue:7', text: 'data-i18n-title: use t() in a Vue template' },
|
||||||
|
{ where: 'View.vue:8', text: 'data-i18n: use t() in a Vue template' },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(
|
||||||
|
markedTemplate(
|
||||||
|
'View.vue',
|
||||||
|
'<template><b :title="t(\'Pin\')">x</b></template>',
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('unmarkedScript finds literals that reach the page without t/tn/msg', () => {
|
test('unmarkedScript finds literals that reach the page without t/tn/msg', () => {
|
||||||
const texts = (js) => unmarkedScript('f.js', js).map((f) => f.text);
|
const texts = (js) => unmarkedScript('f.js', js).map((f) => f.text);
|
||||||
assert.deepEqual(texts("el.textContent = 'Saved';"), ['Saved']);
|
assert.deepEqual(texts("el.textContent = 'Saved';"), ['Saved']);
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ test('the Visual Presets tray owns Map Source and the retired left panel is abse
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
html,
|
html,
|
||||||
/<section class="map-source-section"[\s\S]*?<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source" data-i18n-aria-label><\/div>/,
|
/<section class="map-source-section"[\s\S]*?<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source"><\/div>/,
|
||||||
);
|
);
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ export function createStandaloneApplication({
|
|||||||
constructed = true;
|
constructed = true;
|
||||||
// Locale is resolved before any UI initialization so <html lang>/<html dir>
|
// Locale is resolved before any UI initialization so <html lang>/<html dir>
|
||||||
// and every catalog lookup agree from the first painted frame. Module
|
// and every catalog lookup agree from the first painted frame. Module
|
||||||
// scripts execute after parsing, so the DOM is ready here and every
|
// scripts execute after parsing, so the DOM is ready here: the views
|
||||||
// data-i18n* element honors the resolved locale without a reload.
|
// translate as they render, and the page's static data-i18n* markup (the
|
||||||
|
// loading screen) is translated once.
|
||||||
const activeLocale = resolveLocale();
|
const activeLocale = resolveLocale();
|
||||||
setLocale(activeLocale);
|
setLocale(activeLocale);
|
||||||
applyDocumentLanguage(document, activeLocale);
|
applyDocumentLanguage(document, activeLocale);
|
||||||
// Views render the page's markup; translation and wiring read it.
|
// Views render the page's markup; wiring reads it.
|
||||||
mountApplicationViews(document);
|
mountApplicationViews(document);
|
||||||
applyDocumentTranslations(document);
|
applyDocumentTranslations(document);
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
const loadingScreen = document.getElementById('loading-screen');
|
||||||
|
|||||||
@@ -575,8 +575,6 @@ export class StyleManager extends ShellFacade {
|
|||||||
});
|
});
|
||||||
this._initUI();
|
this._initUI();
|
||||||
this._initMapStackControl();
|
this._initMapStackControl();
|
||||||
// Headers first: panel chrome builds its toggle labels from them.
|
|
||||||
this._applyRuntimeStaticHeaderText();
|
|
||||||
this._initPanelChrome();
|
this._initPanelChrome();
|
||||||
this._panelNavigation = new PanelNavigation({
|
this._panelNavigation = new PanelNavigation({
|
||||||
ui: this._ui,
|
ui: this._ui,
|
||||||
@@ -802,55 +800,6 @@ export class StyleManager extends ShellFacade {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Rewrites a header's last text node, keeping the leading icon span that a
|
|
||||||
* textContent write would destroy.
|
|
||||||
* @param {Element|null} element
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_setHeaderText(element, text) {
|
|
||||||
if (!element) return;
|
|
||||||
const textNode = Array.from(element.childNodes)
|
|
||||||
.reverse()
|
|
||||||
.find(
|
|
||||||
(node) => node.nodeType === Node.TEXT_NODE && node.nodeValue?.trim(),
|
|
||||||
);
|
|
||||||
if (textNode) textNode.nodeValue = text;
|
|
||||||
else element.textContent = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates at boot the header texts that share their element with an
|
|
||||||
* icon or a <br> (data-i18n-runtime in the templates).
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_applyRuntimeStaticHeaderText() {
|
|
||||||
this._setHeaderText(
|
|
||||||
document.querySelector('#control-panel .panel-title'),
|
|
||||||
t('VISUAL PRESETS'),
|
|
||||||
);
|
|
||||||
this._setHeaderText(
|
|
||||||
document.querySelector('#location-bar .location-toolbar-label'),
|
|
||||||
t('LOCATION'),
|
|
||||||
);
|
|
||||||
// One span holds both mode descriptions around a <br>; keep the <br>.
|
|
||||||
const standbyDescription = document.querySelector(
|
|
||||||
'#context-mode-standby span',
|
|
||||||
);
|
|
||||||
if (standbyDescription) {
|
|
||||||
standbyDescription.replaceChildren(
|
|
||||||
document.createTextNode(
|
|
||||||
t('CONTACTS — nearest planes · vessels · sites'),
|
|
||||||
),
|
|
||||||
document.createElement('br'),
|
|
||||||
document.createTextNode(
|
|
||||||
t('SPACE MISSIONS — launches & orbital assets'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phone layout: a bottom tab bar presenting one panel sheet at a time
|
* Phone layout: a bottom tab bar presenting one panel sheet at a time
|
||||||
* (src/ui/mobileShell.js). Inactive above 720px.
|
* (src/ui/mobileShell.js). Inactive above 720px.
|
||||||
|
|||||||
@@ -654,11 +654,11 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
|
|||||||
const militaryRendering = read('../layers/military/rendering.js');
|
const militaryRendering = read('../layers/military/rendering.js');
|
||||||
const setHud = GEV_ACTION_SCHEMAS.find((schema) => schema.name === 'set_hud');
|
const setHud = GEV_ACTION_SCHEMAS.find((schema) => schema.name === 'set_hud');
|
||||||
|
|
||||||
assert.match(display, /<option value="cyber" data-i18n>Cyber<\/option>/);
|
assert.match(display, /<option value="cyber">\{\{ t\('Cyber'\) \}\}<\/option>/);
|
||||||
assert.match(display, /id="cyber-sonar-toggle"/);
|
assert.match(display, /id="cyber-sonar-toggle"/);
|
||||||
assert.match(
|
assert.match(
|
||||||
display,
|
display,
|
||||||
/id="cyber-sonar-opacity"[^>]*:model-value="sonar\?\.opacity \?\? 84" aria-label="Cyber contact and label opacity"/,
|
/id="cyber-sonar-opacity"[^>]*:model-value="sonar\?\.opacity \?\? 84" :aria-label="t\('Cyber contact and label opacity'\)"/,
|
||||||
);
|
);
|
||||||
assert.match(display, /id="cyber-sonar-sector"/);
|
assert.match(display, /id="cyber-sonar-sector"/);
|
||||||
assert.deepEqual(setHud.parameters.properties.layout.enum, HUD_LAYOUTS);
|
assert.deepEqual(setHud.parameters.properties.layout.enum, HUD_LAYOUTS);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const tickStyle = (tick) =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Aircraft first-person view. It can be entered from the visible view switcher. -->
|
<!-- Aircraft first-person view. It can be entered from the visible view switcher. -->
|
||||||
<section id="cockpit-hud" aria-label="Aircraft cockpit view" :hidden="!active" :style="utilityStyle" v-class-toggle="{ 'signals-active': active, 'context-active': Boolean(presence?.context) }" data-i18n-aria-label :data-layer="hud.layer ?? undefined">
|
<section id="cockpit-hud" :aria-label="t('Aircraft cockpit view')" :hidden="!active" :style="utilityStyle" v-class-toggle="{ 'signals-active': active, 'context-active': Boolean(presence?.context) }" :data-layer="hud.layer ?? undefined">
|
||||||
<!-- One optical coordinate system: every targeting mark shares this center.
|
<!-- One optical coordinate system: every targeting mark shares this center.
|
||||||
Decorative only; telemetry and interactive panels remain separate. -->
|
Decorative only; telemetry and interactive panels remain separate. -->
|
||||||
<svg class="cockpit-frame-lines" viewBox="0 0 1200 720" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
<svg class="cockpit-frame-lines" viewBox="0 0 1200 720" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
||||||
@@ -117,19 +117,19 @@ const tickStyle = (tick) =>
|
|||||||
<span>+20</span><span>+10</span><span class="active">00</span><span>−10</span><span>−20</span>
|
<span>+20</span><span>+10</span><span class="active">00</span><span>−10</span><span>−20</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-horizon-guide">
|
<div class="cockpit-horizon-guide">
|
||||||
<span></span><small data-i18n>LEVEL</small><span></span>
|
<span></span><small>{{ t('LEVEL') }}</small><span></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-route-direction" class="cockpit-route-direction" :hidden="!direction.visible" aria-label="Estimated destination direction" data-i18n-aria-label :style="direction.angle === null ? undefined : { '--route-angle': direction.angle }">
|
<div id="cockpit-route-direction" class="cockpit-route-direction" :hidden="!direction.visible" :aria-label="t('Estimated destination direction')" :style="direction.angle === null ? undefined : { '--route-angle': direction.angle }">
|
||||||
<svg class="cockpit-route-chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
<svg class="cockpit-route-chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
<path d="M12 2 4.58 19.93c-.34.83.51 1.63 1.31 1.27L12 18.47l6.11 2.73c.8.36 1.65-.44 1.31-1.27L12 2Z" />
|
<path d="M12 2 4.58 19.93c-.34.83.51 1.63 1.31 1.27L12 18.47l6.11 2.73c.8.36 1.65-.44 1.31-1.27L12 2Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<small id="cockpit-route-direction-label">{{ direction.label }}</small>
|
<small id="cockpit-route-direction-label">{{ direction.label }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-visor-status cockpit-visor-status-left" data-i18n>OPTICAL PLANE · 01</div>
|
<div class="cockpit-visor-status cockpit-visor-status-left">{{ t('OPTICAL PLANE · 01') }}</div>
|
||||||
<div class="cockpit-visor-status cockpit-visor-status-right" data-i18n>VISOR LOCK · ACTIVE</div>
|
<div class="cockpit-visor-status cockpit-visor-status-right">{{ t('VISOR LOCK · ACTIVE') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-speed-rim" class="cockpit-altitude-rim cockpit-speed-rim" v-class-toggle="{ unavailable: hud.speedRim.unavailable }">
|
<div id="cockpit-speed-rim" class="cockpit-altitude-rim cockpit-speed-rim" v-class-toggle="{ unavailable: hud.speedRim.unavailable }">
|
||||||
<span class="cockpit-altitude-rim-label" data-i18n>GROUND SPEED · KTS</span>
|
<span class="cockpit-altitude-rim-label">{{ t('GROUND SPEED · KTS') }}</span>
|
||||||
<div class="cockpit-altitude-rim-rail"></div>
|
<div class="cockpit-altitude-rim-rail"></div>
|
||||||
<div class="cockpit-altitude-rim-scale">
|
<div class="cockpit-altitude-rim-scale">
|
||||||
<span v-for="(tick, index) in hud.speedRim.ticks" :key="index" data-speed-rim-tick :hidden="tick.hidden" :class="{ major: tick.major }" :style="tickStyle(tick)"><b>{{ tick.label }}</b><i></i></span>
|
<span v-for="(tick, index) in hud.speedRim.ticks" :key="index" data-speed-rim-tick :hidden="tick.hidden" :class="{ major: tick.major }" :style="tickStyle(tick)"><b>{{ tick.label }}</b><i></i></span>
|
||||||
@@ -140,7 +140,7 @@ const tickStyle = (tick) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-altitude-rim" class="cockpit-altitude-rim" v-class-toggle="{ unavailable: hud.altitudeRim.unavailable }">
|
<div id="cockpit-altitude-rim" class="cockpit-altitude-rim" v-class-toggle="{ unavailable: hud.altitudeRim.unavailable }">
|
||||||
<span class="cockpit-altitude-rim-label" data-i18n>ALTITUDE · FT</span>
|
<span class="cockpit-altitude-rim-label">{{ t('ALTITUDE · FT') }}</span>
|
||||||
<div class="cockpit-altitude-rim-rail"></div>
|
<div class="cockpit-altitude-rim-rail"></div>
|
||||||
<div class="cockpit-altitude-rim-scale">
|
<div class="cockpit-altitude-rim-scale">
|
||||||
<span v-for="(tick, index) in hud.altitudeRim.ticks" :key="index" data-altitude-rim-tick :hidden="tick.hidden" :class="{ major: tick.major }" :style="tickStyle(tick)"><b>{{ tick.label }}</b><i></i></span>
|
<span v-for="(tick, index) in hud.altitudeRim.ticks" :key="index" data-altitude-rim-tick :hidden="tick.hidden" :class="{ major: tick.major }" :style="tickStyle(tick)"><b>{{ tick.label }}</b><i></i></span>
|
||||||
@@ -152,38 +152,38 @@ const tickStyle = (tick) =>
|
|||||||
</div>
|
</div>
|
||||||
<div class="cockpit-topline">
|
<div class="cockpit-topline">
|
||||||
<div>
|
<div>
|
||||||
<span class="cockpit-kicker" data-i18n>FIRST PERSON</span>
|
<span class="cockpit-kicker">{{ t('FIRST PERSON') }}</span>
|
||||||
<strong id="cockpit-callsign">{{ hud.callsign }}</strong>
|
<strong id="cockpit-callsign">{{ hud.callsign }}</strong>
|
||||||
<span id="cockpit-aircraft-meta" v-class-toggle="{ emergency: hud.meta.emergency }" class="cockpit-aircraft-meta">{{ hud.meta.text }}</span>
|
<span id="cockpit-aircraft-meta" v-class-toggle="{ emergency: hud.meta.emergency }" class="cockpit-aircraft-meta">{{ hud.meta.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-vision-controls" role="group" aria-label="Cockpit vision style" data-i18n-aria-label>
|
<div class="cockpit-vision-controls" role="group" :aria-label="t('Cockpit vision style')">
|
||||||
<button id="cockpit-vision-previous" class="cockpit-vision-nav" type="button" aria-label="Previous cockpit vision style" title="Previous vision style" data-i18n-aria-label data-i18n-title>
|
<button id="cockpit-vision-previous" class="cockpit-vision-nav" type="button" :aria-label="t('Previous cockpit vision style')" :title="t('Previous vision style')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
||||||
<small data-i18n>PREV</small>
|
<small>{{ t('PREV') }}</small>
|
||||||
</button>
|
</button>
|
||||||
<button id="cockpit-vision-current" class="cockpit-vision-current" type="button" :data-cockpit-vision="v.mode" :aria-label="visionLabel" :title="visionTitle">
|
<button id="cockpit-vision-current" class="cockpit-vision-current" type="button" :data-cockpit-vision="v.mode" :aria-label="visionLabel" :title="visionTitle">
|
||||||
<small data-i18n>CURRENT</small>
|
<small>{{ t('CURRENT') }}</small>
|
||||||
<strong id="cockpit-vision-current-label" aria-live="polite">{{ v.label }}</strong>
|
<strong id="cockpit-vision-current-label" aria-live="polite">{{ v.label }}</strong>
|
||||||
</button>
|
</button>
|
||||||
<button id="cockpit-vision-next" class="cockpit-vision-nav" type="button" aria-label="Next cockpit vision style" title="Next vision style" data-i18n-aria-label data-i18n-title>
|
<button id="cockpit-vision-next" class="cockpit-vision-nav" type="button" :aria-label="t('Next cockpit vision style')" :title="t('Next vision style')">
|
||||||
<small data-i18n>NEXT</small>
|
<small>{{ t('NEXT') }}</small>
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_right</span>
|
<span class="material-symbols-outlined" aria-hidden="true">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-reticle material-symbols-outlined" aria-hidden="true">my_location</div>
|
<div class="cockpit-reticle material-symbols-outlined" aria-hidden="true">my_location</div>
|
||||||
<div class="cockpit-compass" aria-label="Current aircraft heading" data-i18n-aria-label>
|
<div class="cockpit-compass" :aria-label="t('Current aircraft heading')">
|
||||||
<div class="cockpit-instrument-row">
|
<div class="cockpit-instrument-row">
|
||||||
<div class="cockpit-readout cockpit-speed">
|
<div class="cockpit-readout cockpit-speed">
|
||||||
<span class="cockpit-readout-label" data-i18n>GROUND SPEED</span>
|
<span class="cockpit-readout-label">{{ t('GROUND SPEED') }}</span>
|
||||||
<CockpitRollingValue id="cockpit-speed-value" tag="strong" :reading="hud.speed" placeholder="---" />
|
<CockpitRollingValue id="cockpit-speed-value" tag="strong" :reading="hud.speed" placeholder="---" />
|
||||||
<span class="cockpit-readout-unit" data-i18n>KTS</span>
|
<span class="cockpit-readout-unit">{{ t('KTS') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-heading-value"><CockpitRollingValue id="cockpit-heading-value" :reading="hud.heading" placeholder="---" :circular-range="360" /><small>°</small></div>
|
<div class="cockpit-heading-value"><CockpitRollingValue id="cockpit-heading-value" :reading="hud.heading" placeholder="---" :circular-range="360" /><small>°</small></div>
|
||||||
<div class="cockpit-readout cockpit-altitude">
|
<div class="cockpit-readout cockpit-altitude">
|
||||||
<span class="cockpit-readout-label" data-i18n>ALTITUDE</span>
|
<span class="cockpit-readout-label">{{ t('ALTITUDE') }}</span>
|
||||||
<CockpitRollingValue id="cockpit-altitude-value" tag="strong" :reading="hud.altitude" placeholder="-----" />
|
<CockpitRollingValue id="cockpit-altitude-value" tag="strong" :reading="hud.altitude" placeholder="-----" />
|
||||||
<span class="cockpit-readout-unit" data-i18n>FT</span>
|
<span class="cockpit-readout-unit">{{ t('FT') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="cockpit-heading-caret material-symbols-outlined" aria-hidden="true">arrow_drop_down</span>
|
<span class="cockpit-heading-caret material-symbols-outlined" aria-hidden="true">arrow_drop_down</span>
|
||||||
@@ -196,17 +196,17 @@ const tickStyle = (tick) =>
|
|||||||
<CockpitContext />
|
<CockpitContext />
|
||||||
<CockpitBriefing />
|
<CockpitBriefing />
|
||||||
<CockpitUtilityControls />
|
<CockpitUtilityControls />
|
||||||
<div class="cockpit-help"><bdi data-i18n>ESC EXIT</bdi> <span>·</span> <bdi data-i18n>C TOGGLE</bdi></div>
|
<div class="cockpit-help"><bdi>{{ t('ESC EXIT') }}</bdi> <span>·</span> <bdi>{{ t('C TOGGLE') }}</bdi></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav id="view-switcher" aria-label="View switcher" data-i18n-aria-label>
|
<nav id="view-switcher" :aria-label="t('View switcher')">
|
||||||
<button id="cockpit-reset-globe" type="button" :aria-label="resetBusy ? t('Resetting cockpit to full globe view') : t('Reset cockpit to full globe view')" title="Exit cockpit and return to full globe view" :hidden="!active" data-i18n-title @click="globe.act('reset')">
|
<button id="cockpit-reset-globe" type="button" :aria-label="resetBusy ? t('Resetting cockpit to full globe view') : t('Reset cockpit to full globe view')" :title="t('Exit cockpit and return to full globe view')" :hidden="!active" @click="globe.act('reset')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||||
<span data-i18n>RESET</span>
|
<span>{{ t('RESET') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="map-view-switch" type="button" aria-label="Exit cockpit view" aria-keyshortcuts="Escape C" title="Exit cockpit view" :hidden="!active" data-i18n-aria-label data-i18n-title>
|
<button id="map-view-switch" type="button" :aria-label="t('Exit cockpit view')" aria-keyshortcuts="Escape C" :title="t('Exit cockpit view')" :hidden="!active">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">close_fullscreen</span>
|
<span class="material-symbols-outlined" aria-hidden="true">close_fullscreen</span>
|
||||||
<span data-i18n>EXIT COCKPIT</span>
|
<span>{{ t('EXIT COCKPIT') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
@@ -66,7 +66,7 @@ function selectMapSource(chip) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Unified bottom command dock: locations, voice, visual presets -->
|
<!-- Unified bottom command dock: locations, voice, visual presets -->
|
||||||
<div id="command-dock" class="ui-scroll" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
|
<div id="command-dock" class="ui-scroll" :aria-label="t('Navigation, voice, and visual preset controls')">
|
||||||
<template v-for="slot in dockOrder" :key="slot">
|
<template v-for="slot in dockOrder" :key="slot">
|
||||||
<!-- Control panel -->
|
<!-- Control panel -->
|
||||||
<div v-if="slot === 'control'" id="control-panel" v-panel-state="'control-panel'" class="panel-collapsible collapsed">
|
<div v-if="slot === 'control'" id="control-panel" v-panel-state="'control-panel'" class="panel-collapsible collapsed">
|
||||||
@@ -75,53 +75,53 @@ function selectMapSource(chip) {
|
|||||||
<button id="control-panel-toggle" class="panel-header dock-tray-toggle" type="button"
|
<button id="control-panel-toggle" class="panel-header dock-tray-toggle" type="button"
|
||||||
data-dock-toggle-target="control-panel" :aria-expanded="String(panels['control-panel']?.collapsed === false)"
|
data-dock-toggle-target="control-panel" :aria-expanded="String(panels['control-panel']?.collapsed === false)"
|
||||||
aria-controls="control-panel-popover" :aria-label="trayWords('control-panel', t('VISUAL PRESETS'), t('Expand Visual Presets'))" :title="trayWords('control-panel', t('VISUAL PRESETS'), undefined)">
|
aria-controls="control-panel-popover" :aria-label="trayWords('control-panel', t('VISUAL PRESETS'), t('Expand Visual Presets'))" :title="trayWords('control-panel', t('VISUAL PRESETS'), undefined)">
|
||||||
<span class="panel-title" data-i18n-runtime><span class="dock-label-icon dock-label-icon-presets" aria-hidden="true"></span>VISUAL PRESETS</span>
|
<span class="panel-title"><span class="dock-label-icon dock-label-icon-presets" aria-hidden="true"></span>{{ t('VISUAL PRESETS') }}</span>
|
||||||
<span class="panel-divider"></span>
|
<span class="panel-divider"></span>
|
||||||
</button>
|
</button>
|
||||||
<div id="control-panel-popover" class="dock-popover-content">
|
<div id="control-panel-popover" class="dock-popover-content">
|
||||||
<UiButton variant="icon" class="dock-pin-btn" data-pin-target="control-panel" :label="t('Pin visual presets')" :title="t('Keep visual presets open')" :pressed="!!panels['control-panel']?.pinned" @click="pin($event, 'control-panel')"><img src="/pin.svg" alt="" /></UiButton>
|
<UiButton variant="icon" class="dock-pin-btn" data-pin-target="control-panel" :label="t('Pin visual presets')" :title="t('Keep visual presets open')" :pressed="!!panels['control-panel']?.pinned" @click="pin($event, 'control-panel')"><img src="/pin.svg" alt="" /></UiButton>
|
||||||
<div id="style-buttons" class="button-grid">
|
<div id="style-buttons" class="button-grid">
|
||||||
<button class="style-btn active" data-style="normal" @click="display.act('setStyle', 'normal')" v-class-toggle="{ ...(style && { active: style.name === 'normal' }) }" title="Show the globe without a visual filter." data-i18n-title>
|
<button class="style-btn active" data-style="normal" @click="display.act('setStyle', 'normal')" v-class-toggle="{ ...(style && { active: style.name === 'normal' }) }" :title="t('Show the globe without a visual filter.')">
|
||||||
<span class="btn-icon">◯</span>
|
<span class="btn-icon">◯</span>
|
||||||
<span class="btn-label" data-i18n>Normal</span>
|
<span class="btn-label">{{ t('Normal') }}</span>
|
||||||
<span class="btn-key">1</span>
|
<span class="btn-key">1</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="style-btn" data-style="retro" @click="display.act('setStyle', 'retro')" v-class-toggle="{ ...(style && { active: style.name === 'retro' }) }" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title>
|
<button class="style-btn" data-style="retro" @click="display.act('setStyle', 'retro')" v-class-toggle="{ ...(style && { active: style.name === 'retro' }) }" :title="t('Emulate a green phosphor CRT with scanlines and screen curvature.')">
|
||||||
<span class="btn-icon">▦</span>
|
<span class="btn-icon">▦</span>
|
||||||
<span class="btn-label" data-i18n>CRT</span>
|
<span class="btn-label">{{ t('CRT') }}</span>
|
||||||
<span class="btn-key">2</span>
|
<span class="btn-key">2</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="style-btn" data-style="surveillance" @click="display.act('setStyle', 'surveillance')" v-class-toggle="{ ...(style && { active: style.name === 'surveillance' }) }" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title>
|
<button class="style-btn" data-style="surveillance" @click="display.act('setStyle', 'surveillance')" v-class-toggle="{ ...(style && { active: style.name === 'surveillance' }) }" :title="t('Simulate night-vision goggles with green intensification and a tube vignette.')">
|
||||||
<span class="btn-icon">🌙</span>
|
<span class="btn-icon">🌙</span>
|
||||||
<span class="btn-label" data-i18n>NVG</span>
|
<span class="btn-label">{{ t('NVG') }}</span>
|
||||||
<span class="btn-key">3</span>
|
<span class="btn-key">3</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="style-btn" data-style="thermal" @click="display.act('setStyle', 'thermal')" v-class-toggle="{ ...(style && { active: style.name === 'thermal' }) }" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title>
|
<button class="style-btn" data-style="thermal" @click="display.act('setStyle', 'thermal')" v-class-toggle="{ ...(style && { active: style.name === 'thermal' }) }" :title="t('Simulate FLIR-style thermal contrast. Turn up Ironbow for color.')">
|
||||||
<span class="btn-icon">🌡️</span>
|
<span class="btn-icon">🌡️</span>
|
||||||
<span class="btn-label" data-i18n>FLIR</span>
|
<span class="btn-label">{{ t('FLIR') }}</span>
|
||||||
<span class="btn-key">4</span>
|
<span class="btn-key">4</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="style-btn" data-style="anime" @click="display.act('setStyle', 'anime')" v-class-toggle="{ ...(style && { active: style.name === 'anime' }) }" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title>
|
<button class="style-btn" data-style="anime" @click="display.act('setStyle', 'anime')" v-class-toggle="{ ...(style && { active: style.name === 'anime' }) }" :title="t('Apply bright cel-shaded color and illustrated outlines.')">
|
||||||
<span class="btn-icon">✦</span>
|
<span class="btn-icon">✦</span>
|
||||||
<span class="btn-label" data-i18n>Anime</span>
|
<span class="btn-label">{{ t('Anime') }}</span>
|
||||||
<span class="btn-key">5</span>
|
<span class="btn-key">5</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="style-btn" data-style="noir" @click="display.act('setStyle', 'noir')" v-class-toggle="{ ...(style && { active: style.name === 'noir' }) }" title="Apply high-contrast monochrome film-noir grading." data-i18n-title>
|
<button class="style-btn" data-style="noir" @click="display.act('setStyle', 'noir')" v-class-toggle="{ ...(style && { active: style.name === 'noir' }) }" :title="t('Apply high-contrast monochrome film-noir grading.')">
|
||||||
<span class="btn-icon">◐</span>
|
<span class="btn-icon">◐</span>
|
||||||
<span class="btn-label" data-i18n>Noir</span>
|
<span class="btn-label">{{ t('Noir') }}</span>
|
||||||
<span class="btn-key">6</span>
|
<span class="btn-key">6</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<section class="map-source-section" aria-labelledby="map-source-label">
|
<section class="map-source-section" aria-labelledby="map-source-label">
|
||||||
<div class="map-source-heading">
|
<div class="map-source-heading">
|
||||||
<span data-i18n id="map-source-label">MAP SOURCE</span>
|
<span id="map-source-label">{{ t('MAP SOURCE') }}</span>
|
||||||
<span id="map-stack-status" class="pp-stack-status" v-class-toggle="{ warn: !!mapSource?.status.warn }" aria-live="polite">{{ mapSource ? mapSource.status.text : '3D' }}</span>
|
<span id="map-stack-status" class="pp-stack-status" v-class-toggle="{ warn: !!mapSource?.status.warn }" aria-live="polite">{{ mapSource ? mapSource.status.text : '3D' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source" data-i18n-aria-label><UiButton v-for="chip in mapSource?.chips" :key="chip.id" class="map-stack-chip" :pressed="Boolean(chip.active)" pressed-class="active" v-class-toggle="{ unavailable: !chip.available }" :data-stack-id="chip.id" :title="chip.title" :aria-disabled="String(!chip.available)" :aria-label="chip.ariaLabel" @click="selectMapSource(chip)"><span class="map-stack-chip-label">{{ chip.label }}</span><span v-if="chip.requirement" class="map-stack-chip-req">{{ chip.requirement }}</span></UiButton></div>
|
<div id="map-stack-chips" class="map-stack-chip-row" role="group" :aria-label="t('Map source')"><UiButton v-for="chip in mapSource?.chips" :key="chip.id" class="map-stack-chip" :pressed="Boolean(chip.active)" pressed-class="active" v-class-toggle="{ unavailable: !chip.available }" :data-stack-id="chip.id" :title="chip.title" :aria-disabled="String(!chip.available)" :aria-label="chip.ariaLabel" @click="selectMapSource(chip)"><span class="map-stack-chip-label">{{ chip.label }}</span><span v-if="chip.requirement" class="map-stack-chip-req">{{ chip.requirement }}</span></UiButton></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
|
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
|
||||||
<span class="panel-mini-label" data-i18n>Style</span>
|
<span class="panel-mini-label">{{ t('Style') }}</span>
|
||||||
<span id="style-mini-value" class="panel-mini-value">{{ style ? style.label : t('NORMAL') }}</span>
|
<span id="style-mini-value" class="panel-mini-value">{{ style ? style.label : t('NORMAL') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,9 +135,9 @@ function selectMapSource(chip) {
|
|||||||
<div class="location-inner">
|
<div class="location-inner">
|
||||||
<div class="location-toolbar">
|
<div class="location-toolbar">
|
||||||
<button id="location-bar-toggle" class="dock-tray-toggle" type="button" data-dock-toggle-target="location-bar" aria-controls="location-bar-popover" :aria-expanded="String(panels['location-bar']?.collapsed === false)" :aria-label="trayWords('location-bar', t('LOCATION'), t('Expand LOCATION'))" :title="trayWords('location-bar', t('LOCATION'), t('Expand LOCATION'))">
|
<button id="location-bar-toggle" class="dock-tray-toggle" type="button" data-dock-toggle-target="location-bar" aria-controls="location-bar-popover" :aria-expanded="String(panels['location-bar']?.collapsed === false)" :aria-label="trayWords('location-bar', t('LOCATION'), t('Expand LOCATION'))" :title="trayWords('location-bar', t('LOCATION'), t('Expand LOCATION'))">
|
||||||
<span class="location-toolbar-label" data-i18n-runtime><span class="dock-label-icon dock-label-icon-location" aria-hidden="true"></span>LOCATION</span>
|
<span class="location-toolbar-label"><span class="dock-label-icon dock-label-icon-location" aria-hidden="true"></span>{{ t('LOCATION') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="panel-collapse-btn" data-collapse-target="location-bar" title="Collapse panel" data-i18n-title @click="chrome.act('toggle', 'location-bar')">+</button>
|
<button class="panel-collapse-btn" data-collapse-target="location-bar" :title="t('Collapse panel')" @click="chrome.act('toggle', 'location-bar')">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="location-mini-status" class="location-mini-status" aria-live="polite">
|
<div id="location-mini-status" class="location-mini-status" aria-live="polite">
|
||||||
<div id="location-mini-city" class="location-mini-city">{{ loc ? loc.status.city : t('📍 Location: --') }}</div>
|
<div id="location-mini-city" class="location-mini-city">{{ loc ? loc.status.city : t('📍 Location: --') }}</div>
|
||||||
@@ -150,10 +150,10 @@ function selectMapSource(chip) {
|
|||||||
<div class="location-city-row">
|
<div class="location-city-row">
|
||||||
<div id="location-pills" class="location-pills"><UiButton v-for="city in loc?.cities" :key="city.id" class="location-pill" :pressed="Boolean(city.active)" pressed-class="active" :data-location-id="city.id" @click="locationControls?.chooseCity(city.id)">{{ city.label }}</UiButton></div>
|
<div id="location-pills" class="location-pills"><UiButton v-for="city in loc?.cities" :key="city.id" class="location-pill" :pressed="Boolean(city.active)" pressed-class="active" :data-location-id="city.id" @click="locationControls?.chooseCity(city.id)">{{ city.label }}</UiButton></div>
|
||||||
<div class="location-search-wrap">
|
<div class="location-search-wrap">
|
||||||
<button id="search-toggle" class="search-toggle-btn" title="Search any location" data-i18n-title @click="locationControls?.toggleSearch()">
|
<button id="search-toggle" class="search-toggle-btn" :title="t('Search any location')" @click="locationControls?.toggleSearch()">
|
||||||
<span>🔎</span>
|
<span>🔎</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="text" id="location-search" v-class-toggle="{ expanded: !!loc?.searchExpanded, searching: !!loc?.searching }" placeholder="Search any location..." autocomplete="off" spellcheck="false" aria-label="Search location by name or coordinates" :aria-invalid="loc?.searchError ? 'true' : null" :aria-describedby="loc?.searchError ? 'location-search-error' : null" data-i18n-aria-label data-i18n-placeholder />
|
<input type="text" id="location-search" v-class-toggle="{ expanded: !!loc?.searchExpanded, searching: !!loc?.searching }" :placeholder="t('Search any location...')" autocomplete="off" spellcheck="false" :aria-label="t('Search location by name or coordinates')" :aria-invalid="loc?.searchError ? 'true' : null" :aria-describedby="loc?.searchError ? 'location-search-error' : null" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="location-search-error" class="location-search-error" role="alert">{{ loc?.searchError }}</p>
|
<p id="location-search-error" class="location-search-error" role="alert">{{ loc?.searchError }}</p>
|
||||||
|
|||||||
@@ -30,37 +30,37 @@ const sceneRuntime = computed(() => scenePanel.value?.runtime ?? '');
|
|||||||
<!-- Global Context is deliberately independent from the left layer accordion. -->
|
<!-- Global Context is deliberately independent from the left layer accordion. -->
|
||||||
<aside id="right-context-rail" class="ui-scroll" v-class-toggle="{ 'bhote-event-active': !!bhotePanel }">
|
<aside id="right-context-rail" class="ui-scroll" v-class-toggle="{ 'bhote-event-active': !!bhotePanel }">
|
||||||
<RailPanel id="weather-panel" hidden :title="t('WEATHER')" sideways rail-scroller>
|
<RailPanel id="weather-panel" hidden :title="t('WEATHER')" sideways rail-scroller>
|
||||||
<template #badge><span id="weather-panel-count" aria-label="Active weather products" data-i18n-aria-label>0</span></template>
|
<template #badge><span id="weather-panel-count" :aria-label="t('Active weather products')">0</span></template>
|
||||||
<div id="weather-panel-body" class="ui-scroll" data-rail-scroller></div>
|
<div id="weather-panel-body" class="ui-scroll" data-rail-scroller></div>
|
||||||
</RailPanel>
|
</RailPanel>
|
||||||
<RailPanel id="recent-imagery-panel" hidden :title="t('RECENT IMAGERY')" sideways rail-scroller>
|
<RailPanel id="recent-imagery-panel" hidden :title="t('RECENT IMAGERY')" sideways rail-scroller>
|
||||||
<template #badge><span id="recent-imagery-panel-count" aria-label="Imagery days in the box" data-i18n-aria-label></span></template>
|
<template #badge><span id="recent-imagery-panel-count" :aria-label="t('Imagery days in the box')"></span></template>
|
||||||
<div id="recent-imagery-panel-body" class="ui-scroll" data-rail-scroller></div>
|
<div id="recent-imagery-panel-body" class="ui-scroll" data-rail-scroller></div>
|
||||||
</RailPanel>
|
</RailPanel>
|
||||||
<RailPanel id="global-context-panel" start-collapsed v-class-toggle="{ ...(ctx && { 'context-enabled': ctx.enabled }) }" :data-context-mode="ctx?.mode" :title="t('CONTEXT')" sideways scroll rail-scroller>
|
<RailPanel id="global-context-panel" start-collapsed v-class-toggle="{ ...(ctx && { 'context-enabled': ctx.enabled }) }" :data-context-mode="ctx?.mode" :title="t('CONTEXT')" sideways scroll rail-scroller>
|
||||||
<div class="cyber-panel-body ui-scroll" data-rail-scroller>
|
<div class="cyber-panel-body ui-scroll" data-rail-scroller>
|
||||||
<div class="global-context-modes" role="tablist" aria-label="Context mode" data-i18n-aria-label>
|
<div class="global-context-modes" role="tablist" :aria-label="t('Context mode')">
|
||||||
<button id="global-context-flights-btn" class="context-mode-button" v-class-toggle="{ active: ctx?.flightsActive }" type="button" role="tab" tabindex="0" :aria-selected="String(ctx?.flightsActive ?? false)" :aria-disabled="ctx && String(ctx.busy)" :aria-busy="ctx && String(ctx.busy)" aria-controls="context-flights-view" aria-label="CONTACTS" title="Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently." data-i18n-aria-label data-i18n-title>
|
<button id="global-context-flights-btn" class="context-mode-button" v-class-toggle="{ active: ctx?.flightsActive }" type="button" role="tab" tabindex="0" :aria-selected="String(ctx?.flightsActive ?? false)" :aria-disabled="ctx && String(ctx.busy)" :aria-busy="ctx && String(ctx.busy)" aria-controls="context-flights-view" :aria-label="t('CONTACTS')" :title="t('Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
|
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
|
||||||
<span data-i18n>CONTACTS</span>
|
<span>{{ t('CONTACTS') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="global-context-missions-btn" class="context-mode-button" v-class-toggle="{ active: ctx?.missionsActive }" type="button" role="tab" tabindex="0" :aria-selected="String(ctx?.missionsActive ?? false)" :aria-disabled="ctx && String(ctx.busy)" :aria-busy="ctx && String(ctx.busy)" aria-controls="context-missions-view">
|
<button id="global-context-missions-btn" class="context-mode-button" v-class-toggle="{ active: ctx?.missionsActive }" type="button" role="tab" tabindex="0" :aria-selected="String(ctx?.missionsActive ?? false)" :aria-disabled="ctx && String(ctx.busy)" :aria-busy="ctx && String(ctx.busy)" aria-controls="context-missions-view">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
||||||
<span data-i18n>SPACE MISSIONS</span>
|
<span>{{ t('SPACE MISSIONS') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="context-mode-standby" class="context-mode-standby" :hidden="ctx?.enabled">
|
<div id="context-mode-standby" class="context-mode-standby" :hidden="ctx?.enabled">
|
||||||
<strong data-i18n>SELECT CONTEXT</strong>
|
<strong>{{ t('SELECT CONTEXT') }}</strong>
|
||||||
<span data-i18n-runtime>CONTACTS — nearest planes · vessels · sites<br>SPACE MISSIONS — launches & orbital assets</span>
|
<span>{{ t('CONTACTS — nearest planes · vessels · sites') }}<br>{{ t('SPACE MISSIONS — launches & orbital assets') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<section id="context-flights-view" class="context-mode-view" role="tabpanel" aria-labelledby="global-context-flights-btn" :hidden="!ctx?.flightsActive">
|
<section id="context-flights-view" class="context-mode-view" role="tabpanel" aria-labelledby="global-context-flights-btn" :hidden="!ctx?.flightsActive">
|
||||||
<div class="global-context-actions" aria-label="Contact Context actions" data-i18n-aria-label>
|
<div class="global-context-actions" :aria-label="t('Contact Context actions')">
|
||||||
<button id="cockpit-entry" type="button" :hidden="!cockpit?.entryAvailable">
|
<button id="cockpit-entry" type="button" :hidden="!cockpit?.entryAvailable">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">flight</span>
|
<span class="material-symbols-outlined" aria-hidden="true">flight</span>
|
||||||
<span data-i18n>COCKPIT</span>
|
<span>{{ t('COCKPIT') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="installations-search-btn" class="panel-layer-toggle" type="button" :aria-disabled="ctx && String(ctx.installationsBusy)" :aria-busy="ctx && String(ctx.installationsBusy)" data-i18n>SEARCH NEARBY SITES</button>
|
<button id="installations-search-btn" class="panel-layer-toggle" type="button" :aria-disabled="ctx && String(ctx.installationsBusy)" :aria-busy="ctx && String(ctx.installationsBusy)">{{ t('SEARCH NEARBY SITES') }}</button>
|
||||||
<UiButton id="tr3b-toggle" class="tr3b-toggle" :pressed="!!cockpit?.tr3b?.converted" :hidden="!cockpit?.tr3b" aria-label="Reclassify tracked contact as TR-3B" :title="cockpit?.tr3b?.converted ? t('Restore real aircraft') : t('Reclassify as TR-3B')" data-i18n-aria-label>🛸</UiButton>
|
<UiButton id="tr3b-toggle" class="tr3b-toggle" :pressed="!!cockpit?.tr3b?.converted" :hidden="!cockpit?.tr3b" :aria-label="t('Reclassify tracked contact as TR-3B')" :title="cockpit?.tr3b?.converted ? t('Restore real aircraft') : t('Reclassify as TR-3B')">🛸</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<AwarenessPanel />
|
<AwarenessPanel />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { t } from '../../i18n/index.js';
|
||||||
import { useDisplayStore } from '../stores/display.js';
|
import { useDisplayStore } from '../stores/display.js';
|
||||||
import DisplayPanel from './panels/DisplayPanel.vue';
|
import DisplayPanel from './panels/DisplayPanel.vue';
|
||||||
|
|
||||||
@@ -7,5 +8,5 @@ const display = useDisplayStore();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DisplayPanel />
|
<DisplayPanel />
|
||||||
<button id="clean-view-exit" @click="display.act('exitCleanView')" title="Return UI controls" data-i18n-title data-i18n>EXIT CLEAN VIEW</button>
|
<button id="clean-view-exit" @click="display.act('exitCleanView')" :title="t('Return UI controls')">{{ t('EXIT CLEAN VIEW') }}</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,30 +51,30 @@ async function importChosen(event) {
|
|||||||
|
|
||||||
<!-- Scene Director Panel -->
|
<!-- Scene Director Panel -->
|
||||||
<RailPanel id="scene-panel" start-collapsed v-class-toggle="{ running: scene?.running, 'has-shots': scene?.hasShots }" :title="t('AUTHORING')" :hint="t('Create and edit scenes')" scroll>
|
<RailPanel id="scene-panel" start-collapsed v-class-toggle="{ running: scene?.running, 'has-shots': scene?.hasShots }" :title="t('AUTHORING')" :hint="t('Create and edit scenes')" scroll>
|
||||||
<p class="scene-step-heading" data-i18n>SCENE</p>
|
<p class="scene-step-heading">{{ t('SCENE') }}</p>
|
||||||
<div class="scene-controls">
|
<div class="scene-controls">
|
||||||
<UiSelect class="gev-field" id="scene-select" aria-label="Scene" data-i18n-aria-label :disabled="off('select')" @update:model-value="scenes.act('selectScene', $event)"><SceneOptions /></UiSelect>
|
<UiSelect class="gev-field" id="scene-select" :aria-label="t('Scene')" :disabled="off('select')" @update:model-value="scenes.act('selectScene', $event)"><SceneOptions /></UiSelect>
|
||||||
<UiButton id="scene-new-btn" type="button" :disabled="off('new')" @click="scenes.act('create')" class="gev-action-button scene-btn" title="Start a new, empty scene" data-i18n data-i18n-title>NEW SCENE</UiButton>
|
<UiButton id="scene-new-btn" type="button" :disabled="off('new')" @click="scenes.act('create')" class="gev-action-button scene-btn" :title="t('Start a new, empty scene')">{{ t('NEW SCENE') }}</UiButton>
|
||||||
<UiButton tone="alert" id="scene-delete-btn" type="button" :disabled="off('delete')" @click="scenes.act('deleteScene')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Delete this scene and all of its shots" data-i18n data-i18n-title>DELETE</UiButton>
|
<UiButton tone="alert" id="scene-delete-btn" type="button" :disabled="off('delete')" @click="scenes.act('deleteScene')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" :title="t('Delete this scene and all of its shots')">{{ t('DELETE') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<p class="scene-step-heading" data-i18n>SHOTS</p>
|
<p class="scene-step-heading">{{ t('SHOTS') }}</p>
|
||||||
<div class="scene-controls">
|
<div class="scene-controls">
|
||||||
<UiButton variant="primary" id="scene-capture-btn" type="button" :disabled="off('capture')" @click="scenes.act('capture')" class="gev-action-button scene-btn scene-btn-primary" title="Add the current view, style and layers as the next shot" data-i18n data-i18n-title>+ CAPTURE SHOT</UiButton>
|
<UiButton variant="primary" id="scene-capture-btn" type="button" :disabled="off('capture')" @click="scenes.act('capture')" class="gev-action-button scene-btn scene-btn-primary" :title="t('Add the current view, style and layers as the next shot')">{{ t('+ CAPTURE SHOT') }}</UiButton>
|
||||||
<UiButton id="scene-update-shot-btn" type="button" :disabled="off('update')" @click="scenes.act('update')" class="gev-action-button scene-btn" title="Replace the selected shot with the current view" data-i18n data-i18n-title>UPDATE SHOT</UiButton>
|
<UiButton id="scene-update-shot-btn" type="button" :disabled="off('update')" @click="scenes.act('update')" class="gev-action-button scene-btn" :title="t('Replace the selected shot with the current view')">{{ t('UPDATE SHOT') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<SceneShotList />
|
<SceneShotList />
|
||||||
<p class="scene-shot-hint" data-i18n>Click a shot to select it, double-click to rename.</p>
|
<p class="scene-shot-hint">{{ t('Click a shot to select it, double-click to rename.') }}</p>
|
||||||
<p class="scene-step-heading" data-i18n>PREVIEW</p>
|
<p class="scene-step-heading">{{ t('PREVIEW') }}</p>
|
||||||
<div class="scene-controls">
|
<div class="scene-controls">
|
||||||
<UiButton variant="primary" id="scene-start-btn" type="button" class="gev-action-button scene-btn scene-btn-primary" title="Play from this scene through the rest of the project" data-i18n-title :disabled="off('start')" @click="scenes.act('start')">{{ scene?.playLabel ?? t('PLAY', { context: 'scene' }) }}</UiButton>
|
<UiButton variant="primary" id="scene-start-btn" type="button" class="gev-action-button scene-btn scene-btn-primary" :title="t('Play from this scene through the rest of the project')" :disabled="off('start')" @click="scenes.act('start')">{{ scene?.playLabel ?? t('PLAY', { context: 'scene' }) }}</UiButton>
|
||||||
<UiButton tone="alert" id="scene-stop-btn" type="button" :disabled="off('stop')" @click="scenes.act('stop')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Stop playback (Esc)" data-i18n data-i18n-title>STOP</UiButton>
|
<UiButton tone="alert" id="scene-stop-btn" type="button" :disabled="off('stop')" @click="scenes.act('stop')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" :title="t('Stop playback (Esc)')">{{ t('STOP') }}</UiButton>
|
||||||
<UiButton id="scene-next-btn" type="button" :disabled="off('next')" @click="scenes.act('next')" class="gev-action-button scene-btn" title="Play the shot after the selected one, moving on to the next scene when needed" data-i18n data-i18n-title>NEXT</UiButton>
|
<UiButton id="scene-next-btn" type="button" :disabled="off('next')" @click="scenes.act('next')" class="gev-action-button scene-btn" :title="t('Play the shot after the selected one, moving on to the next scene when needed')">{{ t('NEXT') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<p class="scene-step-heading" data-i18n>SAVE & SHARE</p>
|
<p class="scene-step-heading">{{ t('SAVE & SHARE') }}</p>
|
||||||
<div class="scene-controls scene-file-controls">
|
<div class="scene-controls scene-file-controls">
|
||||||
<UiButton id="scene-export-btn" type="button" :disabled="off('export')" @click="scenes.act('export')" class="gev-action-button scene-btn" title="Download every scene as a JSON file you can import later" data-i18n data-i18n-title>EXPORT</UiButton>
|
<UiButton id="scene-export-btn" type="button" :disabled="off('export')" @click="scenes.act('export')" class="gev-action-button scene-btn" :title="t('Download every scene as a JSON file you can import later')">{{ t('EXPORT') }}</UiButton>
|
||||||
<UiButton id="scene-import-btn" type="button" :disabled="off('import')" @click="chooseImport($refs.sceneFile)" class="gev-action-button scene-btn" title="Load scenes from an exported JSON file" data-i18n data-i18n-title>IMPORT</UiButton>
|
<UiButton id="scene-import-btn" type="button" :disabled="off('import')" @click="chooseImport($refs.sceneFile)" class="gev-action-button scene-btn" :title="t('Load scenes from an exported JSON file')">{{ t('IMPORT') }}</UiButton>
|
||||||
<UiButton id="scene-download-btn" type="button" :disabled="off('download')" @click="scenes.act('download')" class="gev-action-button scene-btn" title="Download the timing log of the last play" data-i18n data-i18n-title>RUN LOG</UiButton>
|
<UiButton id="scene-download-btn" type="button" :disabled="off('download')" @click="scenes.act('download')" class="gev-action-button scene-btn" :title="t('Download the timing log of the last play')">{{ t('RUN LOG') }}</UiButton>
|
||||||
<input ref="sceneFile" type="file" id="scene-import-file" accept="application/json,.json" hidden @change="importChosen" />
|
<input ref="sceneFile" type="file" id="scene-import-file" accept="application/json,.json" hidden @change="importChosen" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="toolbar" data-director-authoring class="scene-controls">
|
<div v-if="toolbar" data-director-authoring class="scene-controls">
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ const removeTitle = (title) =>
|
|||||||
<aside v-if="available" id="key-setup" role="dialog" aria-labelledby="key-setup-title" aria-describedby="key-setup-description" :hidden="dialog.hidden" v-class-toggle="{ visible: dialog.visible }" :data-ready="view?.ready ? 'true' : undefined" @transitionend="controls?.transitionEnd()">
|
<aside v-if="available" id="key-setup" role="dialog" aria-labelledby="key-setup-title" aria-describedby="key-setup-description" :hidden="dialog.hidden" v-class-toggle="{ visible: dialog.visible }" :data-ready="view?.ready ? 'true' : undefined" @transitionend="controls?.transitionEnd()">
|
||||||
<div class="key-setup-scanline" aria-hidden="true"></div>
|
<div class="key-setup-scanline" aria-hidden="true"></div>
|
||||||
<header class="key-setup-header">
|
<header class="key-setup-header">
|
||||||
<span class="key-setup-kicker" data-i18n>GROUND STATION · PROVIDER SETTINGS</span>
|
<span class="key-setup-kicker">{{ t('GROUND STATION · PROVIDER SETTINGS') }}</span>
|
||||||
<button type="button" class="key-setup-close" data-key-setup-close aria-label="Close key setup" data-i18n-aria-label @click="controls?.close()">
|
<button type="button" class="key-setup-close" data-key-setup-close :aria-label="t('Close key setup')" @click="controls?.close()">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<h2 id="key-setup-title" data-i18n>Power up the globe</h2>
|
<h2 id="key-setup-title">{{ t('Power up the globe') }}</h2>
|
||||||
<p id="key-setup-description" data-i18n>The globe already flies keyless. Every key below switches on another real feed — paste one and it's saved into this app's local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.</p>
|
<p id="key-setup-description">{{ t('The globe already flies keyless. Every key below switches on another real feed — paste one and it\'s saved into this app\'s local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.') }}</p>
|
||||||
<div class="key-setup-rows ui-scroll" data-key-setup-rows>
|
<div class="key-setup-rows ui-scroll" data-key-setup-rows>
|
||||||
<section v-for="key in view?.rows ?? []" :key="`${key.id}:${view.generation}`" class="key-setup-row" :data-key-id="key.id" :data-set="String(key.set)" :data-managed="key.managed || undefined">
|
<section v-for="key in view?.rows ?? []" :key="`${key.id}:${view.generation}`" class="key-setup-row" :data-key-id="key.id" :data-set="String(key.set)" :data-managed="key.managed || undefined">
|
||||||
<div class="key-setup-row-head">
|
<div class="key-setup-row-head">
|
||||||
@@ -95,8 +95,8 @@ const removeTitle = (title) =>
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="key-setup-footer">
|
<div class="key-setup-footer">
|
||||||
<button type="button" class="key-setup-apply" data-key-setup-apply :aria-disabled="view?.saving == null ? undefined : String(view.saving)" data-i18n @click="controls?.apply()">SAVE KEYS</button>
|
<button type="button" class="key-setup-apply" data-key-setup-apply :aria-disabled="view?.saving == null ? undefined : String(view.saving)" @click="controls?.apply()">{{ t('SAVE KEYS') }}</button>
|
||||||
<span class="key-setup-hint" data-i18n>ESC to close</span>
|
<span class="key-setup-hint">{{ t('ESC to close') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="key-setup-note" data-key-setup-status role="status" aria-live="polite" v-status-text="statusText"></p>
|
<p class="key-setup-note" data-key-setup-status role="status" aria-live="polite" v-status-text="statusText"></p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const vToastText = {
|
|||||||
<div id="world-overlay-root" aria-hidden="true">
|
<div id="world-overlay-root" aria-hidden="true">
|
||||||
<canvas id="world-overlay-canvas"></canvas>
|
<canvas id="world-overlay-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div id="world-overlay-actions" class="world-overlay-accessibility" role="region" aria-label="Visible map targets" data-i18n-aria-label>
|
<div id="world-overlay-actions" class="world-overlay-accessibility" role="region" :aria-label="t('Visible map targets')">
|
||||||
<div id="world-overlay-action-list"><UiButton v-for="item in targets?.items ?? []" :key="item.key" :aria-label="item.label" :pressed="Boolean(item.selected)" :data-overlay-action-key="item.key" :onClick="onOverlayAction(targets.owner, item.key)">{{ item.label }}</UiButton></div>
|
<div id="world-overlay-action-list"><UiButton v-for="item in targets?.items ?? []" :key="item.key" :aria-label="item.label" :pressed="Boolean(item.selected)" :data-overlay-action-key="item.key" :onClick="onOverlayAction(targets.owner, item.key)">{{ item.label }}</UiButton></div>
|
||||||
<div id="world-overlay-status" v-overlay-status="targets" role="status" aria-live="polite" aria-atomic="true"></div>
|
<div id="world-overlay-status" v-overlay-status="targets" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,13 +72,13 @@ const vToastText = {
|
|||||||
<div id="title-bar">
|
<div id="title-bar">
|
||||||
<div class="title-glow"></div>
|
<div class="title-glow"></div>
|
||||||
<h1><span class="title-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span> <span translate="no">GOD'S EYE <span class="title-accent">VIEW</span></span></h1>
|
<h1><span class="title-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span> <span translate="no">GOD'S EYE <span class="title-accent">VIEW</span></span></h1>
|
||||||
<p class="subtitle scene-text-holdout" data-i18n>NO PLACE LEFT BEHIND</p>
|
<p class="subtitle scene-text-holdout">{{ t('NO PLACE LEFT BEHIND') }}</p>
|
||||||
<div v-if="fps" class="frame-rate-readout scene-text-holdout" :hidden="!fps.visible" :title="t('Rendered globe frames per second · toggle with `')">{{ fps.text }}</div>
|
<div v-if="fps" class="frame-rate-readout scene-text-holdout" :hidden="!fps.visible" :title="t('Rendered globe frames per second · toggle with `')">{{ fps.text }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Style indicator -->
|
<!-- Style indicator -->
|
||||||
<div id="style-indicator">
|
<div id="style-indicator">
|
||||||
<span class="indicator-label scene-text-holdout" data-i18n>ACTIVE STYLE</span>
|
<span class="indicator-label scene-text-holdout">{{ t('ACTIVE STYLE') }}</span>
|
||||||
<span class="indicator-value scene-text-holdout" id="active-style-name">{{ style ? style.label : t('NORMAL') }}</span>
|
<span class="indicator-value scene-text-holdout" id="active-style-name">{{ style ? style.label : t('NORMAL') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { msg } from '../../i18n/index.js';
|
import { msg, t } from '../../i18n/index.js';
|
||||||
import {
|
import {
|
||||||
FIRST_RUN_TIP,
|
FIRST_RUN_TIP,
|
||||||
environmentalLabel,
|
environmentalLabel,
|
||||||
@@ -32,23 +32,23 @@ function onSuppress(event) {
|
|||||||
<aside v-if="!launcher?.removed" id="first-run-launcher" role="dialog" aria-labelledby="first-run-title" aria-describedby="first-run-description" :class="{ visible: launcher?.visible }" :hidden="!launcher?.shown" :aria-hidden="launcher?.closing ? 'true' : undefined" :aria-busy="launcher?.busy ?? undefined" :data-state="launcher?.state ?? undefined">
|
<aside v-if="!launcher?.removed" id="first-run-launcher" role="dialog" aria-labelledby="first-run-title" aria-describedby="first-run-description" :class="{ visible: launcher?.visible }" :hidden="!launcher?.shown" :aria-hidden="launcher?.closing ? 'true' : undefined" :aria-busy="launcher?.busy ?? undefined" :data-state="launcher?.state ?? undefined">
|
||||||
<div class="first-run-scanline" aria-hidden="true"></div>
|
<div class="first-run-scanline" aria-hidden="true"></div>
|
||||||
<header class="first-run-header">
|
<header class="first-run-header">
|
||||||
<span class="first-run-kicker" data-i18n>MISSION CONTROL · FIRST LAUNCH</span>
|
<span class="first-run-kicker">{{ t('MISSION CONTROL · FIRST LAUNCH') }}</span>
|
||||||
</header>
|
</header>
|
||||||
<h2 id="first-run-title" data-i18n>Choose your first view</h2>
|
<h2 id="first-run-title">{{ t('Choose your first view') }}</h2>
|
||||||
<!-- The card's one persuasive line, owner-authored and VERBATIM (2026-08-23)
|
<!-- The card's one persuasive line, owner-authored and VERBATIM (2026-08-23)
|
||||||
— including the unspaced em dash. The arc it has to carry: this looks
|
— including the unspaced em dash. The arc it has to carry: this looks
|
||||||
like something you should not have access to → the sources are all
|
like something you should not have access to → the sources are all
|
||||||
public → the data is real. Tile subcopy stays purely functional. -->
|
public → the data is real. Tile subcopy stays purely functional. -->
|
||||||
<p id="first-run-description" data-i18n-runtime>{{ launcher?.description ?? DESCRIPTION }}</p>
|
<p id="first-run-description">{{ launcher?.description ?? DESCRIPTION }}</p>
|
||||||
<div class="first-run-choices ui-scroll" :data-scrollable="launcher?.scrollable ?? undefined">
|
<div class="first-run-choices ui-scroll" :data-scrollable="launcher?.scrollable ?? undefined">
|
||||||
<button type="button" data-first-run-choice="contacts" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('contacts')">
|
<button type="button" data-first-run-choice="contacts" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('contacts')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
|
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
|
||||||
<span><strong data-i18n>LIVE CONTACTS</strong><small data-i18n>Aircraft, vessels and nearby intelligence</small></span>
|
<span><strong>{{ t('LIVE CONTACTS') }}</strong><small>{{ t('Aircraft, vessels and nearby intelligence') }}</small></span>
|
||||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-first-run-choice="space-missions" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('space-missions')">
|
<button type="button" data-first-run-choice="space-missions" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('space-missions')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
||||||
<span><strong data-i18n>SPACE MISSIONS</strong><small data-i18n>Launches, spacecraft and orbital context</small></span>
|
<span><strong>{{ t('SPACE MISSIONS') }}</strong><small>{{ t('Launches, spacecraft and orbital context') }}</small></span>
|
||||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-first-run-choice="environmental" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('environmental')">
|
<button type="button" data-first-run-choice="environmental" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('environmental')">
|
||||||
@@ -57,26 +57,26 @@ function onSuppress(event) {
|
|||||||
<!-- Subcopy names BOTH feeds this tile turns on, because the launcher
|
<!-- Subcopy names BOTH feeds this tile turns on, because the launcher
|
||||||
optimizes for the fully configured experience (owner ruling,
|
optimizes for the fully configured experience (owner ruling,
|
||||||
2026-08-23) — see the mission table in src/firstRunExperience.js. -->
|
2026-08-23) — see the mission table in src/firstRunExperience.js. -->
|
||||||
<span data-i18n-runtime><strong data-first-run-environmental-title>{{ launcher?.environmentalTitle ?? environmentalLabel().title }}</strong><small>{{ launcher?.environmentalSub ?? ENVIRONMENTAL_SUB }}</small></span>
|
<span><strong data-first-run-environmental-title>{{ launcher?.environmentalTitle ?? environmentalLabel().title }}</strong><small>{{ launcher?.environmentalSub ?? ENVIRONMENTAL_SUB }}</small></span>
|
||||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="first-run-explore" data-first-run-choice="explore" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('explore')">
|
<button type="button" class="first-run-explore" data-first-run-choice="explore" :aria-disabled="launcher?.busy ?? undefined" @click="controls?.choose('explore')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||||
<span><strong data-i18n>EXPLORE MANUALLY</strong><small data-i18n>Begin with a clean globe</small></span>
|
<span><strong>{{ t('EXPLORE MANUALLY') }}</strong><small>{{ t('Begin with a clean globe') }}</small></span>
|
||||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="first-run-footer">
|
<div class="first-run-footer">
|
||||||
<label class="first-run-suppress">
|
<label class="first-run-suppress">
|
||||||
<input type="checkbox" data-first-run-suppress @change="onSuppress" />
|
<input type="checkbox" data-first-run-suppress @change="onSuppress" />
|
||||||
<span data-i18n>Don't show this again</span>
|
<span>{{ t('Don\'t show this again') }}</span>
|
||||||
</label>
|
</label>
|
||||||
<!-- A button, not just a hint: phones and tablets have no ESC key. It
|
<!-- A button, not just a hint: phones and tablets have no ESC key. It
|
||||||
closes exactly as ESC does, for this session only. Touch screens
|
closes exactly as ESC does, for this session only. Touch screens
|
||||||
see DISMISS; a keyboard keeps the ESC hint (first-run.css). -->
|
see DISMISS; a keyboard keeps the ESC hint (first-run.css). -->
|
||||||
<button type="button" class="first-run-dismiss" data-first-run-dismiss @click="controls?.dismiss()">
|
<button type="button" class="first-run-dismiss" data-first-run-dismiss @click="controls?.dismiss()">
|
||||||
<span class="first-run-dismiss-key" data-i18n>ESC to dismiss</span>
|
<span class="first-run-dismiss-key">{{ t('ESC to dismiss') }}</span>
|
||||||
<span class="first-run-dismiss-touch" data-i18n>DISMISS</span>
|
<span class="first-run-dismiss-touch">{{ t('DISMISS') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Kept out of the footer row: it swaps to mission progress and errors, and
|
<!-- Kept out of the footer row: it swaps to mission progress and errors, and
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const frameData = computed(() =>
|
|||||||
<RailPanel id="cctv-panel" start-collapsed :title="t('CCTV')" sideways scroll rail-scroller>
|
<RailPanel id="cctv-panel" start-collapsed :title="t('CCTV')" sideways scroll rail-scroller>
|
||||||
<div class="cyber-panel-body ui-scroll" data-rail-scroller>
|
<div class="cyber-panel-body ui-scroll" data-rail-scroller>
|
||||||
<div id="cctv-frame-wrap" :class="['cctv-frame-wrap', { loading: frame?.wrapLoading, 'has-frame': frame?.hasFrame }]">
|
<div id="cctv-frame-wrap" :class="['cctv-frame-wrap', { loading: frame?.wrapLoading, 'has-frame': frame?.hasFrame }]">
|
||||||
<span class="cctv-frame-caption" aria-hidden="true" data-i18n>ACQUIRING FRAME</span>
|
<span class="cctv-frame-caption" aria-hidden="true">{{ t('ACQUIRING FRAME') }}</span>
|
||||||
<img id="cctv-frame" alt="CCTV feed frame" :class="{ active: frame?.hasFrame }" :src="frame?.src || undefined" :hidden="live" v-bind="frameData" />
|
<img id="cctv-frame" alt="CCTV feed frame" :class="{ active: frame?.hasFrame }" :src="frame?.src || undefined" :hidden="live" v-bind="frameData" />
|
||||||
<canvas id="cctv-video" class="cctv-frame" :hidden="!live"></canvas>
|
<canvas id="cctv-video" class="cctv-frame" :hidden="!live"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,15 +40,15 @@ const frameData = computed(() =>
|
|||||||
<div id="cctv-meta">{{ view?.meta ?? t('Enable CCTV to load camera intersections') }}</div>
|
<div id="cctv-meta">{{ view?.meta ?? t('Enable CCTV to load camera intersections') }}</div>
|
||||||
<div class="cctv-controls">
|
<div class="cctv-controls">
|
||||||
<UiButton class="scene-btn" :pressed="Boolean(view?.enabled)" pressed-class="active" id="cctv-enable-btn">{{ view?.enabled ? t('CCTV ON') : t('CCTV OFF') }}</UiButton>
|
<UiButton class="scene-btn" :pressed="Boolean(view?.enabled)" pressed-class="active" id="cctv-enable-btn">{{ view?.enabled ? t('CCTV ON') : t('CCTV OFF') }}</UiButton>
|
||||||
<UiButton id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEAREST</UiButton>
|
<UiButton id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled">{{ t('NEAREST') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="cctv-controls">
|
<div class="cctv-controls">
|
||||||
<UiButton id="cctv-prev-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>PREV</UiButton>
|
<UiButton id="cctv-prev-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled">{{ t('PREV') }}</UiButton>
|
||||||
<CctvPicker class="gev-field" id="cctv-camera-select" aria-label="CCTV camera" data-i18n-aria-label />
|
<CctvPicker class="gev-field" id="cctv-camera-select" :aria-label="t('CCTV camera')" />
|
||||||
<UiButton id="cctv-next-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEXT</UiButton>
|
<UiButton id="cctv-next-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled">{{ t('NEXT') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="cctv-controls">
|
<div class="cctv-controls">
|
||||||
<UiButton id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.focusDisabled" data-i18n>FOCUS</UiButton>
|
<UiButton id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.focusDisabled">{{ t('FOCUS') }}</UiButton>
|
||||||
<UiButton class="scene-btn" :pressed="Boolean(view?.coverage.active)" pressed-class="active" id="cctv-coverage-btn" :disabled="view?.toggleDisabled">{{ view?.coverage.text ?? t('COVERAGE OFF') }}</UiButton>
|
<UiButton class="scene-btn" :pressed="Boolean(view?.coverage.active)" pressed-class="active" id="cctv-coverage-btn" :disabled="view?.toggleDisabled">{{ view?.coverage.text ?? t('COVERAGE OFF') }}</UiButton>
|
||||||
<UiButton class="scene-btn" :pressed="Boolean(view?.autoHop.active)" pressed-class="active" id="cctv-auto-hop-btn" :disabled="view?.toggleDisabled">{{ view?.autoHop.text ?? t('AUTO HOP OFF') }}</UiButton>
|
<UiButton class="scene-btn" :pressed="Boolean(view?.autoHop.active)" pressed-class="active" id="cctv-auto-hop-btn" :disabled="view?.toggleDisabled">{{ view?.autoHop.text ?? t('AUTO HOP OFF') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,24 +58,24 @@ const frameData = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
<div class="cctv-calibration-block">
|
<div class="cctv-calibration-block">
|
||||||
<div class="cctv-calibration-header">
|
<div class="cctv-calibration-header">
|
||||||
<div class="cctv-summary-label" data-i18n>CALIBRATION</div>
|
<div class="cctv-summary-label">{{ t('CALIBRATION') }}</div>
|
||||||
<UiButton class="scene-btn" :pressed="Boolean(view?.calibration.adjust.active)" pressed-class="active" id="cctv-adjust-btn" title="Drag the camera in the world: rings rotate, arrows move, handles set range/FOV" data-i18n-title :disabled="view?.calibration.disabled">{{ view?.calibration.adjust.text ?? t('ADJUST') }}</UiButton>
|
<UiButton class="scene-btn" :pressed="Boolean(view?.calibration.adjust.active)" pressed-class="active" id="cctv-adjust-btn" :title="t('Drag the camera in the world: rings rotate, arrows move, handles set range/FOV')" :disabled="view?.calibration.disabled">{{ view?.calibration.adjust.text ?? t('ADJUST') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<div id="cctv-cal-readout" class="cctv-cal-readout" aria-label="Camera pose — click a value to type" data-i18n-aria-label>
|
<div id="cctv-cal-readout" class="cctv-cal-readout" :aria-label="t('Camera pose — click a value to type')">
|
||||||
<CctvCalValue field="heading" label="HDG" title="Heading (compass °) — click to type" data-i18n-title />
|
<CctvCalValue field="heading" label="HDG" :title="t('Heading (compass °) — click to type')" />
|
||||||
<CctvCalValue field="pitch" label="PITCH" title="Pitch (° up/down) — click to type" data-i18n-title />
|
<CctvCalValue field="pitch" label="PITCH" :title="t('Pitch (° up/down) — click to type')" />
|
||||||
<CctvCalValue field="fov" label="FOV" title="Horizontal FOV (°) — click to type" data-i18n-title />
|
<CctvCalValue field="fov" label="FOV" :title="t('Horizontal FOV (°) — click to type')" />
|
||||||
<CctvCalValue field="range" label="RANGE" title="Range / monitor-plane distance (m) — click to type" data-i18n-title />
|
<CctvCalValue field="range" label="RANGE" :title="t('Range / monitor-plane distance (m) — click to type')" />
|
||||||
<CctvCalValue field="height" label="HGT" title="Mount height above ground (m) — click to type" data-i18n-title />
|
<CctvCalValue field="height" label="HGT" :title="t('Mount height above ground (m) — click to type')" />
|
||||||
<CctvCalValue field="north" label="ΔN" title="North offset from catalog position (m) — click to type" data-i18n-title />
|
<CctvCalValue field="north" label="ΔN" :title="t('North offset from catalog position (m) — click to type')" />
|
||||||
<CctvCalValue field="east" label="ΔE" title="East offset from catalog position (m) — click to type" data-i18n-title />
|
<CctvCalValue field="east" label="ΔE" :title="t('East offset from catalog position (m) — click to type')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="cctv-controls cctv-calibration-actions">
|
<div class="cctv-controls cctv-calibration-actions">
|
||||||
<UiButton id="cctv-calib-save-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>SAVE CAL</UiButton>
|
<UiButton id="cctv-calib-save-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled">{{ t('SAVE CAL') }}</UiButton>
|
||||||
<UiButton id="cctv-calib-reset-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>RESET CAL</UiButton>
|
<UiButton id="cctv-calib-reset-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled">{{ t('RESET CAL') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div>
|
<div class="cctv-summary-label">{{ t('SCENE SUMMARY') }}</div>
|
||||||
<div id="cctv-summary">{{ summary ?? t('Enable CCTV to start camera-linked intelligence summaries.') }}</div>
|
<div id="cctv-summary">{{ summary ?? t('Enable CCTV to start camera-linked intelligence summaries.') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</RailPanel>
|
</RailPanel>
|
||||||
|
|||||||
@@ -37,19 +37,19 @@ function leaveSignals() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside id="cockpit-signal-stream" class="cockpit-signal-window" aria-label="Cockpit briefing carousel" :hidden="!presence?.active" data-i18n-aria-label v-bind="streamState" ref="stream">
|
<aside id="cockpit-signal-stream" class="cockpit-signal-window" :aria-label="t('Cockpit briefing carousel')" :hidden="!presence?.active" v-bind="streamState" ref="stream">
|
||||||
<CockpitRoute />
|
<CockpitRoute />
|
||||||
<header class="cockpit-card-header cockpit-signal-header">
|
<header class="cockpit-card-header cockpit-signal-header">
|
||||||
<div class="cockpit-brief-heading">
|
<div class="cockpit-brief-heading">
|
||||||
<span id="cockpit-brief-kicker" class="cockpit-card-kicker cockpit-signal-kicker"><i aria-hidden="true"></i> {{ b.kicker }}</span>
|
<span id="cockpit-brief-kicker" class="cockpit-card-kicker cockpit-signal-kicker"><i aria-hidden="true"></i> {{ b.kicker }}</span>
|
||||||
<strong id="cockpit-brief-subtitle">{{ b.subtitle }}</strong>
|
<strong id="cockpit-brief-subtitle">{{ b.subtitle }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<nav class="cockpit-card-actions cockpit-brief-actions" aria-label="Cockpit briefing controls" data-i18n-aria-label>
|
<nav class="cockpit-card-actions cockpit-brief-actions" :aria-label="t('Cockpit briefing controls')">
|
||||||
<button id="cockpit-brief-previous" type="button" aria-label="Previous briefing page" title="Previous briefing page" data-i18n-aria-label data-i18n-title>
|
<button id="cockpit-brief-previous" type="button" :aria-label="t('Previous briefing page')" :title="t('Previous briefing page')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
<span id="cockpit-brief-position" aria-live="polite">{{ b.position }}</span>
|
<span id="cockpit-brief-position" aria-live="polite">{{ b.position }}</span>
|
||||||
<button id="cockpit-brief-next" type="button" aria-label="Next briefing page" title="Next briefing page" data-i18n-aria-label data-i18n-title>
|
<button id="cockpit-brief-next" type="button" :aria-label="t('Next briefing page')" :title="t('Next briefing page')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_right</span>
|
<span class="material-symbols-outlined" aria-hidden="true">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
<UiButton id="cockpit-brief-auto" :aria-label="b.autoLabel" :pressed="Boolean(b.autoRotate)" :title="b.autoHelp">{{ b.autoLabel }}</UiButton>
|
<UiButton id="cockpit-brief-auto" :aria-label="b.autoLabel" :pressed="Boolean(b.autoRotate)" :title="b.autoHelp">{{ b.autoLabel }}</UiButton>
|
||||||
@@ -59,33 +59,33 @@ function leaveSignals() {
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="cockpit-brief-stage">
|
<div class="cockpit-brief-stage">
|
||||||
<section data-cockpit-brief-page="signals" aria-label="Live signals" data-i18n-aria-label :hidden="b.pageId !== 'signals'">
|
<section data-cockpit-brief-page="signals" :aria-label="t('Live signals')" :hidden="b.pageId !== 'signals'">
|
||||||
<CockpitSignalList :leave="leaveSignals" />
|
<CockpitSignalList :leave="leaveSignals" />
|
||||||
</section>
|
</section>
|
||||||
<section data-cockpit-brief-page="news" aria-label="Latest regional news" data-i18n-aria-label :hidden="b.pageId !== 'news'">
|
<section data-cockpit-brief-page="news" :aria-label="t('Latest regional news')" :hidden="b.pageId !== 'news'">
|
||||||
<div id="cockpit-news-status" class="cockpit-brief-status" :hidden="b.news.statusHidden" :data-state="b.news.state ?? undefined">{{ b.news.status }}</div>
|
<div id="cockpit-news-status" class="cockpit-brief-status" :hidden="b.news.statusHidden" :data-state="b.news.state ?? undefined">{{ b.news.status }}</div>
|
||||||
<ol id="cockpit-news-list" class="cockpit-news-list" aria-live="polite"><li v-for="article in b.news.articles" :key="article.key"><a :href="article.url" target="_blank" rel="noopener noreferrer"><strong>{{ article.title }}</strong><span>{{ article.meta }}</span></a></li></ol>
|
<ol id="cockpit-news-list" class="cockpit-news-list" aria-live="polite"><li v-for="article in b.news.articles" :key="article.key"><a :href="article.url" target="_blank" rel="noopener noreferrer"><strong>{{ article.title }}</strong><span>{{ article.meta }}</span></a></li></ol>
|
||||||
</section>
|
</section>
|
||||||
<section data-cockpit-brief-page="local" aria-label="Location-based information" data-i18n-aria-label :hidden="b.pageId !== 'local'">
|
<section data-cockpit-brief-page="local" :aria-label="t('Location-based information')" :hidden="b.pageId !== 'local'">
|
||||||
<div class="cockpit-local-place">
|
<div class="cockpit-local-place">
|
||||||
<strong id="cockpit-local-place">{{ b.local.place }}</strong>
|
<strong id="cockpit-local-place">{{ b.local.place }}</strong>
|
||||||
<small id="cockpit-local-coordinates">{{ b.local.coordinates }}</small>
|
<small id="cockpit-local-coordinates">{{ b.local.coordinates }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-local-grid">
|
<div class="cockpit-local-grid">
|
||||||
<div><span data-i18n>TEMP</span><strong id="cockpit-local-temperature">{{ b.local.temperature }}</strong><small translate="no">OPEN-METEO</small></div>
|
<div><span>{{ t('TEMP') }}</span><strong id="cockpit-local-temperature">{{ b.local.temperature }}</strong><small translate="no">OPEN-METEO</small></div>
|
||||||
<div><span data-i18n>WIND</span><strong id="cockpit-local-wind">{{ b.local.wind }}</strong><small id="cockpit-local-wind-direction">{{ b.local.windDirection }}</small></div>
|
<div><span>{{ t('WIND') }}</span><strong id="cockpit-local-wind">{{ b.local.wind }}</strong><small id="cockpit-local-wind-direction">{{ b.local.windDirection }}</small></div>
|
||||||
<div><span data-i18n>SKY</span><strong id="cockpit-local-condition">{{ b.local.condition }}</strong><small id="cockpit-local-cloud">{{ b.local.cloud }}</small></div>
|
<div><span>{{ t('SKY') }}</span><strong id="cockpit-local-condition">{{ b.local.condition }}</strong><small id="cockpit-local-cloud">{{ b.local.cloud }}</small></div>
|
||||||
<div><span data-i18n>PRECIP</span><strong id="cockpit-local-precipitation">{{ b.local.precipitation }}</strong><small data-i18n>MM</small></div>
|
<div><span>{{ t('PRECIP') }}</span><strong id="cockpit-local-precipitation">{{ b.local.precipitation }}</strong><small>{{ t('MM') }}</small></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="cockpit-weather-credit" href="https://open-meteo.com/" target="_blank" rel="noopener" data-i18n>Weather data by Open-Meteo.com</a>
|
<a class="cockpit-weather-credit" href="https://open-meteo.com/" target="_blank" rel="noopener">{{ t('Weather data by Open-Meteo.com') }}</a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<footer class="cockpit-signal-footer">
|
<footer class="cockpit-signal-footer">
|
||||||
<span id="cockpit-brief-source">{{ b.source }}</span>
|
<span id="cockpit-brief-source">{{ b.source }}</span>
|
||||||
<nav class="cockpit-brief-tabs" aria-label="Cockpit briefing pages" data-i18n-aria-label>
|
<nav class="cockpit-brief-tabs" :aria-label="t('Cockpit briefing pages')">
|
||||||
<button type="button" data-cockpit-brief-index="0" aria-label="Show Live Signals" :aria-current="String(b.pageIndex === 0)" data-i18n-aria-label data-i18n>SIG</button>
|
<button type="button" data-cockpit-brief-index="0" :aria-label="t('Show Live Signals')" :aria-current="String(b.pageIndex === 0)">{{ t('SIG') }}</button>
|
||||||
<button type="button" data-cockpit-brief-index="1" aria-label="Show Regional News" :aria-current="brief ? String(b.pageIndex === 1) : undefined" data-i18n-aria-label data-i18n>NEWS</button>
|
<button type="button" data-cockpit-brief-index="1" :aria-label="t('Show Regional News')" :aria-current="brief ? String(b.pageIndex === 1) : undefined">{{ t('NEWS') }}</button>
|
||||||
<button type="button" data-cockpit-brief-index="2" aria-label="Show Local Info" :aria-current="brief ? String(b.pageIndex === 2) : undefined" data-i18n-aria-label data-i18n>LOCAL</button>
|
<button type="button" data-cockpit-brief-index="2" :aria-label="t('Show Local Info')" :aria-current="brief ? String(b.pageIndex === 2) : undefined">{{ t('LOCAL') }}</button>
|
||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -29,35 +29,35 @@ const weatherLabel = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside id="cockpit-context" class="cockpit-context-window" aria-label="Contact cockpit summary" :hidden="!presence?.context" data-i18n-aria-label :data-state="c.state ?? undefined" :data-collapsed="presence ? String(collapsed) : undefined">
|
<aside id="cockpit-context" class="cockpit-context-window" :aria-label="t('Contact cockpit summary')" :hidden="!presence?.context" :data-state="c.state ?? undefined" :data-collapsed="presence ? String(collapsed) : undefined">
|
||||||
<header class="cockpit-card-header cockpit-context-header">
|
<header class="cockpit-card-header cockpit-context-header">
|
||||||
<div>
|
<div>
|
||||||
<span data-i18n class="cockpit-card-kicker cockpit-context-kicker">CONTACT</span>
|
<span class="cockpit-card-kicker cockpit-context-kicker">{{ t('CONTACT') }}</span>
|
||||||
<strong id="cockpit-context-subject">{{ c.subject }}</strong>
|
<strong id="cockpit-context-subject">{{ c.subject }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<nav class="cockpit-card-actions cockpit-context-actions" aria-label="Contact navigation" data-i18n-aria-label>
|
<nav class="cockpit-card-actions cockpit-context-actions" :aria-label="t('Contact navigation')">
|
||||||
<button id="cockpit-context-previous" type="button" :disabled="!c.canPrevious" aria-label="Previous — prior visited contact in the 250 km window" title="Previous — prior visited contact in the 250 km window" data-i18n-aria-label data-i18n-title>
|
<button id="cockpit-context-previous" type="button" :disabled="!c.canPrevious" :aria-label="t('Previous — prior visited contact in the 250 km window')" :title="t('Previous — prior visited contact in the 250 km window')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">skip_previous</span>
|
<span class="material-symbols-outlined" aria-hidden="true">skip_previous</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="cockpit-context-next" type="button" :disabled="!c.canNext" aria-label="Next — nearest unvisited contact in the 250 km window" title="Next — nearest unvisited contact in the 250 km window" data-i18n-aria-label data-i18n-title>
|
<button id="cockpit-context-next" type="button" :disabled="!c.canNext" :aria-label="t('Next — nearest unvisited contact in the 250 km window')" :title="t('Next — nearest unvisited contact in the 250 km window')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">skip_next</span>
|
<span class="material-symbols-outlined" aria-hidden="true">skip_next</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="cockpit-context-toggle" type="button" :aria-label="collapsed ? t('Expand Contact panel') : t('Collapse Contact panel')" :aria-expanded="String(!collapsed)" :title="collapsed ? t('Expand contact panel') : t('Collapse contact panel')">
|
<button id="cockpit-context-toggle" type="button" :aria-label="collapsed ? t('Expand Contact panel') : t('Collapse Contact panel')" :aria-expanded="String(!collapsed)" :title="collapsed ? t('Expand contact panel') : t('Collapse contact panel')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">{{ collapsed ? 'chevron_right' : 'chevron_left' }}</span>
|
<span class="material-symbols-outlined" aria-hidden="true">{{ collapsed ? 'chevron_right' : 'chevron_left' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<span class="cockpit-context-qualifier" data-i18n>CONTEXT ONLY</span>
|
<span class="cockpit-context-qualifier">{{ t('CONTEXT ONLY') }}</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="cockpit-context-cohorts" role="list" aria-label="Nearby cohort counts" data-i18n-aria-label>
|
<div class="cockpit-context-cohorts" role="list" :aria-label="t('Nearby cohort counts')">
|
||||||
<div v-class-toggle="{ unknown: c.cohorts.flights.unknown }" role="listitem" data-context-cohort="flights"><span data-i18n>FLT</span><strong>{{ c.cohorts.flights.count }}</strong><small translate="no">OpenSky</small></div>
|
<div v-class-toggle="{ unknown: c.cohorts.flights.unknown }" role="listitem" data-context-cohort="flights"><span>{{ t('FLT') }}</span><strong>{{ c.cohorts.flights.count }}</strong><small translate="no">OpenSky</small></div>
|
||||||
<div v-class-toggle="{ unknown: c.cohorts.military.unknown }" role="listitem" data-context-cohort="military"><span data-i18n>MIL</span><strong>{{ c.cohorts.military.count }}</strong><small translate="no">adsb.lol</small></div>
|
<div v-class-toggle="{ unknown: c.cohorts.military.unknown }" role="listitem" data-context-cohort="military"><span>{{ t('MIL') }}</span><strong>{{ c.cohorts.military.count }}</strong><small translate="no">adsb.lol</small></div>
|
||||||
<div v-class-toggle="{ unknown: c.cohorts['ais-live-vessels'].unknown }" role="listitem" data-context-cohort="ais-live-vessels"><span translate="no">AIS</span><strong>{{ c.cohorts['ais-live-vessels'].count }}</strong><small translate="no">AISStream</small></div>
|
<div v-class-toggle="{ unknown: c.cohorts['ais-live-vessels'].unknown }" role="listitem" data-context-cohort="ais-live-vessels"><span translate="no">AIS</span><strong>{{ c.cohorts['ais-live-vessels'].count }}</strong><small translate="no">AISStream</small></div>
|
||||||
<div v-class-toggle="{ unknown: c.cohorts['military-installations'].unknown }" role="listitem" data-context-cohort="military-installations"><span data-i18n>SITE</span><strong>{{ c.cohorts['military-installations'].count }}</strong><small translate="no">OpenStreetMap</small></div>
|
<div v-class-toggle="{ unknown: c.cohorts['military-installations'].unknown }" role="listitem" data-context-cohort="military-installations"><span>{{ t('SITE') }}</span><strong>{{ c.cohorts['military-installations'].count }}</strong><small translate="no">OpenStreetMap</small></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-context-nearest">
|
<div class="cockpit-context-nearest">
|
||||||
<span id="cockpit-context-direction" v-class-toggle="{ unknown: c.direction.unknown }" class="material-symbols-outlined" :style="directionStyle" aria-hidden="true">navigation</span>
|
<span id="cockpit-context-direction" v-class-toggle="{ unknown: c.direction.unknown }" class="material-symbols-outlined" :style="directionStyle" aria-hidden="true">navigation</span>
|
||||||
<div>
|
<div>
|
||||||
<small data-i18n>NEAREST OBSERVED / MAPPED</small>
|
<small>{{ t('NEAREST OBSERVED / MAPPED') }}</small>
|
||||||
<strong id="cockpit-context-nearest-label" :aria-label="c.nearest.ariaLabel ?? undefined">{{ c.nearest.label }}</strong>
|
<strong id="cockpit-context-nearest-label" :aria-label="c.nearest.ariaLabel ?? undefined">{{ c.nearest.label }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<span id="cockpit-context-bearing">{{ c.bearing }}</span>
|
<span id="cockpit-context-bearing">{{ c.bearing }}</span>
|
||||||
@@ -66,7 +66,7 @@ const weatherLabel = computed(() =>
|
|||||||
<footer class="cockpit-context-footer">
|
<footer class="cockpit-context-footer">
|
||||||
<span id="cockpit-context-uncertainty">{{ c.uncertainty }}</span>
|
<span id="cockpit-context-uncertainty">{{ c.uncertainty }}</span>
|
||||||
<UiButton id="cockpit-weather-toggle" class="cockpit-context-weather" :aria-label="weatherLabel" :pressed="Boolean(c.weather)" :title="weatherLabel">
|
<UiButton id="cockpit-weather-toggle" class="cockpit-context-weather" :aria-label="weatherLabel" :pressed="Boolean(c.weather)" :title="weatherLabel">
|
||||||
<span data-i18n>WX</span>
|
<span>{{ t('WX') }}</span>
|
||||||
<strong id="cockpit-weather-state">{{ c.weather ? t('ON') : t('OFF') }}</strong>
|
<strong id="cockpit-weather-state">{{ c.weather ? t('ON') : t('OFF') }}</strong>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<time id="cockpit-context-updated">{{ c.updated }}</time>
|
<time id="cockpit-context-updated">{{ c.updated }}</time>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { t } from '../../../i18n/index.js';
|
||||||
import { createRouteState } from '../../cockpitInstrumentState.js';
|
import { createRouteState } from '../../cockpitInstrumentState.js';
|
||||||
import { useCockpitStore } from '../../stores/cockpit.js';
|
import { useCockpitStore } from '../../stores/cockpit.js';
|
||||||
|
|
||||||
@@ -13,19 +14,19 @@ const r = computed(() => route.value ?? initial);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section id="cockpit-route" class="cockpit-route-card" aria-label="Estimated flight plan" data-i18n-aria-label :hidden="!r.visible">
|
<section id="cockpit-route" class="cockpit-route-card" :aria-label="t('Estimated flight plan')" :hidden="!r.visible">
|
||||||
<div class="cockpit-route-heading">
|
<div class="cockpit-route-heading">
|
||||||
<span class="cockpit-route-kicker" data-i18n>ESTIMATED FLIGHT PLAN</span>
|
<span class="cockpit-route-kicker">{{ t('ESTIMATED FLIGHT PLAN') }}</span>
|
||||||
<small id="cockpit-route-status">{{ r.status }}</small>
|
<small id="cockpit-route-status">{{ r.status }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="cockpit-route-endpoints">
|
<div class="cockpit-route-endpoints">
|
||||||
<div class="cockpit-route-airport">
|
<div class="cockpit-route-airport">
|
||||||
<span data-i18n>FROM</span>
|
<span>{{ t('FROM') }}</span>
|
||||||
<strong id="cockpit-route-from">{{ r.from }}</strong>
|
<strong id="cockpit-route-from">{{ r.from }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<span class="cockpit-route-arrow material-symbols-outlined" aria-hidden="true">east</span>
|
<span class="cockpit-route-arrow material-symbols-outlined" aria-hidden="true">east</span>
|
||||||
<div class="cockpit-route-airport">
|
<div class="cockpit-route-airport">
|
||||||
<span data-i18n>TO</span>
|
<span>{{ t('TO') }}</span>
|
||||||
<strong id="cockpit-route-to">{{ r.to }}</strong>
|
<strong id="cockpit-route-to">{{ r.to }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ const radioLabel = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside id="cockpit-utility-controls" class="cockpit-utility-controls" v-class-toggle="{ 'layout-primary-only': !!layout?.primaryOnly }" aria-label="Cockpit display and Radio controls" data-i18n-aria-label>
|
<aside id="cockpit-utility-controls" class="cockpit-utility-controls" v-class-toggle="{ 'layout-primary-only': !!layout?.primaryOnly }" :aria-label="t('Cockpit display and Radio controls')">
|
||||||
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.display }" :aria-hidden="ariaHidden(open.display)">
|
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.display }" :aria-hidden="ariaHidden(open.display)">
|
||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
||||||
<span data-i18n>DISPLAY</span>
|
<span>{{ t('DISPLAY') }}</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel">{{ open.display ? '▶' : '◀' }}</button>
|
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel">{{ open.display ? '▶' : '◀' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-display-panel" class="cockpit-utility-popover ui-scroll" v-class-toggle="{ 'uses-shared-display-controls': !!cockpitPortal }" role="group" aria-label="Cockpit display options" :hidden="!open.display" data-i18n-aria-label>
|
<div id="cockpit-display-panel" class="cockpit-utility-popover ui-scroll" v-class-toggle="{ 'uses-shared-display-controls': !!cockpitPortal }" role="group" :aria-label="t('Cockpit display options')" :hidden="!open.display">
|
||||||
<div class="cockpit-display-slot" data-cockpit-display-slot="hud"></div>
|
<div class="cockpit-display-slot" data-cockpit-display-slot="hud"></div>
|
||||||
<div class="cockpit-display-slot" data-cockpit-display-slot="detection"></div>
|
<div class="cockpit-display-slot" data-cockpit-display-slot="detection"></div>
|
||||||
<div class="cockpit-display-slot" data-cockpit-display-slot="parameters"></div>
|
<div class="cockpit-display-slot" data-cockpit-display-slot="parameters"></div>
|
||||||
@@ -63,21 +63,21 @@ const radioLabel = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.radio }" :aria-hidden="ariaHidden(open.radio)">
|
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.radio }" :aria-hidden="ariaHidden(open.radio)">
|
||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
||||||
<span data-i18n>RADIO</span>
|
<span>{{ t('RADIO') }}</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||||
<button id="cockpit-radio-toggle-btn" @click="act('toggleCockpitRadio')" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
|
<button id="cockpit-radio-toggle-btn" @click="act('toggleCockpitRadio')" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" aria-label="Cockpit compact Radio controls" :hidden="!open.radio" data-i18n-aria-label>
|
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" :aria-label="t('Cockpit compact Radio controls')" :hidden="!open.radio">
|
||||||
<strong id="cockpit-radio-station" class="cockpit-radio-station">{{ cockpit ? cockpit.station : t('READY') }}</strong>
|
<strong id="cockpit-radio-station" class="cockpit-radio-station">{{ cockpit ? cockpit.station : t('READY') }}</strong>
|
||||||
<UiButton id="cockpit-radio-enable-btn" @click="act('toggleRadio', 'cockpit')" class="panel-layer-toggle" :pressed="cockpit?.enable.active ?? false" pressed-class="active" :aria-label="cockpit?.enable.label" :aria-disabled="cockpit ? String(cockpit.enable.busy) : undefined" :aria-busy="cockpit ? String(cockpit.enable.busy) : undefined">{{ cockpit ? cockpit.enable.text : t('ENABLE') }}</UiButton>
|
<UiButton id="cockpit-radio-enable-btn" @click="act('toggleRadio', 'cockpit')" class="panel-layer-toggle" :pressed="cockpit?.enable.active ?? false" pressed-class="active" :aria-label="cockpit?.enable.label" :aria-disabled="cockpit ? String(cockpit.enable.busy) : undefined" :aria-busy="cockpit ? String(cockpit.enable.busy) : undefined">{{ cockpit ? cockpit.enable.text : t('ENABLE') }}</UiButton>
|
||||||
<div class="cockpit-radio-transport">
|
<div class="cockpit-radio-transport">
|
||||||
<button id="cockpit-radio-prev-btn" @click="act('cycleRadio', -1, { rotate: false })" type="button" aria-label="Previous filtered radio station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label>◀◀</button>
|
<button id="cockpit-radio-prev-btn" @click="act('cycleRadio', -1, { rotate: false })" type="button" :aria-label="t('Previous filtered radio station')" :disabled="radio ? radio.transport.stepDisabled : true">◀◀</button>
|
||||||
<button id="cockpit-radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected radio station')" :title="cockpit?.play.title" :disabled="radio ? radio.transport.play.disabled : true">{{ cockpit ? cockpit.play.glyph : '▶' }}</button>
|
<button id="cockpit-radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected radio station')" :title="cockpit?.play.title" :disabled="radio ? radio.transport.play.disabled : true">{{ cockpit ? cockpit.play.glyph : '▶' }}</button>
|
||||||
<button id="cockpit-radio-next-btn" @click="act('cycleRadio', 1, { rotate: false })" type="button" aria-label="Next filtered radio station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label>▶▶</button>
|
<button id="cockpit-radio-next-btn" @click="act('cycleRadio', 1, { rotate: false })" type="button" :aria-label="t('Next filtered radio station')" :disabled="radio ? radio.transport.stepDisabled : true">▶▶</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="cockpit-radio-volume" for="cockpit-radio-volume">
|
<label class="cockpit-radio-volume" for="cockpit-radio-volume">
|
||||||
<span data-i18n>VOLUME</span>
|
<span>{{ t('VOLUME') }}</span>
|
||||||
<UiSlider id="cockpit-radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" aria-label="Cockpit Radio volume" :disabled="cockpit ? cockpit.volume.disabled : true" :model-value="cockpit ? cockpit.volume.value : 80" data-i18n-aria-label />
|
<UiSlider id="cockpit-radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" :aria-label="t('Cockpit Radio volume')" :disabled="cockpit ? cockpit.volume.disabled : true" :model-value="cockpit ? cockpit.volume.value : 80" />
|
||||||
<output id="cockpit-radio-volume-value" class="gev-slider-value" for="cockpit-radio-volume">{{ cockpit ? `${cockpit.volume.value}%` : '80%' }}</output>
|
<output id="cockpit-radio-volume-value" class="gev-slider-value" for="cockpit-radio-volume">{{ cockpit ? `${cockpit.volume.value}%` : '80%' }}</output>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const allocationOptions = [
|
|||||||
<!-- Post-processing toggles (top-right) -->
|
<!-- Post-processing toggles (top-right) -->
|
||||||
<div id="pp-toggles" v-panel-state="'pp-toggles'" class="panel-collapsible collapsed ui-scroll" data-panel-id="pp-toggles">
|
<div id="pp-toggles" v-panel-state="'pp-toggles'" class="panel-collapsible collapsed ui-scroll" data-panel-id="pp-toggles">
|
||||||
<div class="compact pp-header-row">
|
<div class="compact pp-header-row">
|
||||||
<span data-i18n class="pp-header-label">DISPLAY</span>
|
<span class="pp-header-label">{{ t('DISPLAY') }}</span>
|
||||||
<span class="panel-divider" aria-hidden="true"></span>
|
<span class="panel-divider" aria-hidden="true"></span>
|
||||||
<PanelCollapseButton class="pp-collapse-btn" target="pp-toggles" :name="t('DISPLAY')" sideways start-collapsed />
|
<PanelCollapseButton class="pp-collapse-btn" target="pp-toggles" :name="t('DISPLAY')" sideways start-collapsed />
|
||||||
</div>
|
</div>
|
||||||
@@ -67,73 +67,73 @@ const allocationOptions = [
|
|||||||
<!-- Cockpit shows these four groups in its Display popover: each teleports to the slot the Cockpit display portal publishes. While disabled a Teleport stays in place; its 'body' target only keeps server rendering from dropping it. -->
|
<!-- Cockpit shows these four groups in its Display popover: each teleports to the slot the Cockpit display portal publishes. While disabled a Teleport stays in place; its 'body' target only keeps server rendering from dropping it. -->
|
||||||
<Teleport :to="cockpitPortal?.hud ?? 'body'" :disabled="!cockpitPortal?.hud">
|
<Teleport :to="cockpitPortal?.hud ?? 'body'" :disabled="!cockpitPortal?.hud">
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<button class="pp-toggle-btn" :class="{ active: hud?.visible }" id="hud-toggle" @click="act('toggleHud')" title="Intelligence HUD (H)" data-i18n-title>
|
<button class="pp-toggle-btn" :class="{ active: hud?.visible }" id="hud-toggle" @click="act('toggleHud')" :title="t('Intelligence HUD (H)')">
|
||||||
<span class="pp-icon">◎</span>
|
<span class="pp-icon">◎</span>
|
||||||
<span class="pp-label" translate="no">HUD</span>
|
<span class="pp-label" translate="no">HUD</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="pp-slider-row" :class="{ visible: hud?.visible }" id="hud-layout-row">
|
<div class="pp-slider-row" :class="{ visible: hud?.visible }" id="hud-layout-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Layout</span>
|
<span class="pp-slider-mini-label">{{ t('Layout') }}</span>
|
||||||
<UiSelect id="hud-layout-select" class="pp-select gev-field gev-field--compact" :model-value="hud?.variant ?? 'tactical'" @update:model-value="act('setHudLayout', $event)" aria-label="HUD layout" data-i18n-aria-label>
|
<UiSelect id="hud-layout-select" class="pp-select gev-field gev-field--compact" :model-value="hud?.variant ?? 'tactical'" @update:model-value="act('setHudLayout', $event)" :aria-label="t('HUD layout')">
|
||||||
<option value="tactical" data-i18n>Tactical</option>
|
<option value="tactical">{{ t('Tactical') }}</option>
|
||||||
<option value="minimal" data-i18n>Minimal</option>
|
<option value="minimal">{{ t('Minimal') }}</option>
|
||||||
<option value="cyber" data-i18n>Cyber</option>
|
<option value="cyber">{{ t('Cyber') }}</option>
|
||||||
</UiSelect>
|
</UiSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-toggle-row" id="cyber-sonar-row">
|
<div class="pp-slider-row cyber-sonar-control cyber-sonar-toggle-row" id="cyber-sonar-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Sonar</span>
|
<span class="pp-slider-mini-label">{{ t('Sonar') }}</span>
|
||||||
<UiButton class="pp-mode-btn" :pressed="sonarOn" pressed-class="active" id="cyber-sonar-toggle" @click="act('toggleSonar')" title="Cyber sonar contact scanner" aria-label="Cyber sonar contact scanner" data-i18n-title data-i18n-aria-label>{{ sonarOn ? t('ON') : t('OFF') }}</UiButton>
|
<UiButton class="pp-mode-btn" :pressed="sonarOn" pressed-class="active" id="cyber-sonar-toggle" @click="act('toggleSonar')" :title="t('Cyber sonar contact scanner')" :aria-label="t('Cyber sonar contact scanner')">{{ sonarOn ? t('ON') : t('OFF') }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<output id="cyber-sonar-status" class="cyber-sonar-control" role="status"></output>
|
<output id="cyber-sonar-status" class="cyber-sonar-control" role="status"></output>
|
||||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-rings-row">
|
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-rings-row">
|
||||||
<label class="pp-slider-mini-label" for="cyber-sonar-rings" data-i18n>Rings</label>
|
<label class="pp-slider-mini-label" for="cyber-sonar-rings">{{ t('Rings') }}</label>
|
||||||
<UiSlider class="pp-slider" id="cyber-sonar-rings" @update:model-value="act('setSonarRings', $event)" min="3" max="12" step="1" :model-value="sonar?.rings ?? 8" aria-label="Sonar ring count" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="cyber-sonar-rings" @update:model-value="act('setSonarRings', $event)" min="3" max="12" step="1" :model-value="sonar?.rings ?? 8" :aria-label="t('Sonar ring count')" />
|
||||||
<span class="pp-slider-value" id="cyber-sonar-rings-value">{{ sonar?.rings ?? 8 }}</span>
|
<span class="pp-slider-value" id="cyber-sonar-rings-value">{{ sonar?.rings ?? 8 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-range-row">
|
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-range-row">
|
||||||
<label class="pp-slider-mini-label" for="cyber-sonar-range" data-i18n>Range</label>
|
<label class="pp-slider-mini-label" for="cyber-sonar-range">{{ t('Range') }}</label>
|
||||||
<UiSlider class="pp-slider" id="cyber-sonar-range" @update:model-value="act('setSonarRange', $event)" min="60" max="120" step="1" :model-value="sonar?.range ?? 100" aria-label="Sonar ring range" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="cyber-sonar-range" @update:model-value="act('setSonarRange', $event)" min="60" max="120" step="1" :model-value="sonar?.range ?? 100" :aria-label="t('Sonar ring range')" />
|
||||||
<span class="pp-slider-value" id="cyber-sonar-range-value">{{ pct(sonar?.range ?? 100) }}</span>
|
<span class="pp-slider-value" id="cyber-sonar-range-value">{{ pct(sonar?.range ?? 100) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-intensity-row">
|
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-intensity-row">
|
||||||
<label class="pp-slider-mini-label" for="cyber-sonar-intensity" data-i18n>Power</label>
|
<label class="pp-slider-mini-label" for="cyber-sonar-intensity">{{ t('Power') }}</label>
|
||||||
<UiSlider class="pp-slider" id="cyber-sonar-intensity" @update:model-value="act('setSonarIntensity', $event)" min="0" max="100" step="1" :model-value="sonar?.intensity ?? 70" aria-label="Sonar intensity" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="cyber-sonar-intensity" @update:model-value="act('setSonarIntensity', $event)" min="0" max="100" step="1" :model-value="sonar?.intensity ?? 70" :aria-label="t('Sonar intensity')" />
|
||||||
<span class="pp-slider-value" id="cyber-sonar-intensity-value">{{ pct(sonar?.intensity ?? 70) }}</span>
|
<span class="pp-slider-value" id="cyber-sonar-intensity-value">{{ pct(sonar?.intensity ?? 70) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-opacity-row">
|
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-opacity-row">
|
||||||
<label class="pp-slider-mini-label" for="cyber-sonar-opacity" data-i18n>Opacity</label>
|
<label class="pp-slider-mini-label" for="cyber-sonar-opacity">{{ t('Opacity') }}</label>
|
||||||
<UiSlider class="pp-slider" id="cyber-sonar-opacity" @update:model-value="act('setSonarOpacity', $event)" min="35" max="100" step="1" :model-value="sonar?.opacity ?? 84" aria-label="Cyber contact and label opacity" title="Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes" data-i18n-aria-label data-i18n-title />
|
<UiSlider class="pp-slider" id="cyber-sonar-opacity" @update:model-value="act('setSonarOpacity', $event)" min="35" max="100" step="1" :model-value="sonar?.opacity ?? 84" :aria-label="t('Cyber contact and label opacity')" :title="t('Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes')" />
|
||||||
<span class="pp-slider-value" id="cyber-sonar-opacity-value">{{ pct(sonar?.opacity ?? 84) }}</span>
|
<span class="pp-slider-value" id="cyber-sonar-opacity-value">{{ pct(sonar?.opacity ?? 84) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-sector-row">
|
<div class="pp-slider-row cyber-sonar-control cyber-sonar-setting" id="cyber-sonar-sector-row">
|
||||||
<label class="pp-slider-mini-label" for="cyber-sonar-sector" data-i18n>Sector</label>
|
<label class="pp-slider-mini-label" for="cyber-sonar-sector">{{ t('Sector') }}</label>
|
||||||
<UiSlider class="pp-slider" id="cyber-sonar-sector" @update:model-value="act('setSonarSector', $event)" min="8" max="60" step="1" :model-value="sonar?.sector ?? 24" aria-label="Sonar sector width" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="cyber-sonar-sector" @update:model-value="act('setSonarSector', $event)" min="8" max="60" step="1" :model-value="sonar?.sector ?? 24" :aria-label="t('Sonar sector width')" />
|
||||||
<span class="pp-slider-value" id="cyber-sonar-sector-value">{{ `${sonar?.sector ?? 24}°` }}</span>
|
<span class="pp-slider-value" id="cyber-sonar-sector-value">{{ `${sonar?.sector ?? 24}°` }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="cockpitPortal?.detection ?? 'body'" :disabled="!cockpitPortal?.detection">
|
<Teleport :to="cockpitPortal?.detection ?? 'body'" :disabled="!cockpitPortal?.detection">
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<UiButton class="pp-toggle-btn" :class="{ active: detectButton?.active, panoptic: detectButton?.panoptic }" :pressed="detectButton?.pressed ?? false" id="detection-toggle" @click="act('cycleDetection')" title="Detection Overlay (D)" :aria-label="detectButton?.ariaLabel ?? t('Detection overlay')" data-i18n-title>
|
<UiButton class="pp-toggle-btn" :class="{ active: detectButton?.active, panoptic: detectButton?.panoptic }" :pressed="detectButton?.pressed ?? false" id="detection-toggle" @click="act('cycleDetection')" :title="t('Detection Overlay (D)')" :aria-label="detectButton?.ariaLabel ?? t('Detection overlay')">
|
||||||
<span class="pp-icon">▣</span>
|
<span class="pp-icon">▣</span>
|
||||||
<span class="pp-label">{{ detectButton?.label ?? t('DETECT') }}</span>
|
<span class="pp-label">{{ detectButton?.label ?? t('DETECT') }}</span>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<div class="pp-slider-row detection-slider-row" :class="{ visible: detection?.rowsVisible }" id="detection-slider-row">
|
<div class="pp-slider-row detection-slider-row" :class="{ visible: detection?.rowsVisible }" id="detection-slider-row">
|
||||||
<label class="pp-slider-mini-label" for="detection-density-slider" data-i18n>Density</label>
|
<label class="pp-slider-mini-label" for="detection-density-slider">{{ t('Density') }}</label>
|
||||||
<UiSlider class="pp-slider" id="detection-density-slider" @update:model-value="act('setDensity', $event)" min="0" max="100" step="25" :model-value="detection?.densityPct ?? 50" aria-label="Detection label density" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="detection-density-slider" @update:model-value="act('setDensity', $event)" min="0" max="100" step="25" :model-value="detection?.densityPct ?? 50" :aria-label="t('Detection label density')" />
|
||||||
<span class="pp-slider-value" id="detection-density-value">{{ pct(detection?.densityPct ?? 50) }}</span>
|
<span class="pp-slider-value" id="detection-density-value">{{ pct(detection?.densityPct ?? 50) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row detection-allocation-row" :class="{ visible: detection?.rowsVisible }" id="detection-allocation-row">
|
<div class="pp-slider-row detection-allocation-row" :class="{ visible: detection?.rowsVisible }" id="detection-allocation-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Allocation</span>
|
<span class="pp-slider-mini-label">{{ t('Allocation') }}</span>
|
||||||
<UiSegmented class="pp-mode-seg detection-allocation-seg" item-class="pp-mode-btn" keyboard="none" :label="t('Detection label allocation')" :options="allocationOptions" :model-value="allocation" @update:model-value="act('setAllocation', $event)" />
|
<UiSegmented class="pp-mode-seg detection-allocation-seg" item-class="pp-mode-btn" keyboard="none" :label="t('Detection label allocation')" :options="allocationOptions" :model-value="allocation" @update:model-value="act('setAllocation', $event)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row detection-fade-row" :class="{ visible: detection?.rowsVisible }" id="detection-fade-row">
|
<div class="pp-slider-row detection-fade-row" :class="{ visible: detection?.rowsVisible }" id="detection-fade-row">
|
||||||
<label class="pp-slider-mini-label" for="detection-fade-slider" data-i18n>Fade</label>
|
<label class="pp-slider-mini-label" for="detection-fade-slider">{{ t('Fade') }}</label>
|
||||||
<UiSlider class="pp-slider" id="detection-fade-slider" @update:model-value="act('setFade', $event)" min="0" max="40" step="1" :model-value="detection?.fadePct ?? 7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label data-i18n-title />
|
<UiSlider class="pp-slider" id="detection-fade-slider" @update:model-value="act('setFade', $event)" min="0" max="40" step="1" :model-value="detection?.fadePct ?? 7" :aria-label="t('Detection fade distance')" :title="t('World-overlay fade distance outside the keyhole as a percentage of its radius')" />
|
||||||
<span class="pp-slider-value" id="detection-fade-value">{{ pct(detection?.fadePct ?? 7) }}</span>
|
<span class="pp-slider-value" id="detection-fade-value">{{ pct(detection?.fadePct ?? 7) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row detection-opacity-row" :class="{ visible: detection?.rowsVisible }" id="detection-opacity-row">
|
<div class="pp-slider-row detection-opacity-row" :class="{ visible: detection?.rowsVisible }" id="detection-opacity-row">
|
||||||
<label class="pp-slider-mini-label" for="detection-opacity-slider" data-i18n>Outside</label>
|
<label class="pp-slider-mini-label" for="detection-opacity-slider">{{ t('Outside') }}</label>
|
||||||
<UiSlider class="pp-slider" id="detection-opacity-slider" @update:model-value="act('setOutsideOpacity', $event)" min="0" max="100" step="1" :model-value="detection?.outsideOpacityPct ?? 1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label data-i18n-title />
|
<UiSlider class="pp-slider" id="detection-opacity-slider" @update:model-value="act('setOutsideOpacity', $event)" min="0" max="100" step="1" :model-value="detection?.outsideOpacityPct ?? 1" :aria-label="t('Detection opacity outside the keyhole')" :title="t('World-overlay label and card opacity beyond the fade distance')" />
|
||||||
<span class="pp-slider-value" id="detection-opacity-value">{{ pct(detection?.outsideOpacityPct ?? 1) }}</span>
|
<span class="pp-slider-value" id="detection-opacity-value">{{ pct(detection?.outsideOpacityPct ?? 1) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +142,7 @@ const allocationOptions = [
|
|||||||
<Teleport :to="cockpitPortal?.parameters ?? 'body'" :disabled="!cockpitPortal?.parameters">
|
<Teleport :to="cockpitPortal?.parameters ?? 'body'" :disabled="!cockpitPortal?.parameters">
|
||||||
<div id="param-slider-panel" v-panel-state="'param-slider-panel'" v-class-toggle="{ active: !!parameters }">
|
<div id="param-slider-panel" v-panel-state="'param-slider-panel'" v-class-toggle="{ active: !!parameters }">
|
||||||
<div class="param-panel-header">
|
<div class="param-panel-header">
|
||||||
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
<span class="param-panel-title">{{ t('PARAMETERS') }}</span>
|
||||||
<span class="param-panel-divider"></span>
|
<span class="param-panel-divider"></span>
|
||||||
<PanelCollapseButton id="param-panel-collapse-btn" target="param-slider-panel" />
|
<PanelCollapseButton id="param-panel-collapse-btn" target="param-slider-panel" />
|
||||||
</div>
|
</div>
|
||||||
@@ -155,47 +155,47 @@ const allocationOptions = [
|
|||||||
button ships lit and the mode row ships open — the same markup-carries-the-default
|
button ships lit and the mode row ships open — the same markup-carries-the-default
|
||||||
convention as #scope-toggle. ui.js re-syncs both from durable layer state, which
|
convention as #scope-toggle. ui.js re-syncs both from durable layer state, which
|
||||||
is what flips them back for a share link or a stored session carrying 3D off. -->
|
is what flips them back for a share link or a stored session carrying 3D off. -->
|
||||||
<UiButton class="pp-toggle-btn" :pressed="models3dOn" pressed-class="active" id="models3d-toggle" @click="act('toggleModels')" title="3D aircraft — flat icons zoomed out, 3D models up close" data-i18n-title>
|
<UiButton class="pp-toggle-btn" :pressed="models3dOn" pressed-class="active" id="models3d-toggle" @click="act('toggleModels')" :title="t('3D aircraft — flat icons zoomed out, 3D models up close')">
|
||||||
<span class="pp-icon">✈</span>
|
<span class="pp-icon">✈</span>
|
||||||
<span class="pp-label">3D</span>
|
<span class="pp-label">3D</span>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<div class="pp-slider-row" :class="{ visible: models3d?.rowVisible ?? true }" id="models3d-mode-row">
|
<div class="pp-slider-row" :class="{ visible: models3d?.rowVisible ?? true }" id="models3d-mode-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Models</span>
|
<span class="pp-slider-mini-label">{{ t('Models') }}</span>
|
||||||
<UiSegmented class="pp-mode-seg" item-class="pp-mode-btn" keyboard="none" :label="t('3D model coverage')" :options="modelsOptions" :model-value="modelsMode" @update:model-value="act('setModelsMode', $event)" />
|
<UiSegmented class="pp-mode-seg" item-class="pp-mode-btn" keyboard="none" :label="t('3D model coverage')" :options="modelsOptions" :model-value="modelsMode" @update:model-value="act('setModelsMode', $event)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<label class="pp-toggle-btn pp-scope-toggle" title="Scope — the circular viewport mask" data-i18n-title>
|
<label class="pp-toggle-btn pp-scope-toggle" :title="t('Scope — the circular viewport mask')">
|
||||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
|
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
|
||||||
<span class="pp-label" data-i18n>Scope</span>
|
<span class="pp-label">{{ t('Scope') }}</span>
|
||||||
<input type="checkbox" id="scope-toggle" @change="act('setScopeEnabled', $event.target.checked)" :checked="scopeOn" />
|
<input type="checkbox" id="scope-toggle" @change="act('setScopeEnabled', $event.target.checked)" :checked="scopeOn" />
|
||||||
</label>
|
</label>
|
||||||
<div class="pp-slider-row visible" id="scope-slider-row">
|
<div class="pp-slider-row visible" id="scope-slider-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Feather</span>
|
<span class="pp-slider-mini-label">{{ t('Feather') }}</span>
|
||||||
<UiSlider class="pp-slider" id="scope-feather-slider" @update:model-value="act('setScopeFeather', $event)" min="0" max="200" :model-value="scopeFeather" :disabled="!scopeOn" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
|
<UiSlider class="pp-slider" id="scope-feather-slider" @update:model-value="act('setScopeFeather', $event)" min="0" max="200" :model-value="scopeFeather" :disabled="!scopeOn" :aria-label="t('Scope edge feather')" :title="t('Scope edge feather as a percentage of the keyhole radius')" />
|
||||||
<span class="pp-slider-value" id="scope-feather-value">{{ pct(scopeFeather) }}</span>
|
<span class="pp-slider-value" id="scope-feather-value">{{ pct(scopeFeather) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<UiButton class="pp-toggle-btn" :pressed="draw?.active ?? false" pressed-class="active" id="draw-toggle" @click="act('toggleDraw')" title="Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel" data-i18n-title>
|
<UiButton class="pp-toggle-btn" :pressed="draw?.active ?? false" pressed-class="active" id="draw-toggle" @click="act('toggleDraw')" :title="t('Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel')">
|
||||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">draw</span>
|
<span class="pp-icon material-symbols-outlined" aria-hidden="true">draw</span>
|
||||||
<span class="pp-label" data-i18n>Draw</span>
|
<span class="pp-label">{{ t('Draw') }}</span>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-mode-row">
|
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-mode-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Shape</span>
|
<span class="pp-slider-mini-label">{{ t('Shape') }}</span>
|
||||||
<UiSegmented class="pp-mode-seg" item-class="pp-mode-btn" keyboard="none" :label="t('Shape to draw')" :options="drawShapeOptions" :model-value="drawShape" @update:model-value="act('setDrawShape', $event)" />
|
<UiSegmented class="pp-mode-seg" item-class="pp-mode-btn" keyboard="none" :label="t('Shape to draw')" :options="drawShapeOptions" :model-value="drawShape" @update:model-value="act('setDrawShape', $event)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-label-row">
|
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-label-row">
|
||||||
<input type="text" class="pp-text-input" id="draw-label-input" v-range-value="draw?.label ?? ''" @input="act('setDrawLabel', $event.target.value)" maxlength="120" placeholder="Label (optional)" aria-label="Label for the drawn shape" data-i18n-placeholder data-i18n-aria-label autocomplete="off" spellcheck="false">
|
<input type="text" class="pp-text-input" id="draw-label-input" v-range-value="draw?.label ?? ''" @input="act('setDrawLabel', $event.target.value)" maxlength="120" :placeholder="t('Label (optional)')" :aria-label="t('Label for the drawn shape')" autocomplete="off" spellcheck="false">
|
||||||
<UiSelect class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" :model-value="draw?.color ?? 'primary'" @update:model-value="act('setDrawColor', $event)" aria-label="Colour of the drawn shape" data-i18n-aria-label>
|
<UiSelect class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" :model-value="draw?.color ?? 'primary'" @update:model-value="act('setDrawColor', $event)" :aria-label="t('Colour of the drawn shape')">
|
||||||
<option value="primary" data-i18n>Primary</option>
|
<option value="primary">{{ t('Primary') }}</option>
|
||||||
<option value="amber" data-i18n>Amber</option>
|
<option value="amber">{{ t('Amber') }}</option>
|
||||||
<option value="cyan" data-i18n>Cyan</option>
|
<option value="cyan">{{ t('Cyan') }}</option>
|
||||||
<option value="green" data-i18n>Green</option>
|
<option value="green">{{ t('Green') }}</option>
|
||||||
<option value="red" data-i18n>Red</option>
|
<option value="red">{{ t('Red') }}</option>
|
||||||
</UiSelect>
|
</UiSelect>
|
||||||
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" @click="act('clearDraw')" title="Remove every mark from the board" data-i18n-title data-i18n>Clear</button>
|
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" @click="act('clearDraw')" :title="t('Remove every mark from the board')">{{ t('Clear') }}</button>
|
||||||
<span class="draw-hint" id="draw-hint" aria-live="polite">{{ draw?.hint ?? '' }}</span>
|
<span class="draw-hint" id="draw-hint" aria-live="polite">{{ draw?.hint ?? '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,30 +203,30 @@ const allocationOptions = [
|
|||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<UiButton class="pp-toggle-btn" :pressed="celestial?.active ?? false" pressed-class="active" id="celestial-toggle" @click="act('toggleCelestial')" :title="celestialTitle" :disabled="celestial ? !celestial.supported : undefined" :aria-disabled="celestial ? String(!celestial.supported) : undefined">
|
<UiButton class="pp-toggle-btn" :pressed="celestial?.active ?? false" pressed-class="active" id="celestial-toggle" @click="act('toggleCelestial')" :title="celestialTitle" :disabled="celestial ? !celestial.supported : undefined" :aria-disabled="celestial ? String(!celestial.supported) : undefined">
|
||||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">flare</span>
|
<span class="pp-icon material-symbols-outlined" aria-hidden="true">flare</span>
|
||||||
<span class="pp-label" data-i18n>Celestial</span>
|
<span class="pp-label">{{ t('Celestial') }}</span>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<button class="pp-toggle-btn" id="clean-view-toggle" @click="act('toggleCleanView')" title="Hide UI chrome" data-i18n-title>
|
<button class="pp-toggle-btn" id="clean-view-toggle" @click="act('toggleCleanView')" :title="t('Hide UI chrome')">
|
||||||
<span class="pp-icon">□</span>
|
<span class="pp-icon">□</span>
|
||||||
<span class="pp-label" data-i18n>Clean UI</span>
|
<span class="pp-label">{{ t('Clean UI') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<button class="pp-toggle-btn" :class="{ active: view?.bloom.active }" id="bloom-toggle" @click="act('toggleBloom')" title="Bloom / Glow" data-i18n-title>
|
<button class="pp-toggle-btn" :class="{ active: view?.bloom.active }" id="bloom-toggle" @click="act('toggleBloom')" :title="t('Bloom / Glow')">
|
||||||
<span class="pp-icon">✨</span>
|
<span class="pp-icon">✨</span>
|
||||||
<span class="pp-label" data-i18n>Bloom</span>
|
<span class="pp-label">{{ t('Bloom') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="pp-slider-row" :class="{ visible: view?.bloom.active }" id="bloom-slider-row">
|
<div class="pp-slider-row" :class="{ visible: view?.bloom.active }" id="bloom-slider-row">
|
||||||
<UiSlider class="pp-slider" id="bloom-intensity-slider" @update:model-value="act('setBloomIntensity', $event)" min="0" max="200" :model-value="view?.bloom.intensity ?? 0" aria-label="Bloom intensity" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="bloom-intensity-slider" @update:model-value="act('setBloomIntensity', $event)" min="0" max="200" :model-value="view?.bloom.intensity ?? 0" :aria-label="t('Bloom intensity')" />
|
||||||
<span class="pp-slider-value" id="bloom-intensity-value">{{ pct(view?.bloom.intensity ?? 0) }}</span>
|
<span class="pp-slider-value" id="bloom-intensity-value">{{ pct(view?.bloom.intensity ?? 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<button class="pp-toggle-btn" :class="{ active: view?.sharpen.active }" id="sharpen-toggle" @click="act('toggleSharpen')" title="Sharpening" data-i18n-title>
|
<button class="pp-toggle-btn" :class="{ active: view?.sharpen.active }" id="sharpen-toggle" @click="act('toggleSharpen')" :title="t('Sharpening')">
|
||||||
<span class="pp-icon">🔍</span>
|
<span class="pp-icon">🔍</span>
|
||||||
<span class="pp-label" data-i18n>Sharpen</span>
|
<span class="pp-label">{{ t('Sharpen') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="pp-slider-row" :class="{ visible: view?.sharpen.active }" id="sharpen-slider-row">
|
<div class="pp-slider-row" :class="{ visible: view?.sharpen.active }" id="sharpen-slider-row">
|
||||||
<UiSlider class="pp-slider" id="sharpen-intensity-slider" @update:model-value="act('setSharpenIntensity', $event)" min="0" max="100" :model-value="view?.sharpen.intensity ?? 49" aria-label="Sharpen intensity" data-i18n-aria-label />
|
<UiSlider class="pp-slider" id="sharpen-intensity-slider" @update:model-value="act('setSharpenIntensity', $event)" min="0" max="100" :model-value="view?.sharpen.intensity ?? 49" :aria-label="t('Sharpen intensity')" />
|
||||||
<span class="pp-slider-value" id="sharpen-intensity-value">{{ pct(view?.sharpen.intensity ?? 49) }}</span>
|
<span class="pp-slider-value" id="sharpen-intensity-value">{{ pct(view?.sharpen.intensity ?? 49) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { t } from '../../../i18n/index.js';
|
||||||
import { vFocusSafeChildren } from '../../rail/focusSafeChildren.js';
|
import { vFocusSafeChildren } from '../../rail/focusSafeChildren.js';
|
||||||
import { useDisplayStore } from '../../stores/display.js';
|
import { useDisplayStore } from '../../stores/display.js';
|
||||||
|
|
||||||
@@ -57,12 +58,12 @@ function leave() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pp-toggle-group" id="geo-import-group">
|
<div class="pp-toggle-group" id="geo-import-group">
|
||||||
<button ref="importButton" class="pp-toggle-btn" id="geo-import-button" type="button" @click="openPicker" title="Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice." data-i18n-title>
|
<button ref="importButton" class="pp-toggle-btn" id="geo-import-button" type="button" @click="openPicker" :title="t('Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice.')">
|
||||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">upload_file</span>
|
<span class="pp-icon material-symbols-outlined" aria-hidden="true">upload_file</span>
|
||||||
<span class="pp-label" data-i18n>Import</span>
|
<span class="pp-label">{{ t('Import') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<input ref="picker" type="file" id="geo-import-input" accept=".geojson,.json,.kml,.kmz,.gpx" multiple hidden @change="pick" aria-label="Choose GeoJSON, KML, KMZ or GPX files" data-i18n-aria-label>
|
<input ref="picker" type="file" id="geo-import-input" accept=".geojson,.json,.kml,.kmz,.gpx" multiple hidden @change="pick" :aria-label="t('Choose GeoJSON, KML, KMZ or GPX files')">
|
||||||
<ul class="geo-import-list" :class="{ visible: listed }" id="geo-import-list" v-focus-safe-children="leave" aria-label="Imported files" data-i18n-aria-label><li v-for="row in rows" :key="row.id" class="geo-import-row" :data-import-id="row.id"><span :class="['geo-import-swatch', { 'own-styles': row.ownStyles }]" :style="row.ownStyles ? null : { background: row.color }"></span><span class="geo-import-name" :title="row.title">{{ row.name }}</span><button v-for="item in row.actions" :key="item.action" type="button" class="geo-import-action" :data-action="item.action" :title="item.label" :aria-label="item.label" :aria-pressed="item.pressed" @click="display.act(ROW_ACTIONS[item.action], row.id)"><span class="material-symbols-outlined" aria-hidden="true">{{ item.icon }}</span></button></li></ul>
|
<ul class="geo-import-list" :class="{ visible: listed }" id="geo-import-list" v-focus-safe-children="leave" :aria-label="t('Imported files')"><li v-for="row in rows" :key="row.id" class="geo-import-row" :data-import-id="row.id"><span :class="['geo-import-swatch', { 'own-styles': row.ownStyles }]" :style="row.ownStyles ? null : { background: row.color }"></span><span class="geo-import-name" :title="row.title">{{ row.name }}</span><button v-for="item in row.actions" :key="item.action" type="button" class="geo-import-action" :data-action="item.action" :title="item.label" :aria-label="item.label" :aria-pressed="item.pressed" @click="display.act(ROW_ACTIONS[item.action], row.id)"><span class="material-symbols-outlined" aria-hidden="true">{{ item.icon }}</span></button></li></ul>
|
||||||
<span class="draw-hint geo-import-hint" id="geo-import-hint" aria-live="polite">{{ geoImport?.hint ?? '' }}</span>
|
<span class="draw-hint geo-import-hint" id="geo-import-hint" aria-live="polite">{{ geoImport?.hint ?? '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,18 +17,18 @@ const tilted = computed(() => Boolean(camera.value?.tilted));
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav id="top-center-actions" aria-label="Globe actions" data-i18n-aria-label>
|
<nav id="top-center-actions" :aria-label="t('Globe actions')">
|
||||||
<button id="clear-selected-layers" type="button" :aria-label="clearBusy ? t('Clearing selected data layers') : t('Clear selected data layers')" :aria-disabled="clearBusy == null ? undefined : String(clearBusy)" :aria-busy="clearBusy == null ? undefined : String(clearBusy)" title="Turn off all selected data layers" data-i18n-title @click="act('clear')">
|
<button id="clear-selected-layers" type="button" :aria-label="clearBusy ? t('Clearing selected data layers') : t('Clear selected data layers')" :aria-disabled="clearBusy == null ? undefined : String(clearBusy)" :aria-busy="clearBusy == null ? undefined : String(clearBusy)" :title="t('Turn off all selected data layers')" @click="act('clear')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">layers_clear</span>
|
<span class="material-symbols-outlined" aria-hidden="true">layers_clear</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="share-btn" type="button" aria-label="Copy share link" title="Copy share link" data-i18n-aria-label data-i18n-title @click="act('share')">
|
<button id="share-btn" type="button" :aria-label="t('Copy share link')" :title="t('Copy share link')" @click="act('share')">
|
||||||
<span class="share-icon" aria-hidden="true">🔗</span>
|
<span class="share-icon" aria-hidden="true">🔗</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="tilt-map-view" type="button" :aria-label="tilted ? t('Return map to straight-down view') : t('Tilt map to oblique view')" :aria-pressed="String(tilted)" title="Toggle straight-down and tilted map views" data-i18n-title @click="act('tilt')">
|
<button id="tilt-map-view" type="button" :aria-label="tilted ? t('Return map to straight-down view') : t('Tilt map to oblique view')" :aria-pressed="String(tilted)" :title="t('Toggle straight-down and tilted map views')" @click="act('tilt')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">view_in_ar</span>
|
<span class="material-symbols-outlined" aria-hidden="true">view_in_ar</span>
|
||||||
</button>
|
</button>
|
||||||
<GlobeCompass @click="act('northUp')" />
|
<GlobeCompass @click="act('northUp')" />
|
||||||
<button id="reset-globe-view" type="button" :aria-label="resetBusy ? t('Resetting to full globe view') : t('Reset to full globe view')" title="Reset camera and return to full globe view" data-i18n-title @click="act('reset')">
|
<button id="reset-globe-view" type="button" :aria-label="resetBusy ? t('Resetting to full globe view') : t('Reset to full globe view')" :title="t('Reset camera and return to full globe view')" @click="act('reset')">
|
||||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const label = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button id="north-up-view" type="button" :aria-label="label" :data-north-up="heading == null ? undefined : String(heading === 0)" :style="heading == null ? undefined : { '--camera-heading': `${heading}deg` }" title="Reset map bearing to north" data-i18n-title @click="$emit('click')">
|
<button id="north-up-view" type="button" :aria-label="label" :data-north-up="heading == null ? undefined : String(heading === 0)" :style="heading == null ? undefined : { '--camera-heading': `${heading}deg` }" :title="t('Reset map bearing to north')" @click="$emit('click')">
|
||||||
<span class="camera-compass-n" aria-hidden="true">N</span>
|
<span class="camera-compass-n" aria-hidden="true">N</span>
|
||||||
<span class="camera-compass-needle material-symbols-outlined" aria-hidden="true">navigation</span>
|
<span class="camera-compass-needle material-symbols-outlined" aria-hidden="true">navigation</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ onUpdated(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="space-mission-roster" ref="root" class="space-mission-roster" aria-label="Available Space Missions" data-i18n-aria-label>
|
<div id="space-mission-roster" ref="root" class="space-mission-roster" :aria-label="t('Available Space Missions')">
|
||||||
<div class="space-mission-roster-header">
|
<div class="space-mission-roster-header">
|
||||||
<div>
|
<div>
|
||||||
<strong data-i18n>AVAILABLE MISSIONS</strong>
|
<strong>{{ t('AVAILABLE MISSIONS') }}</strong>
|
||||||
<span data-i18n>SELECT A MISSION TO INSPECT</span>
|
<span>{{ t('SELECT A MISSION TO INSPECT') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<output data-mission-roster-count>{{ roster ? roster.count : '—' }}</output>
|
<output data-mission-roster-count>{{ roster ? roster.count : '—' }}</output>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,6 +74,6 @@ onUpdated(() => {
|
|||||||
<UiEmpty v-else-if="!roster.entries.length" class="space-mission-roster-empty" :title="roster.empty" />
|
<UiEmpty v-else-if="!roster.entries.length" class="space-mission-roster-empty" :title="roster.empty" />
|
||||||
<button v-for="entry in roster?.entries" v-else :key="entry.id" type="button" class="space-mission-roster-item" :data-mission-roster-index="entry.index" :data-mission-roster-id="entry.id" :aria-label="entry.ariaLabel" @click="layer?.selectMissionAt(entry.index)" @mouseenter="enter(entry.index)" @mousemove="move(entry.index)" @mouseleave="layer?.rosterPointerLeave()" @focus="layer?.rosterFocus(entry.index)" @blur="blur"><span class="space-mission-roster-marker" :style="{ '--mission-roster-color': entry.color }" aria-hidden="true"></span><span class="space-mission-roster-copy"><strong>{{ entry.label }}</strong><small>{{ entry.provider }} · {{ entry.date }}</small></span><span class="space-mission-roster-chevron" aria-hidden="true">›</span></button>
|
<button v-for="entry in roster?.entries" v-else :key="entry.id" type="button" class="space-mission-roster-item" :data-mission-roster-index="entry.index" :data-mission-roster-id="entry.id" :aria-label="entry.ariaLabel" @click="layer?.selectMissionAt(entry.index)" @mouseenter="enter(entry.index)" @mousemove="move(entry.index)" @mouseleave="layer?.rosterPointerLeave()" @focus="layer?.rosterFocus(entry.index)" @blur="blur"><span class="space-mission-roster-marker" :style="{ '--mission-roster-color': entry.color }" aria-hidden="true"></span><span class="space-mission-roster-copy"><strong>{{ entry.label }}</strong><small>{{ entry.provider }} · {{ entry.date }}</small></span><span class="space-mission-roster-chevron" aria-hidden="true">›</span></button>
|
||||||
</div>
|
</div>
|
||||||
<p class="space-mission-roster-focus-continuation" tabindex="-1" data-mission-roster-focus-continuation data-i18n>TAB PREVIEWS · ENTER / SPACE SELECTS</p>
|
<p class="space-mission-roster-focus-continuation" tabindex="-1" data-mission-roster-focus-continuation>{{ t('TAB PREVIEWS · ENTER / SPACE SELECTS') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,12 +36,12 @@ const radioWords = {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RailPanel id="radio-panel" tag="section" start-collapsed v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" aria-label="Internet radio and local SDR" data-i18n-aria-label :title="t('RADIO')" :toggle-words="radioWords" :glow="false" scroll>
|
<RailPanel id="radio-panel" tag="section" start-collapsed v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" :aria-label="t('Internet radio and local SDR')" :title="t('RADIO')" :toggle-words="radioWords" :glow="false" scroll>
|
||||||
<template #badge><span id="radio-layer-state" class="radio-layer-state" :class="{ active: radio?.layerState.active }">{{ radio ? radio.layerState.text : t('OFF') }}</span></template>
|
<template #badge><span id="radio-layer-state" class="radio-layer-state" :class="{ active: radio?.layerState.active }">{{ radio ? radio.layerState.text : t('OFF') }}</span></template>
|
||||||
<div class="radio-directory-row">
|
<div class="radio-directory-row">
|
||||||
<UiButton id="radio-enable-btn" @click="act('toggleRadio', 'panel')" class="panel-layer-toggle" :pressed="radio?.enable.active ?? false" pressed-class="active" :aria-label="radio?.enable.label" :aria-disabled="radio ? String(radio.enable.busy) : undefined" :aria-busy="radio ? String(radio.enable.busy) : undefined">{{ radio ? radio.enable.text : t('ENABLE') }}</UiButton>
|
<UiButton id="radio-enable-btn" @click="act('toggleRadio', 'panel')" class="panel-layer-toggle" :pressed="radio?.enable.active ?? false" pressed-class="active" :aria-label="radio?.enable.label" :aria-disabled="radio ? String(radio.enable.busy) : undefined" :aria-busy="radio ? String(radio.enable.busy) : undefined">{{ radio ? radio.enable.text : t('ENABLE') }}</UiButton>
|
||||||
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
|
<label class="radio-filter-label" for="radio-filter">{{ t('STATION TAG') }}</label>
|
||||||
<UiSelect class="gev-field" id="radio-filter" @change="setFilter($event.target)" aria-label="Filter stations by station tag" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" :model-value="radio ? radio.filter.value : 'all'" data-i18n-aria-label>
|
<UiSelect class="gev-field" id="radio-filter" @change="setFilter($event.target)" :aria-label="t('Filter stations by station tag')" :disabled="radio ? radio.filter.disabled : true" v-bind="radio?.filter.color ? { style: { color: radio.filter.color } } : {}" :model-value="radio ? radio.filter.value : 'all'">
|
||||||
<option v-if="!radio?.filter.options" value="all">{{ t('All', { context: 'radio tag' }) }}</option><template v-else><option v-for="option in radio.filter.options" :key="option.id" :value="option.id" :data-radio-color="option.color" :style="{ color: option.color }" :aria-label="option.label">● {{ option.label }}</option></template>
|
<option v-if="!radio?.filter.options" value="all">{{ t('All', { context: 'radio tag' }) }}</option><template v-else><option v-for="option in radio.filter.options" :key="option.id" :value="option.id" :data-radio-color="option.color" :style="{ color: option.color }" :aria-label="option.label">● {{ option.label }}</option></template>
|
||||||
</UiSelect>
|
</UiSelect>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,23 +51,23 @@ const radioWords = {
|
|||||||
<span id="radio-station-tags">{{ radio?.station.tags ?? '' }}</span>
|
<span id="radio-station-tags">{{ radio?.station.tags ?? '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<RadioTuner />
|
<RadioTuner />
|
||||||
<div class="radio-transport" role="group" aria-label="Radio playback" data-i18n-aria-label>
|
<div class="radio-transport" role="group" :aria-label="t('Radio playback')">
|
||||||
<button id="radio-prev-btn" @click="act('cycleRadio', -1)" type="button" aria-label="Previous filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>PREV</button>
|
<button id="radio-prev-btn" @click="act('cycleRadio', -1)" type="button" :aria-label="t('Previous filtered station')" :disabled="radio ? radio.transport.stepDisabled : true">{{ t('PREV') }}</button>
|
||||||
<button id="radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected station')" :disabled="radio ? radio.transport.play.disabled : true">{{ radio ? radio.transport.play.text : t('PLAY') }}</button>
|
<button id="radio-play-btn" @click="act('toggleRadioPlayback')" type="button" v-class-toggle="{ active: radio?.transport.play.active }" :aria-label="radio ? radio.transport.play.label : t('Play selected station')" :disabled="radio ? radio.transport.play.disabled : true">{{ radio ? radio.transport.play.text : t('PLAY') }}</button>
|
||||||
<button id="radio-next-btn" @click="act('cycleRadio', 1)" type="button" aria-label="Next filtered station" :disabled="radio ? radio.transport.stepDisabled : true" data-i18n-aria-label data-i18n>NEXT</button>
|
<button id="radio-next-btn" @click="act('cycleRadio', 1)" type="button" :aria-label="t('Next filtered station')" :disabled="radio ? radio.transport.stepDisabled : true">{{ t('NEXT') }}</button>
|
||||||
<button id="radio-stop-btn" @click="act('stopRadio')" type="button" aria-label="Stop radio playback" :disabled="radio ? radio.transport.stopDisabled : true" data-i18n-aria-label data-i18n>STOP</button>
|
<button id="radio-stop-btn" @click="act('stopRadio')" type="button" :aria-label="t('Stop radio playback')" :disabled="radio ? radio.transport.stopDisabled : true">{{ t('STOP') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="radio-volume-row" for="radio-volume">
|
<label class="radio-volume-row" for="radio-volume">
|
||||||
<span data-i18n>VOLUME</span>
|
<span>{{ t('VOLUME') }}</span>
|
||||||
<UiSlider id="radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" aria-label="Radio volume" :disabled="radio?.volume.disabled" :model-value="radio ? radio.volume.value : 80" data-i18n-aria-label />
|
<UiSlider id="radio-volume" hold @update:model-value="act('setRadioVolume', $event)" min="0" max="100" :aria-label="t('Radio volume')" :disabled="radio?.volume.disabled" :model-value="radio ? radio.volume.value : 80" />
|
||||||
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">{{ radio ? `${radio.volume.value}%` : '80%' }}</output>
|
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">{{ radio ? `${radio.volume.value}%` : '80%' }}</output>
|
||||||
</label>
|
</label>
|
||||||
<div id="radio-playback-state" class="radio-playback-state" :class="{ error: radio?.playback.error }" role="status" aria-live="polite">{{ radio ? radio.playback.text : t('Radio off') }}</div>
|
<div id="radio-playback-state" class="radio-playback-state" :class="{ error: radio?.playback.error }" role="status" aria-live="polite">{{ radio ? radio.playback.text : t('Radio off') }}</div>
|
||||||
<div class="radio-links">
|
<div class="radio-links">
|
||||||
<a id="radio-station-homepage" :href="radio ? radio.station.homepage || undefined : '#'" target="_blank" rel="noopener noreferrer" :hidden="radio ? !radio.station.homepage : true" data-i18n>STATION SITE</a>
|
<a id="radio-station-homepage" :href="radio ? radio.station.homepage || undefined : '#'" target="_blank" rel="noopener noreferrer" :hidden="radio ? !radio.station.homepage : true">{{ t('STATION SITE') }}</a>
|
||||||
<a href="https://www.radio-browser.info/" target="_blank" rel="noopener noreferrer" data-i18n>DIRECTORY: RADIO BROWSER</a>
|
<a href="https://www.radio-browser.info/" target="_blank" rel="noopener noreferrer">{{ t('DIRECTORY: RADIO BROWSER') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="radio-privacy" data-i18n>Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.</p>
|
<p class="radio-privacy">{{ t('Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.') }}</p>
|
||||||
<SdrCard />
|
<SdrCard />
|
||||||
</RailPanel>
|
</RailPanel>
|
||||||
</template>
|
</template>
|
||||||
@@ -74,11 +74,11 @@ const vTunerDragging = {
|
|||||||
<input id="radio-tuner-slider" type="range" min="0" :max="tuner ? tuner.max : 0" step="1" value="0" v-range-value="tuner ? tuner.slot : 0"
|
<input id="radio-tuner-slider" type="range" min="0" :max="tuner ? tuner.max : 0" step="1" value="0" v-range-value="tuner ? tuner.slot : 0"
|
||||||
@pointerdown="act('tunerPointerDown', $event)" @pointermove="act('tunerPointerMove', $event)" @pointerup="act('tunerPointerUp', $event)" @pointercancel="act('tunerPointerCancel', $event)" @lostpointercapture="act('tunerLostPointerCapture', $event)"
|
@pointerdown="act('tunerPointerDown', $event)" @pointermove="act('tunerPointerMove', $event)" @pointerup="act('tunerPointerUp', $event)" @pointercancel="act('tunerPointerCancel', $event)" @lostpointercapture="act('tunerLostPointerCapture', $event)"
|
||||||
@keydown="act('tunerKeyDown', $event)" @keyup="act('tunerKeyUp', $event)" @input="act('tunerInput')" @change="act('tunerChange')" @blur="act('tunerBlur')"
|
@keydown="act('tunerKeyDown', $event)" @keyup="act('tunerKeyUp', $event)" @input="act('tunerInput')" @change="act('tunerChange')" @blur="act('tunerBlur')"
|
||||||
aria-label="Tune available internet radio stations" :aria-valuetext="tuner?.valuetext ?? t('No station available')" :disabled="tuner ? !tuner.available : true" data-i18n-aria-label />
|
:aria-label="t('Tune available internet radio stations')" :aria-valuetext="tuner?.valuetext ?? t('No station available')" :disabled="tuner ? !tuner.available : true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="radio-tuner-readout">
|
<div class="radio-tuner-readout">
|
||||||
<span id="radio-tuner-station">{{ tuner?.station ?? t('ALL · DRAG THE NEEDLE') }}</span>
|
<span id="radio-tuner-station">{{ tuner?.station ?? t('ALL · DRAG THE NEEDLE') }}</span>
|
||||||
<span data-i18n>SNAPS TO AVAILABLE STATIONS</span>
|
<span>{{ t('SNAPS TO AVAILABLE STATIONS') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -24,46 +24,46 @@ const tune = () => act('tuneSdr', frequency.value.value);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="sdr-radio-card" class="sdr-radio-card" role="group" aria-label="Local RTL-SDR receiver" data-i18n-aria-label>
|
<div id="sdr-radio-card" class="sdr-radio-card" role="group" :aria-label="t('Local RTL-SDR receiver')">
|
||||||
<div class="sdr-radio-heading">
|
<div class="sdr-radio-heading">
|
||||||
<strong data-i18n>LOCAL RTL-SDR</strong>
|
<strong>{{ t('LOCAL RTL-SDR') }}</strong>
|
||||||
<span id="sdr-connection-state" v-class-toggle="{ active: sdr?.connectionActive }" data-i18n-runtime>{{ sdr ? sdr.connectionLabel : 'NO USB' }}</span>
|
<span id="sdr-connection-state" v-class-toggle="{ active: sdr?.connectionActive }">{{ sdr ? sdr.connectionLabel : 'NO USB' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p data-i18n>Chrome/Edge · WebUSB · one local tuner shared by FM and the Local ADS-B layer.</p>
|
<p>{{ t('Chrome/Edge · WebUSB · one local tuner shared by FM and the Local ADS-B layer.') }}</p>
|
||||||
<div class="sdr-radio-actions">
|
<div class="sdr-radio-actions">
|
||||||
<UiButton id="sdr-connect-btn" @click="act('toggleSdrConnection')" class="panel-layer-toggle" :pressed="sdr?.connectPressed ?? false" pressed-class="active" :disabled="sdr?.connectDisabled">{{ sdr ? sdr.connectLabel : t('CONNECT') }}</UiButton>
|
<UiButton id="sdr-connect-btn" @click="act('toggleSdrConnection')" class="panel-layer-toggle" :pressed="sdr?.connectPressed ?? false" pressed-class="active" :disabled="sdr?.connectDisabled">{{ sdr ? sdr.connectLabel : t('CONNECT') }}</UiButton>
|
||||||
<button id="sdr-locate-btn" type="button" @click="act('locateSdr')" v-class-toggle="{ active: sdr?.locateActive }" title="Use your location for faster ADS-B position decoding" :disabled="sdr?.locateDisabled" data-i18n-title>{{ sdr ? sdr.locateLabel : t('LOCATE') }}</button>
|
<button id="sdr-locate-btn" type="button" @click="act('locateSdr')" v-class-toggle="{ active: sdr?.locateActive }" :title="t('Use your location for faster ADS-B position decoding')" :disabled="sdr?.locateDisabled">{{ sdr ? sdr.locateLabel : t('LOCATE') }}</button>
|
||||||
<button id="sdr-change-device-btn" type="button" @click="act('changeSdrDevice')" title="Choose a different RTL-SDR or receiver channel" :disabled="sdr?.changeDeviceDisabled" data-i18n-title data-i18n>CHANGE DEVICE</button>
|
<button id="sdr-change-device-btn" type="button" @click="act('changeSdrDevice')" :title="t('Choose a different RTL-SDR or receiver channel')" :disabled="sdr?.changeDeviceDisabled">{{ t('CHANGE DEVICE') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sdr-mode-row" role="group" aria-label="RTL-SDR receiver mode" data-i18n-aria-label>
|
<div class="sdr-mode-row" role="group" :aria-label="t('RTL-SDR receiver mode')">
|
||||||
<UiButton id="sdr-mode-fm-btn" @click="act('setSdrMode', 'fm')" :pressed="sdr ? sdr.fmActive : true" pressed-class="active" :disabled="sdr?.modeDisabled" translate="no">FM</UiButton>
|
<UiButton id="sdr-mode-fm-btn" @click="act('setSdrMode', 'fm')" :pressed="sdr ? sdr.fmActive : true" pressed-class="active" :disabled="sdr?.modeDisabled" translate="no">FM</UiButton>
|
||||||
<UiButton id="sdr-mode-adsb-btn" @click="act('setSdrMode', 'adsb')" :pressed="sdr?.adsbActive ?? false" pressed-class="active" :disabled="sdr?.modeDisabled" translate="no">ADS-B · 1090</UiButton>
|
<UiButton id="sdr-mode-adsb-btn" @click="act('setSdrMode', 'adsb')" :pressed="sdr?.adsbActive ?? false" pressed-class="active" :disabled="sdr?.modeDisabled" translate="no">ADS-B · 1090</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="sdr-gain-row">
|
<div class="sdr-gain-row">
|
||||||
<label for="sdr-gain-select" data-i18n>GAIN</label>
|
<label for="sdr-gain-select">{{ t('GAIN') }}</label>
|
||||||
<UiSelect class="gev-field" id="sdr-gain-select" hold :model-value="sdr ? sdr.gain : 'auto'" @update:model-value="act('setSdrGain', $event)" aria-label="RTL-SDR tuner gain for the current mode" :disabled="sdr?.gainDisabled" data-i18n-aria-label>
|
<UiSelect class="gev-field" id="sdr-gain-select" hold :model-value="sdr ? sdr.gain : 'auto'" @update:model-value="act('setSdrGain', $event)" :aria-label="t('RTL-SDR tuner gain for the current mode')" :disabled="sdr?.gainDisabled">
|
||||||
<option value="auto" data-i18n>AUTO</option>
|
<option value="auto">{{ t('AUTO') }}</option>
|
||||||
<option v-for="gain in GAINS" :key="gain" :value="gain" translate="no">{{ localSdrGainOptionLabel(gain) }}</option>
|
<option v-for="gain in GAINS" :key="gain" :value="gain" translate="no">{{ localSdrGainOptionLabel(gain) }}</option>
|
||||||
</UiSelect>
|
</UiSelect>
|
||||||
</div>
|
</div>
|
||||||
<div id="sdr-adsb-stats" class="sdr-adsb-stats" role="status" aria-live="off" :hidden="sdr ? sdr.statsHidden : true">
|
<div id="sdr-adsb-stats" class="sdr-adsb-stats" role="status" aria-live="off" :hidden="sdr ? sdr.statsHidden : true">
|
||||||
<span><output id="sdr-stat-rate">{{ sdr ? sdr.stats.rate : '--' }}</output> <span data-i18n>MSG/S</span></span>
|
<span><output id="sdr-stat-rate">{{ sdr ? sdr.stats.rate : '--' }}</output> <span>{{ t('MSG/S') }}</span></span>
|
||||||
<span><output id="sdr-stat-heard">{{ sdr ? sdr.stats.heard : '0' }}</output> <span data-i18n>HEARD</span></span>
|
<span><output id="sdr-stat-heard">{{ sdr ? sdr.stats.heard : '0' }}</output> <span>{{ t('HEARD') }}</span></span>
|
||||||
<span><output id="sdr-stat-positioned">{{ sdr ? sdr.stats.positioned : '0' }}</output> <span data-i18n>POSITIONED</span></span>
|
<span><output id="sdr-stat-positioned">{{ sdr ? sdr.stats.positioned : '0' }}</output> <span>{{ t('POSITIONED') }}</span></span>
|
||||||
<span translate="no">IQ <output id="sdr-stat-iq">{{ sdr ? sdr.stats.iq : '--' }}</output></span>
|
<span translate="no">IQ <output id="sdr-stat-iq">{{ sdr ? sdr.stats.iq : '--' }}</output></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sdr-frequency-row">
|
<div class="sdr-frequency-row">
|
||||||
<label for="sdr-frequency-input" data-i18n>FM MHZ</label>
|
<label for="sdr-frequency-input">{{ t('FM MHZ') }}</label>
|
||||||
<input ref="frequency" class="gev-field" id="sdr-frequency-input" @keydown.enter.prevent="tune" type="number" min="87.5" max="108" step="0.1" value="98.5" inputmode="decimal" :disabled="sdr?.frequencyDisabled" v-field-value="sdr ? sdr.frequency : '98.5'" />
|
<input ref="frequency" class="gev-field" id="sdr-frequency-input" @keydown.enter.prevent="tune" type="number" min="87.5" max="108" step="0.1" value="98.5" inputmode="decimal" :disabled="sdr?.frequencyDisabled" v-field-value="sdr ? sdr.frequency : '98.5'" />
|
||||||
<button id="sdr-tune-btn" type="button" @click="tune" :disabled="sdr?.tuneDisabled" data-i18n>TUNE</button>
|
<button id="sdr-tune-btn" type="button" @click="tune" :disabled="sdr?.tuneDisabled">{{ t('TUNE') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sdr-seek-row" role="group" aria-label="Seek broadcast FM stations" data-i18n-aria-label>
|
<div class="sdr-seek-row" role="group" :aria-label="t('Seek broadcast FM stations')">
|
||||||
<button id="sdr-seek-back-btn" type="button" @click="act('seekSdr', -1)" :disabled="sdr ? sdr.seekDisabled : true" data-i18n>◀ SEEK</button>
|
<button id="sdr-seek-back-btn" type="button" @click="act('seekSdr', -1)" :disabled="sdr ? sdr.seekDisabled : true">{{ t('◀ SEEK') }}</button>
|
||||||
<button id="sdr-seek-forward-btn" type="button" @click="act('seekSdr', 1)" :disabled="sdr ? sdr.seekDisabled : true" data-i18n>SEEK ▶</button>
|
<button id="sdr-seek-forward-btn" type="button" @click="act('seekSdr', 1)" :disabled="sdr ? sdr.seekDisabled : true">{{ t('SEEK ▶') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="radio-volume-row" for="sdr-volume">
|
<label class="radio-volume-row" for="sdr-volume">
|
||||||
<span data-i18n>SDR VOL</span>
|
<span>{{ t('SDR VOL') }}</span>
|
||||||
<UiSlider id="sdr-volume" hold @update:model-value="act('setSdrVolume', $event)" min="0" max="100" aria-label="Local SDR volume" :disabled="sdr?.volumeDisabled" :model-value="sdr ? sdr.volume : 80" data-i18n-aria-label />
|
<UiSlider id="sdr-volume" hold @update:model-value="act('setSdrVolume', $event)" min="0" max="100" :aria-label="t('Local SDR volume')" :disabled="sdr?.volumeDisabled" :model-value="sdr ? sdr.volume : 80" />
|
||||||
<output id="sdr-volume-value" class="gev-slider-value" for="sdr-volume">{{ sdr ? sdr.volumeText : '80%' }}</output>
|
<output id="sdr-volume-value" class="gev-slider-value" for="sdr-volume">{{ sdr ? sdr.volumeText : '80%' }}</output>
|
||||||
</label>
|
</label>
|
||||||
<div id="sdr-status" class="radio-playback-state" :class="{ error: sdr?.statusError }" role="status" aria-live="polite">{{ sdr ? sdr.statusText : t('Connect an RTL-SDR to begin.') }}</div>
|
<div id="sdr-status" class="radio-playback-state" :class="{ error: sdr?.statusError }" role="status" aria-live="polite">{{ sdr ? sdr.statusText : t('Connect an RTL-SDR to begin.') }}</div>
|
||||||
|
|||||||
@@ -135,39 +135,39 @@ const vThumb = {
|
|||||||
<div id="sl-viewer-wrap" class="sl-viewer-wrap" :hidden="!viewer.open">
|
<div id="sl-viewer-wrap" class="sl-viewer-wrap" :hidden="!viewer.open">
|
||||||
<div class="sl-viewer-bar">
|
<div class="sl-viewer-bar">
|
||||||
<div class="sl-viewer-tools">
|
<div class="sl-viewer-tools">
|
||||||
<UiButton id="sl-viewer-expand" class="scene-btn" :hidden="!expandAvailable" :pressed="Boolean(expanded)" :aria-label="expanded === null ? undefined : expanded ? t('Shrink') : t('Expand')" title="Show the street-level view full screen (Esc shrinks it)" data-i18n-title><span class="sl-btn-icon" aria-hidden="true">{{ expanded ? '⤡' : '⤢' }}</span><span class="sl-btn-text">{{ expanded ? t('SHRINK') : t('EXPAND') }}</span></UiButton>
|
<UiButton id="sl-viewer-expand" class="scene-btn" :hidden="!expandAvailable" :pressed="Boolean(expanded)" :aria-label="expanded === null ? undefined : expanded ? t('Shrink') : t('Expand')" :title="t('Show the street-level view full screen (Esc shrinks it)')"><span class="sl-btn-icon" aria-hidden="true">{{ expanded ? '⤡' : '⤢' }}</span><span class="sl-btn-text">{{ expanded ? t('SHRINK') : t('EXPAND') }}</span></UiButton>
|
||||||
<UiSegmented class="sl-seg" item-class="sl-seg-btn" active-class="is-active" keyboard="radio" :hidden="!viewer.renderModes" :label="t('Image fit')" :options="renderModeOptions" :model-value="viewer.renderMode" @update:model-value="store.act('setRenderMode', $event)" />
|
<UiSegmented class="sl-seg" item-class="sl-seg-btn" active-class="is-active" keyboard="radio" :hidden="!viewer.renderModes" :label="t('Image fit')" :options="renderModeOptions" :model-value="viewer.renderMode" @update:model-value="store.act('setRenderMode', $event)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sl-viewer-tools sl-viewer-tools-right">
|
<div class="sl-viewer-tools sl-viewer-tools-right">
|
||||||
<button id="sl-viewer-close" class="sl-viewer-x" type="button" title="Close the image and deselect it on the map" aria-label="Close the street-level image" data-i18n-title data-i18n-aria-label>×</button>
|
<button id="sl-viewer-close" class="sl-viewer-x" type="button" :title="t('Close the image and deselect it on the map')" :aria-label="t('Close the street-level image')">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sl-viewer-stage">
|
<div class="sl-viewer-stage">
|
||||||
<div id="sl-viewer-placeholder" class="sl-viewer-placeholder" :hidden="!viewer.loading"></div>
|
<div id="sl-viewer-placeholder" class="sl-viewer-placeholder" :hidden="!viewer.loading"></div>
|
||||||
<div id="sl-viewer" class="sl-viewer" aria-label="Street-level viewer" data-i18n-aria-label></div>
|
<div id="sl-viewer" class="sl-viewer" :aria-label="t('Street-level viewer')"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="sl-image-meta" class="sl-image-meta">
|
<div id="sl-image-meta" class="sl-image-meta">
|
||||||
<span id="sl-image-by" class="sl-image-by">{{ image.captionLeft }}</span>
|
<span id="sl-image-by" class="sl-image-by">{{ image.captionLeft }}</span>
|
||||||
<span class="sl-image-right">
|
<span class="sl-image-right">
|
||||||
<span id="sl-image-when" class="sl-image-when">{{ image.captionRight }}</span>
|
<span id="sl-image-when" class="sl-image-when">{{ image.captionRight }}</span>
|
||||||
<a id="sl-image-link" class="sl-image-link" :href="image.href" target="_blank" rel="noopener noreferrer" title="Open this image on the provider's site" data-i18n-title :hidden="!image.link">{{ image.linkLabel }}</a>
|
<a id="sl-image-link" class="sl-image-link" :href="image.href" target="_blank" rel="noopener noreferrer" :title="t('Open this image on the provider\'s site')" :hidden="!image.link">{{ image.linkLabel }}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sl-providers" role="group" aria-label="Imagery providers" data-i18n-aria-label>
|
<div class="sl-providers" role="group" :aria-label="t('Imagery providers')">
|
||||||
<span class="cctv-summary-label" data-i18n>PROVIDERS</span>
|
<span class="cctv-summary-label">{{ t('PROVIDERS') }}</span>
|
||||||
<div id="sl-provider-chips" class="sl-chips"><RailChips :chips="view ? view.providers : []" /></div>
|
<div id="sl-provider-chips" class="sl-chips"><RailChips :chips="view ? view.providers : []" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="sl-error" class="sl-error" role="alert" :hidden="!view?.error"><span aria-hidden="true">⚠</span> <span id="sl-error-text">{{ view?.error || '' }}</span></div>
|
<div id="sl-error" class="sl-error" role="alert" :hidden="!view?.error"><span aria-hidden="true">⚠</span> <span id="sl-error-text">{{ view?.error || '' }}</span></div>
|
||||||
<!-- A missing key gates the filters only: the chips above stay
|
<!-- A missing key gates the filters only: the chips above stay
|
||||||
focusable so their tooltip can say how to add the key. -->
|
focusable so their tooltip can say how to add the key. -->
|
||||||
<fieldset id="sl-controls" class="sl-controls" :disabled="Boolean(view?.controlsDisabled)" :hidden="!shows.pano && !shows.since">
|
<fieldset id="sl-controls" class="sl-controls" :disabled="Boolean(view?.controlsDisabled)" :hidden="!shows.pano && !shows.since">
|
||||||
<div id="sl-pano-row" class="sl-filters" :hidden="!shows.pano" role="group" aria-label="Imagery filters" data-i18n-aria-label>
|
<div id="sl-pano-row" class="sl-filters" :hidden="!shows.pano" role="group" :aria-label="t('Imagery filters')">
|
||||||
<span class="cctv-summary-label" data-i18n>IMAGERY</span>
|
<span class="cctv-summary-label">{{ t('IMAGERY') }}</span>
|
||||||
<UiSegmented class="sl-seg" item-class="sl-seg-btn" active-class="is-active" keyboard="radio" :disabled="Boolean(view?.controlsDisabled)" :label="t('Panorama filter')" :options="panoOptions" :model-value="pano" @update:model-value="store.act('setPano', $event)" />
|
<UiSegmented class="sl-seg" item-class="sl-seg-btn" active-class="is-active" keyboard="radio" :disabled="Boolean(view?.controlsDisabled)" :label="t('Panorama filter')" :options="panoOptions" :model-value="pano" @update:model-value="store.act('setPano', $event)" />
|
||||||
</div>
|
</div>
|
||||||
<div id="sl-since-row" class="sl-since" :hidden="!shows.since">
|
<div id="sl-since-row" class="sl-since" :hidden="!shows.since">
|
||||||
<label class="cctv-summary-label" for="sl-since" data-i18n>SINCE</label>
|
<label class="cctv-summary-label" for="sl-since">{{ t('SINCE') }}</label>
|
||||||
<UiSlider id="sl-since" v-thumb="view?.since" class="sl-range" min="0" max="8" step="1" :aria-valuetext="sinceLabel" aria-describedby="sl-since-label sl-since-note" @input="onSinceInput" @change="onSinceChange" />
|
<UiSlider id="sl-since" v-thumb="view?.since" class="sl-range" min="0" max="8" step="1" :aria-valuetext="sinceLabel" aria-describedby="sl-since-label sl-since-note" @input="onSinceInput" @change="onSinceChange" />
|
||||||
<span class="sl-since-value">
|
<span class="sl-since-value">
|
||||||
<output id="sl-since-label" class="sl-since-label" for="sl-since">{{ sinceLabel }}</output>
|
<output id="sl-since-label" class="sl-since-label" for="sl-since">{{ sinceLabel }}</output>
|
||||||
@@ -178,7 +178,7 @@ const vThumb = {
|
|||||||
<!-- The hint says how to open imagery; the button does it without a pointer. -->
|
<!-- The hint says how to open imagery; the button does it without a pointer. -->
|
||||||
<div class="sl-meta-row">
|
<div class="sl-meta-row">
|
||||||
<div id="sl-coverage-meta" class="sl-meta">{{ view?.meta ?? '' }}</div>
|
<div id="sl-coverage-meta" class="sl-meta">{{ view?.meta ?? '' }}</div>
|
||||||
<UiButton id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" :hidden="!view?.nearest.visible" :title="nearest.title || undefined" :disabled="nearest.disabled" data-i18n-runtime>{{ nearest.label }}</UiButton>
|
<UiButton id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" :hidden="!view?.nearest.visible" :title="nearest.title || undefined" :disabled="nearest.disabled">{{ nearest.label }}</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RailPanel>
|
</RailPanel>
|
||||||
|
|||||||
Reference in new issue
Block a user