feat(foreground): track topmost process + browser page title
Lint & Test / test (push) Failing after 8s
Lint & Test / test (push) Failing after 8s
Adds cross-platform foreground-window tracking and exposes it over REST (/api/foreground) and the existing WebSocket feed. - foreground_service.py: Windows probe via ctypes (HANDLE-correct argtypes to avoid 64-bit handle truncation); macOS via AppKit; Linux via Xlib (Wayland returns unavailable). TTL cache + per-platform fallback. - browser_url_service.py: when foreground is a recognised browser, extract the page title from the window title (browser-name suffix stripped) and surface `is_browser` + `browser_page_title`. Optional UIA-based URL extraction behind MEDIA_SERVER_BROWSER_UIA env flag (off by default — Chromium browsers keep their accessibility tree dormant otherwise). - websocket_manager: poll foreground every 1s inside the existing status loop, broadcast `foreground` on connect and `foreground_update` on change. Diff only on user-visible fields to avoid geometry spam. - WebUI: new editorial card rendered under the monitor list on the Display tab — process name, window title, fullscreen/minimized/monitor chips, browser block when applicable, exe path, PID, started-ago, geometry, platform. 16px inter-section gap matches Settings cadence. - i18n: 25 new keys added to both en.json and ru.json. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9321,3 +9321,321 @@ body.is-fullscreen-player .now-playing .vu-meter {
|
||||
body.is-fullscreen-player .fs-bloom #fs-bloom-art { animation: none !important; }
|
||||
:root[data-theme="light"] body.is-fullscreen-player .fs-bloom { opacity: 0.22; }
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════
|
||||
FOREGROUND container — editorial process plate
|
||||
════════════════════════════════════════════════════════════════ */
|
||||
.foreground-container {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.foreground-stage {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
/* Match the inter-section gap used between .settings-section blocks
|
||||
in the Settings tab — keeps cadence consistent across tabs. */
|
||||
.display-container > * + * {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.foreground-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: clamp(24px, 3vw, 40px) clamp(24px, 3vw, 40px) 28px;
|
||||
border: 1px solid var(--rule);
|
||||
border-top: 2px solid var(--copper);
|
||||
background:
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(var(--copper-rgb), 0.05), transparent 60%),
|
||||
var(--bg-paper);
|
||||
box-shadow:
|
||||
0 1px 0 var(--bg-paper),
|
||||
0 28px 60px -28px rgba(0, 0, 0, 0.45),
|
||||
0 8px 20px -10px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.foreground-card[data-fullscreen="1"] {
|
||||
border-top-color: var(--copper-hi);
|
||||
box-shadow:
|
||||
0 1px 0 var(--bg-paper),
|
||||
0 28px 60px -28px rgba(0, 0, 0, 0.55),
|
||||
0 0 0 1px rgba(var(--copper-rgb), 0.18),
|
||||
0 0 60px -12px var(--copper-glow);
|
||||
}
|
||||
|
||||
.foreground-card .fg-kicker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.32em;
|
||||
text-transform: uppercase;
|
||||
color: var(--copper);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.foreground-card .fg-kicker::before,
|
||||
.foreground-card .fg-kicker::after {
|
||||
content: "";
|
||||
height: 1px;
|
||||
background: var(--copper);
|
||||
opacity: 0.6;
|
||||
flex: 0 0 24px;
|
||||
}
|
||||
.foreground-card .fg-kicker::after { flex: 1 0 auto; }
|
||||
|
||||
.foreground-card .fg-process {
|
||||
font-family: var(--serif);
|
||||
font-weight: 400;
|
||||
font-size: clamp(34px, 4.4vw, 56px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.02em;
|
||||
font-variation-settings: 'opsz' 144;
|
||||
color: var(--ink);
|
||||
margin: 0 0 10px;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
transition: color 180ms var(--ease, ease);
|
||||
}
|
||||
.foreground-card .fg-process:hover {
|
||||
color: var(--copper-hi);
|
||||
}
|
||||
|
||||
.foreground-card .fg-window-title {
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
color: var(--ink-soft);
|
||||
font-variation-settings: 'opsz' 60;
|
||||
margin-bottom: 22px;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
.foreground-card .fg-window-title:empty { display: none; }
|
||||
|
||||
.foreground-card .fg-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.foreground-card .fg-chips:empty { display: none; }
|
||||
|
||||
.fg-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 11px;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-soft);
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule-strong);
|
||||
border-radius: 999px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fg-chip.fg-chip-accent {
|
||||
color: var(--copper);
|
||||
border-color: var(--copper);
|
||||
background: rgba(var(--copper-rgb), 0.07);
|
||||
}
|
||||
.fg-chip.fg-chip-mute {
|
||||
color: var(--ink-mute);
|
||||
border-color: var(--rule);
|
||||
}
|
||||
|
||||
.foreground-card .fg-details {
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.foreground-card .fg-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 220px) 1fr;
|
||||
gap: 24px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
.foreground-card .fg-row dt {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--copper);
|
||||
margin: 0;
|
||||
}
|
||||
.foreground-card .fg-row dd {
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
color: var(--ink);
|
||||
font-variation-settings: 'opsz' 30;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.foreground-card .fg-mono {
|
||||
font-family: var(--mono);
|
||||
font-style: normal;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink-soft);
|
||||
font-variant-numeric: tabular-nums;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.foreground-empty {
|
||||
padding: 60px 24px;
|
||||
text-align: center;
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
.foreground-empty svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 14px;
|
||||
opacity: 0.55;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.foreground-empty p {
|
||||
font-family: var(--serif);
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
color: var(--ink-soft);
|
||||
margin: 0;
|
||||
}
|
||||
.foreground-empty .foreground-empty-error {
|
||||
margin-top: 10px;
|
||||
font-family: var(--mono);
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--ink-mute);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ─── Header status badge ──────────────────────────────────── */
|
||||
.foreground-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 0 12px 0 10px;
|
||||
margin-right: 4px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--ink-soft);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
max-width: 240px;
|
||||
transition: color 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
}
|
||||
.foreground-status-badge:hover {
|
||||
color: var(--ink);
|
||||
border-color: var(--copper);
|
||||
background: rgba(var(--copper-rgb), 0.06);
|
||||
}
|
||||
.foreground-status-badge.hidden { display: none !important; }
|
||||
|
||||
.foreground-status-badge .fg-badge-mark {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--ink-mute);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.foreground-status-badge.is-media .fg-badge-mark,
|
||||
.foreground-status-badge.is-fullscreen .fg-badge-mark {
|
||||
background: var(--copper);
|
||||
box-shadow: 0 0 8px var(--copper-glow);
|
||||
}
|
||||
.foreground-status-badge.is-fullscreen {
|
||||
border-color: var(--copper);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.foreground-status-badge .fg-badge-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 140px;
|
||||
}
|
||||
.foreground-status-badge .fg-badge-tag {
|
||||
color: var(--copper);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.foreground-status-badge .fg-badge-tag.hidden { display: none; }
|
||||
|
||||
/* ─── Light theme overrides ──────────────────────────────── */
|
||||
:root[data-theme="light"] .foreground-card {
|
||||
background:
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(var(--copper-rgb), 0.05), transparent 60%),
|
||||
var(--bg-paper);
|
||||
box-shadow:
|
||||
0 1px 0 var(--bg-paper),
|
||||
0 22px 50px -24px rgba(26, 23, 21, 0.20),
|
||||
0 6px 16px -8px rgba(26, 23, 21, 0.12);
|
||||
}
|
||||
:root[data-theme="light"] .foreground-card[data-fullscreen="1"] {
|
||||
box-shadow:
|
||||
0 1px 0 var(--bg-paper),
|
||||
0 22px 50px -24px rgba(26, 23, 21, 0.28),
|
||||
0 0 0 1px rgba(var(--copper-rgb), 0.20),
|
||||
0 0 50px -12px var(--copper-glow);
|
||||
}
|
||||
:root[data-theme="light"] .foreground-status-badge {
|
||||
border-color: rgba(26, 23, 21, 0.18);
|
||||
}
|
||||
:root[data-theme="light"] .foreground-status-badge:hover {
|
||||
background: rgba(var(--copper-rgb), 0.08);
|
||||
}
|
||||
|
||||
/* ─── Mobile breakpoint ──────────────────────────────────── */
|
||||
@media (max-width: 720px) {
|
||||
.foreground-card {
|
||||
padding: 22px 18px 20px;
|
||||
}
|
||||
.foreground-card .fg-process {
|
||||
font-size: 30px;
|
||||
}
|
||||
.foreground-card .fg-window-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
.foreground-card .fg-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.foreground-card .fg-row dd {
|
||||
font-size: 16px;
|
||||
}
|
||||
.foreground-status-badge {
|
||||
max-width: 160px;
|
||||
}
|
||||
.foreground-status-badge .fg-badge-name {
|
||||
max-width: 80px;
|
||||
}
|
||||
.foreground-status-badge .fg-badge-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +535,7 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Display Control Section -->
|
||||
<!-- Display Control Section (monitors first, foreground overview below) -->
|
||||
<div class="display-container" data-tab-content="display" role="tabpanel" id="panel-display">
|
||||
<div class="display-monitors" id="displayMonitors">
|
||||
<div class="empty-state-illustration">
|
||||
@@ -543,6 +543,12 @@
|
||||
<p data-i18n="display.loading">Loading monitors...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foreground-stage" id="foregroundStage">
|
||||
<div class="empty-state-illustration">
|
||||
<svg viewBox="0 0 64 64"><rect x="6" y="12" width="52" height="36" rx="3" fill="none" stroke="currentColor" stroke-width="2"/><line x1="6" y1="20" x2="58" y2="20" stroke="currentColor" stroke-width="2"/><circle cx="11" cy="16" r="1.4" fill="currentColor"/><circle cx="15" cy="16" r="1.4" fill="currentColor"/><circle cx="19" cy="16" r="1.4" fill="currentColor"/></svg>
|
||||
<p data-i18n="foreground.loading">Waiting for foreground signal…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -74,6 +74,10 @@ import {
|
||||
toggleDynamicBackground, applyDynamicBackground, updateBackgroundColors,
|
||||
} from './background.js';
|
||||
|
||||
import {
|
||||
updateForegroundUI, loadForegroundProcess,
|
||||
} from './foreground.js';
|
||||
|
||||
// ============================================================
|
||||
// Register late-bound callbacks for core's updateAllText()
|
||||
// ============================================================
|
||||
@@ -136,6 +140,8 @@ Object.assign(window, {
|
||||
onAudioDeviceChanged,
|
||||
// About
|
||||
showAboutDialog, closeAboutDialog,
|
||||
// Foreground
|
||||
loadForegroundProcess,
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// ============================================================
|
||||
// Foreground: Currently-focused desktop process card (rendered at
|
||||
// the top of the Display tab)
|
||||
// ============================================================
|
||||
|
||||
import { t } from './core.js';
|
||||
|
||||
let latestForeground = null;
|
||||
let agoTickTimer = null;
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (s === null || s === undefined) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatAgo(epoch) {
|
||||
if (!epoch) return '';
|
||||
const now = Date.now() / 1000;
|
||||
const diff = Math.max(0, now - epoch);
|
||||
if (diff < 60) {
|
||||
return t('foreground.ago.seconds', { n: Math.floor(diff) });
|
||||
}
|
||||
if (diff < 3600) {
|
||||
return t('foreground.ago.minutes', { n: Math.floor(diff / 60) });
|
||||
}
|
||||
if (diff < 86400) {
|
||||
const h = Math.floor(diff / 3600);
|
||||
const m = Math.floor((diff % 3600) / 60);
|
||||
return t('foreground.ago.hours', { n: h, m: m });
|
||||
}
|
||||
return t('foreground.ago.days', { n: Math.floor(diff / 86400) });
|
||||
}
|
||||
|
||||
function formatGeometry(g) {
|
||||
if (!g) return '—';
|
||||
const w = g.width ?? (g.right - g.left);
|
||||
const h = g.height ?? (g.bottom - g.top);
|
||||
return `${w}×${h} @ (${g.left}, ${g.top})`;
|
||||
}
|
||||
|
||||
function truncatePath(p, max = 64) {
|
||||
if (!p) return '';
|
||||
if (p.length <= max) return p;
|
||||
// Keep the tail (filename) visible — that's the part the user cares about.
|
||||
return '…' + p.slice(-(max - 1));
|
||||
}
|
||||
|
||||
function renderEmpty(message, errorMsg) {
|
||||
const stage = document.getElementById('foregroundStage');
|
||||
if (!stage) return;
|
||||
stage.innerHTML = `
|
||||
<div class="empty-state-illustration foreground-empty">
|
||||
<svg viewBox="0 0 64 64"><rect x="6" y="12" width="52" height="36" rx="3" fill="none" stroke="currentColor" stroke-width="2"/><line x1="6" y1="20" x2="58" y2="20" stroke="currentColor" stroke-width="2"/><circle cx="11" cy="16" r="1.4" fill="currentColor"/><circle cx="15" cy="16" r="1.4" fill="currentColor"/><circle cx="19" cy="16" r="1.4" fill="currentColor"/></svg>
|
||||
<p>${escapeHtml(message)}</p>
|
||||
${errorMsg ? `<p class="foreground-empty-error">${escapeHtml(errorMsg)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTile(data) {
|
||||
const stage = document.getElementById('foregroundStage');
|
||||
if (!stage) return;
|
||||
|
||||
const procName = data.process_name || '—';
|
||||
const winTitle = data.window_title || '';
|
||||
const execPath = data.executable_path || '';
|
||||
const pid = data.pid ?? '—';
|
||||
const startedEpoch = data.started_at;
|
||||
const startedAgo = startedEpoch ? formatAgo(startedEpoch) : '—';
|
||||
const startedAbs = startedEpoch
|
||||
? new Date(startedEpoch * 1000).toLocaleString()
|
||||
: '';
|
||||
const geom = formatGeometry(data.window_geometry);
|
||||
const platform = data.platform || '—';
|
||||
const monitorId = data.monitor_id;
|
||||
|
||||
// Chips: only render ones that apply
|
||||
const chips = [];
|
||||
if (data.is_fullscreen) {
|
||||
chips.push(`<span class="fg-chip fg-chip-accent">${escapeHtml(t('foreground.fullscreen'))}</span>`);
|
||||
} else if (!data.is_minimized) {
|
||||
chips.push(`<span class="fg-chip">${escapeHtml(t('foreground.windowed'))}</span>`);
|
||||
}
|
||||
if (data.is_minimized) {
|
||||
chips.push(`<span class="fg-chip fg-chip-mute">${escapeHtml(t('foreground.minimized'))}</span>`);
|
||||
}
|
||||
if (monitorId !== null && monitorId !== undefined) {
|
||||
chips.push(`<span class="fg-chip">${escapeHtml(t('foreground.monitor', { n: monitorId + 1 }))}</span>`);
|
||||
}
|
||||
if (data.is_browser) {
|
||||
chips.push(`<span class="fg-chip fg-chip-mute">${escapeHtml(t('foreground.browser'))}</span>`);
|
||||
}
|
||||
|
||||
// Optional browser-only detail rows (page title + URL when available)
|
||||
const browserRows = [];
|
||||
if (data.is_browser) {
|
||||
if (data.browser_page_title) {
|
||||
browserRows.push(`
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.page_title'))}</dt>
|
||||
<dd title="${escapeHtml(data.browser_page_title)}">${escapeHtml(data.browser_page_title)}</dd>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
if (data.browser_url) {
|
||||
browserRows.push(`
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.url'))}</dt>
|
||||
<dd title="${escapeHtml(data.browser_url)}"><span class="fg-mono">${escapeHtml(truncatePath(data.browser_url, 80))}</span></dd>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
stage.innerHTML = `
|
||||
<article class="foreground-card" data-fullscreen="${data.is_fullscreen ? '1' : '0'}">
|
||||
<div class="fg-kicker">
|
||||
<span data-i18n="foreground.kicker">Foreground</span>
|
||||
</div>
|
||||
<h1 class="fg-process" title="${escapeHtml(procName)}">${escapeHtml(procName)}</h1>
|
||||
<div class="fg-window-title" title="${escapeHtml(winTitle)}">${escapeHtml(winTitle)}</div>
|
||||
|
||||
<div class="fg-chips">${chips.join('')}</div>
|
||||
|
||||
<dl class="fg-details">
|
||||
${browserRows.join('')}
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.executable'))}</dt>
|
||||
<dd title="${escapeHtml(execPath)}"><span class="fg-mono">${escapeHtml(truncatePath(execPath))}</span></dd>
|
||||
</div>
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.pid'))}</dt>
|
||||
<dd><span class="fg-mono">${escapeHtml(String(pid))}</span></dd>
|
||||
</div>
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.started'))}</dt>
|
||||
<dd title="${escapeHtml(startedAbs)}"><span class="fg-ago" data-started="${startedEpoch ?? ''}">${escapeHtml(startedAgo)}</span></dd>
|
||||
</div>
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.geometry'))}</dt>
|
||||
<dd><span class="fg-mono">${escapeHtml(geom)}</span></dd>
|
||||
</div>
|
||||
<div class="fg-row">
|
||||
<dt>${escapeHtml(t('foreground.platform'))}</dt>
|
||||
<dd>${escapeHtml(platform)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function startAgoTicker() {
|
||||
if (agoTickTimer) return;
|
||||
agoTickTimer = setInterval(() => {
|
||||
const el = document.querySelector('.fg-ago[data-started]');
|
||||
if (!el) return;
|
||||
const epoch = parseFloat(el.getAttribute('data-started'));
|
||||
if (!epoch) return;
|
||||
el.textContent = formatAgo(epoch);
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
export function updateForegroundUI(data) {
|
||||
latestForeground = data;
|
||||
|
||||
if (!data || data.available === false) {
|
||||
const errMsg = data && data.error ? data.error : '';
|
||||
renderEmpty(t('foreground.unavailable'), errMsg);
|
||||
} else if (!data.process_name && !data.pid) {
|
||||
renderEmpty(t('foreground.no_process'), '');
|
||||
} else {
|
||||
renderTile(data);
|
||||
startAgoTicker();
|
||||
}
|
||||
}
|
||||
|
||||
export function loadForegroundProcess() {
|
||||
// Push-only — just render the cached state. If nothing has arrived
|
||||
// yet, leave the loading placeholder visible.
|
||||
if (latestForeground !== null) {
|
||||
updateForegroundUI(latestForeground);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from './core.js';
|
||||
import { updateBackgroundColors } from './background.js';
|
||||
import { loadDisplayMonitors } from './links.js';
|
||||
import { loadForegroundProcess } from './foreground.js';
|
||||
import { IconSelect } from './icon-select.js';
|
||||
|
||||
// Tab management
|
||||
@@ -75,6 +76,7 @@ export function switchTab(tabName) {
|
||||
|
||||
if (tabName === 'display') {
|
||||
loadDisplayMonitors();
|
||||
loadForegroundProcess();
|
||||
}
|
||||
|
||||
localStorage.setItem('activeTab', tabName);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { updateUI, setFrequencyData, stopPositionInterpolation, loadAudioDevices
|
||||
import { loadScripts, loadScriptsTable, displayQuickAccess } from './scripts.js';
|
||||
import { loadCallbacksTable } from './callbacks.js';
|
||||
import { loadHeaderLinks, loadLinksTable } from './links.js';
|
||||
import { updateForegroundUI } from './foreground.js';
|
||||
|
||||
let reconnectTimeout = null;
|
||||
let wsReconnectAttempts = 0;
|
||||
@@ -118,6 +119,8 @@ export function connectWebSocket(token) {
|
||||
|
||||
if (msg.type === 'status' || msg.type === 'status_update') {
|
||||
updateUI(msg.data);
|
||||
} else if (msg.type === 'foreground' || msg.type === 'foreground_update') {
|
||||
updateForegroundUI(msg.data);
|
||||
} else if (msg.type === 'scripts_changed') {
|
||||
console.log('Scripts changed, reloading...');
|
||||
loadScripts();
|
||||
|
||||
@@ -292,5 +292,29 @@
|
||||
"about.source_code": "Source Code",
|
||||
"dialog.close": "Close",
|
||||
"update.available": "Update available: v{version}",
|
||||
"update.view_release": "View Release"
|
||||
"update.view_release": "View Release",
|
||||
"tab.foreground": "Foreground",
|
||||
"foreground.kicker": "Foreground",
|
||||
"foreground.loading": "Waiting for foreground signal…",
|
||||
"foreground.no_process": "No foreground process",
|
||||
"foreground.unavailable": "Foreground tracking unavailable on this platform",
|
||||
"foreground.process": "Process",
|
||||
"foreground.window_title": "Window title",
|
||||
"foreground.executable": "Executable",
|
||||
"foreground.pid": "PID",
|
||||
"foreground.monitor": "Monitor {n}",
|
||||
"foreground.started": "Started",
|
||||
"foreground.geometry": "Geometry",
|
||||
"foreground.platform": "Platform",
|
||||
"foreground.fullscreen": "Fullscreen",
|
||||
"foreground.minimized": "Minimized",
|
||||
"foreground.windowed": "Windowed",
|
||||
"foreground.browser": "Browser",
|
||||
"foreground.page_title": "Page title",
|
||||
"foreground.url": "URL",
|
||||
"foreground.badge.title": "View foreground process",
|
||||
"foreground.ago.seconds": "{n}s ago",
|
||||
"foreground.ago.minutes": "{n}m ago",
|
||||
"foreground.ago.hours": "{n}h {m}m ago",
|
||||
"foreground.ago.days": "{n}d ago"
|
||||
}
|
||||
|
||||
@@ -292,5 +292,29 @@
|
||||
"about.source_code": "Исходный код",
|
||||
"dialog.close": "Закрыть",
|
||||
"update.available": "Доступно обновление: v{version}",
|
||||
"update.view_release": "Перейти к релизу"
|
||||
"update.view_release": "Перейти к релизу",
|
||||
"tab.foreground": "Активное окно",
|
||||
"foreground.kicker": "Активное окно",
|
||||
"foreground.loading": "Ожидание сигнала об активном окне…",
|
||||
"foreground.no_process": "Активное окно не определено",
|
||||
"foreground.unavailable": "Отслеживание активного окна недоступно",
|
||||
"foreground.process": "Процесс",
|
||||
"foreground.window_title": "Заголовок окна",
|
||||
"foreground.executable": "Путь к программе",
|
||||
"foreground.pid": "PID",
|
||||
"foreground.monitor": "Монитор {n}",
|
||||
"foreground.started": "Запущено",
|
||||
"foreground.geometry": "Геометрия",
|
||||
"foreground.platform": "Платформа",
|
||||
"foreground.fullscreen": "Полноэкранный",
|
||||
"foreground.minimized": "Свёрнут",
|
||||
"foreground.windowed": "Оконный",
|
||||
"foreground.browser": "Браузер",
|
||||
"foreground.page_title": "Заголовок страницы",
|
||||
"foreground.url": "URL",
|
||||
"foreground.badge.title": "Открыть активное окно",
|
||||
"foreground.ago.seconds": "{n} с назад",
|
||||
"foreground.ago.minutes": "{n} мин назад",
|
||||
"foreground.ago.hours": "{n} ч {m} мин назад",
|
||||
"foreground.ago.days": "{n} дн назад"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user