release: bump version to 0.60.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled

This commit is contained in:
rayd1o
2026-05-17 02:50:42 +08:00
parent 9b913a3b83
commit 81970a1d05
43 changed files with 3378 additions and 463 deletions

View File

@@ -1290,6 +1290,53 @@
gap: 10px;
}
.earth-mobile-settings-tabs {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 2px;
scrollbar-width: none;
}
.earth-mobile-settings-tabs::-webkit-scrollbar {
display: none;
}
.earth-mobile-settings-tab {
flex: 0 0 auto;
min-width: 52px;
border: 1px solid rgba(212, 227, 244, 0.1);
border-radius: 999px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
rgba(255, 255, 255, 0.025);
color: var(--hud-text-soft);
padding: 9px 14px;
font: inherit;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
transition:
background 0.18s ease,
border-color 0.18s ease,
color 0.18s ease,
transform 0.18s ease;
}
.earth-mobile-settings-tab:hover {
color: var(--hud-text);
transform: translateY(-1px);
}
.earth-mobile-settings-tab.is-active {
color: var(--hud-title);
border-color: rgba(122, 180, 255, 0.28);
background:
radial-gradient(circle at 35% 22%, rgba(255, 255, 255, 0.18), transparent 58%),
linear-gradient(180deg, rgba(122, 180, 255, 0.18), rgba(82, 123, 186, 0.24));
}
.earth-mobile-settings-card {
display: flex;
align-items: center;
@@ -2164,6 +2211,52 @@ label.is-disabled.earth-mobile-settings-card {
gap: var(--hud-gap-md);
}
.earth-settings-tabs {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: calc(6px * var(--settings-scale));
padding: calc(2px * var(--settings-scale)) 0 calc(4px * var(--settings-scale));
}
.earth-settings-tab {
min-width: 0;
border: 1px solid rgba(212, 227, 244, 0.1);
border-radius: 999px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent),
rgba(255, 255, 255, 0.025);
color: var(--hud-text-soft);
padding: calc(7px * var(--settings-scale)) calc(10px * var(--settings-scale));
font: inherit;
font-size: calc(0.68rem * var(--hud-scale));
font-weight: 700;
letter-spacing: 0.04em;
cursor: pointer;
transition:
background 0.18s ease,
border-color 0.18s ease,
color 0.18s ease,
transform 0.18s ease;
}
.earth-settings-tab:hover {
color: var(--hud-text);
transform: translateY(-1px);
}
.earth-settings-tab.is-active {
color: var(--hud-title);
border-color: rgba(122, 180, 255, 0.28);
background:
radial-gradient(circle at 35% 22%, rgba(255, 255, 255, 0.2), transparent 58%),
linear-gradient(180deg, rgba(121, 159, 207, 0.22), rgba(72, 101, 139, 0.28));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 8px 18px rgba(0, 0, 0, 0.16);
}
.earth-settings-kicker {
color: var(--hud-text-soft);
font-size: calc(0.72rem * var(--hud-scale));
@@ -2193,6 +2286,8 @@ label.is-disabled.earth-mobile-settings-card {
}
.earth-settings-content {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding-inline: 8px;
padding-right: 10px;
@@ -2656,6 +2751,154 @@ label.is-disabled.earth-mobile-settings-card {
pointer-events: none;
}
.earth-shortcut-list {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.earth-shortcut-category {
margin-top: 4px;
color: var(--hud-text-soft);
font-size: calc(0.62rem * var(--hud-scale));
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.earth-shortcut-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto auto;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 1px solid rgba(212, 227, 244, 0.08);
background: rgba(255, 255, 255, 0.025);
}
.earth-shortcut-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 3px;
}
.earth-shortcut-label {
color: var(--hud-text);
font-size: calc(0.72rem * var(--hud-scale));
font-weight: 700;
}
.earth-shortcut-alias {
color: var(--hud-text-muted);
font-size: calc(0.62rem * var(--hud-scale));
line-height: 1.3;
}
.earth-shortcut-key {
min-width: calc(68px * var(--hud-scale));
border: 1px solid rgba(122, 180, 255, 0.24);
border-radius: calc(6px * var(--hud-scale));
background:
radial-gradient(circle at 35% 22%, rgba(255, 255, 255, 0.16), transparent 58%),
linear-gradient(180deg, rgba(121, 159, 207, 0.16), rgba(72, 101, 139, 0.2));
color: var(--hud-title);
padding: calc(6px * var(--hud-scale)) calc(10px * var(--hud-scale));
font: inherit;
font-size: calc(0.68rem * var(--hud-scale));
font-weight: 800;
cursor: pointer;
}
.earth-shortcut-key.is-capturing {
border-color: rgba(110, 226, 188, 0.52);
color: #d8fff3;
}
.earth-shortcut-reset {
display: inline-flex;
align-items: center;
justify-content: center;
width: calc(28px * var(--hud-scale));
height: calc(28px * var(--hud-scale));
border: 1px solid rgba(212, 227, 244, 0.1);
border-radius: 999px;
background: rgba(255, 255, 255, 0.035);
color: var(--hud-text-soft);
cursor: pointer;
}
.earth-shortcut-enable {
position: relative;
display: inline-flex;
width: calc(34px * var(--hud-scale));
height: calc(20px * var(--hud-scale));
cursor: pointer;
}
.earth-shortcut-enable input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.earth-shortcut-enable .earth-settings-switch-track {
width: 100%;
height: 100%;
}
.earth-shortcut-enable .earth-settings-switch-track::after {
width: calc(14px * var(--hud-scale));
height: calc(14px * var(--hud-scale));
}
.earth-shortcut-enable input:checked + .earth-settings-switch-track {
background: linear-gradient(180deg, rgba(143, 185, 255, 0.72), rgba(104, 147, 221, 0.78));
border-color: rgba(223, 236, 252, 0.28);
}
.earth-shortcut-enable input:checked + .earth-settings-switch-track::after {
transform: translate(calc(14px * var(--hud-scale)), -50%);
}
.earth-shortcut-reset .material-symbols-rounded {
font-size: calc(0.9rem * var(--hud-scale));
}
.earth-settings-shortcut-actions {
display: flex;
justify-content: flex-start;
}
.earth-mobile-settings-card .earth-shortcut-category {
font-size: 0.68rem;
}
.earth-mobile-settings-card .earth-shortcut-row {
grid-template-columns: minmax(0, 1fr) auto auto auto;
padding: 10px;
}
.earth-mobile-settings-card .earth-shortcut-label {
font-size: 0.82rem;
}
.earth-mobile-settings-card .earth-shortcut-alias {
font-size: 0.72rem;
}
.earth-mobile-settings-card .earth-shortcut-key {
min-width: 70px;
padding: 8px 10px;
font-size: 0.78rem;
}
.earth-mobile-settings-card .earth-shortcut-reset {
width: 32px;
height: 32px;
}
.hud-panel-motion-debug {
position: absolute;
right: calc(24px * var(--hud-scale));

View File

@@ -92,6 +92,16 @@
object-fit: contain;
}
.hud-panel-brand .earth-brand__title-text {
color: var(--hud-text);
font-size: calc(1rem * var(--hud-scale) * var(--brand-scale));
line-height: 1.18;
font-weight: 700;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.hud-panel-brand .earth-brand__meta {
display: flex;
flex-direction: column;

View File

@@ -849,8 +849,16 @@
<span class="earth-mobile-page-kicker">Settings</span>
<span class="earth-mobile-page-summary">仅保留移动端仍有意义的 Earth 配置</span>
</div>
<div class="earth-mobile-settings-group">
<div class="earth-mobile-settings-title">旋转</div>
<div class="earth-mobile-settings-tabs" role="tablist" aria-label="移动端设置分类">
<button type="button" class="earth-mobile-settings-tab is-active" data-settings-tab="runtime" aria-selected="true">运行</button>
<button type="button" class="earth-mobile-settings-tab" data-settings-tab="display" aria-selected="false">显示</button>
<button type="button" class="earth-mobile-settings-tab" data-settings-tab="panels" aria-selected="false">面板</button>
<button type="button" class="earth-mobile-settings-tab" data-settings-tab="motion" aria-selected="false">动捕</button>
<button type="button" class="earth-mobile-settings-tab" data-settings-tab="shortcuts" aria-selected="false">快捷键</button>
<button type="button" class="earth-mobile-settings-tab" data-settings-tab="system" aria-selected="false">系统</button>
</div>
<div class="earth-mobile-settings-group" data-settings-tab-panel="runtime">
<div class="earth-mobile-settings-title">运行</div>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">旋转模式</span>
@@ -877,8 +885,8 @@
</div>
</div>
</div>
<div class="earth-mobile-settings-group">
<div class="earth-mobile-settings-title">视图</div>
<div class="earth-mobile-settings-group" data-settings-tab-panel="display" hidden>
<div class="earth-mobile-settings-title">显示</div>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">卫星显示风格</span>
@@ -950,10 +958,115 @@
<span class="earth-mobile-settings-switch-track"></span>
</span>
</label>
<label
class="earth-mobile-settings-card"
data-gatekeeper-permission="earth.motion_debug"
>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">地球默认大小</span>
<span class="earth-mobile-settings-subtitle">用于重置视角、缩放重置和巡航视图</span>
</div>
<div class="earth-mobile-settings-slider-row">
<input
class="earth-mobile-settings-slider"
type="range"
min="0.5"
max="5"
step="0.01"
value="1"
data-default-earth-size-slider
aria-label="移动端调整地球默认大小"
>
<span class="earth-mobile-settings-slider-value" data-default-earth-size-value>100%</span>
</div>
</div>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">地形透明度</span>
<span class="earth-mobile-settings-subtitle">调高后会呈现更明显的绿色地形覆盖效果</span>
</div>
<div class="earth-mobile-settings-slider-row">
<input
class="earth-mobile-settings-slider"
type="range"
min="0.05"
max="1"
step="0.01"
value="0.62"
data-terrain-opacity-slider
aria-label="移动端调整地形透明度"
>
<span class="earth-mobile-settings-slider-value" data-terrain-opacity-value>62%</span>
</div>
</div>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label" data-boundary-precision-status>国界精度</span>
<span class="earth-mobile-settings-subtitle" data-boundary-precision-detail>正在读取高清国界状态...</span>
</div>
<div class="earth-boundary-progress" data-boundary-precision-progress-wrap hidden>
<div class="earth-boundary-progress__track">
<div class="earth-boundary-progress__bar" data-boundary-precision-progress-bar></div>
</div>
<span class="earth-boundary-progress__value" data-boundary-precision-progress-value>0%</span>
</div>
<div class="earth-settings-boundary-action-row">
<div class="earth-mobile-settings-segmented earth-mobile-settings-segmented--boundary" role="group" aria-label="移动端选择国界精度">
<button type="button" class="earth-mobile-settings-pill is-active" data-boundary-precision-disable aria-pressed="true">低精</button>
<button type="button" class="earth-mobile-settings-pill" data-boundary-precision-build aria-pressed="false">高精</button>
</div>
<button type="button" class="earth-settings-reload-action" data-boundary-precision-rebuild aria-label="重新获取并构建高清国界" title="重新获取并构建" hidden disabled>
<span class="material-symbols-rounded">refresh</span>
</button>
</div>
</div>
</div>
<div class="earth-mobile-settings-group" data-settings-tab-panel="panels" hidden>
<div class="earth-mobile-settings-title">面板</div>
<label class="earth-mobile-settings-card">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">图层控制</span>
<span class="earth-mobile-settings-subtitle">控制图层控制面板显示</span>
</div>
<span class="earth-mobile-settings-switch">
<input type="checkbox" data-settings-panel="layer-toggles" checked>
<span class="earth-mobile-settings-switch-track"></span>
</span>
</label>
<label class="earth-mobile-settings-card">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">图例</span>
<span class="earth-mobile-settings-subtitle">控制图例面板显示</span>
</div>
<span class="earth-mobile-settings-switch">
<input type="checkbox" data-settings-panel="legend">
<span class="earth-mobile-settings-switch-track"></span>
</span>
</label>
<label class="earth-mobile-settings-card">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">全球态势</span>
<span class="earth-mobile-settings-subtitle">控制全球态势统计面板显示</span>
</div>
<span class="earth-mobile-settings-switch">
<input type="checkbox" data-settings-panel="earth-stats">
<span class="earth-mobile-settings-switch-track"></span>
</span>
</label>
<label class="earth-mobile-settings-card">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">新闻直播</span>
<span class="earth-mobile-settings-subtitle">控制 Live 新闻面板显示</span>
</div>
<span class="earth-mobile-settings-switch">
<input type="checkbox" data-settings-panel="media-panel">
<span class="earth-mobile-settings-switch-track"></span>
</span>
</label>
</div>
<div class="earth-mobile-settings-group" data-settings-tab-panel="motion" hidden>
<div class="earth-mobile-settings-title">动捕</div>
<label
class="earth-mobile-settings-card"
data-gatekeeper-permission="earth.motion_debug"
>
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">动捕调试模式</span>
<span class="earth-mobile-settings-subtitle">显示摄像头识别到的骨架连线和匹配动作</span>
@@ -976,49 +1089,21 @@
<button type="button" class="earth-mobile-settings-pill" data-motion-provider="motion_agent" aria-pressed="false">Motion Agent</button>
</div>
</div>
</div>
<div class="earth-mobile-settings-group" data-settings-tab-panel="shortcuts" hidden>
<div class="earth-mobile-settings-title">快捷键</div>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">地球默认大小</span>
<span class="earth-mobile-settings-subtitle">用于重置视角、缩放重置和巡航视图</span>
<span class="earth-mobile-settings-label">键盘控制</span>
<span class="earth-mobile-settings-subtitle">点击按键后按下新的快捷键Esc 取消录入</span>
</div>
<div class="earth-mobile-settings-slider-row">
<input
class="earth-mobile-settings-slider"
type="range"
min="0.5"
max="5"
step="0.01"
value="1"
data-default-earth-size-slider
aria-label="移动端调整地球默认大小"
>
<span class="earth-mobile-settings-slider-value" data-default-earth-size-value>100%</span>
<div class="earth-shortcut-list" data-shortcut-list></div>
<div class="earth-mobile-settings-actions">
<button class="earth-mobile-action-btn earth-mobile-action-btn--ghost" type="button" data-shortcut-reset-all>恢复默认快捷键</button>
</div>
</div>
</div>
<div class="earth-mobile-settings-group">
<div class="earth-mobile-settings-title">地形</div>
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
<div class="earth-mobile-settings-copy">
<span class="earth-mobile-settings-label">地形透明度</span>
<span class="earth-mobile-settings-subtitle">调高后会呈现更明显的绿色地形覆盖效果</span>
</div>
<div class="earth-mobile-settings-slider-row">
<input
class="earth-mobile-settings-slider"
type="range"
min="0.05"
max="1"
step="0.01"
value="0.62"
data-terrain-opacity-slider
aria-label="移动端调整地形透明度"
>
<span class="earth-mobile-settings-slider-value" data-terrain-opacity-value>62%</span>
</div>
</div>
</div>
<div class="earth-mobile-settings-group">
<div class="earth-mobile-settings-group" data-settings-tab-panel="system" hidden>
<div class="earth-mobile-settings-title">系统</div>
<div class="earth-mobile-settings-actions">
<button id="mobile-settings-reset" class="earth-mobile-action-btn earth-mobile-action-btn--ghost" type="button">重置设置</button>
@@ -1100,9 +1185,17 @@
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="earth-settings-tabs" role="tablist" aria-label="设置分类">
<button type="button" class="earth-settings-tab is-active" data-settings-tab="runtime" aria-selected="true">运行</button>
<button type="button" class="earth-settings-tab" data-settings-tab="display" aria-selected="false">显示</button>
<button type="button" class="earth-settings-tab" data-settings-tab="panels" aria-selected="false">面板</button>
<button type="button" class="earth-settings-tab" data-settings-tab="motion" aria-selected="false">动捕</button>
<button type="button" class="earth-settings-tab" data-settings-tab="shortcuts" aria-selected="false">快捷键</button>
<button type="button" class="earth-settings-tab" data-settings-tab="system" aria-selected="false">系统</button>
</div>
<div class="earth-settings-content hud-panel__body">
<section class="earth-settings-section">
<div class="earth-settings-section-title">旋转</div>
<section class="earth-settings-section" data-settings-tab-panel="runtime">
<div class="earth-settings-section-title">运行</div>
<div class="earth-settings-list">
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
@@ -1194,8 +1287,8 @@
</div>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">视图</div>
<section class="earth-settings-section" data-settings-tab-panel="display" hidden>
<div class="earth-settings-section-title">显示</div>
<div class="earth-settings-list">
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
@@ -1303,33 +1396,70 @@
<span class="earth-settings-switch-track"></span>
</span>
</label>
<label
class="earth-settings-item"
for="toggle-motion-debug"
data-gatekeeper-permission="earth.motion_debug"
>
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">动捕调试模式</span>
<span class="earth-settings-item-subtitle">打开骨架连线面板;未匹配为红线,匹配动作后变绿</span>
<span class="earth-settings-item-title">地球默认大小</span>
<span class="earth-settings-item-subtitle">用于重置视角、缩放重置和巡航视图的默认缩放比例</span>
</div>
<span class="earth-settings-switch">
<input id="toggle-motion-debug" type="checkbox" data-motion-debug-toggle>
<span class="earth-settings-switch-track"></span>
</span>
</label>
<div
class="earth-settings-item earth-settings-item--stacked"
data-gatekeeper-permission="earth.motion_debug"
>
<div class="earth-settings-copy">
<span class="earth-settings-item-title">动捕输入源</span>
<span class="earth-settings-item-subtitle">浏览器摄像头适合网页/SaaSMotion Agent 适合双摄、RTSP/HTTP 和客户端</span>
</div>
<div class="earth-settings-segmented" role="group" aria-label="选择动捕输入源">
<button type="button" class="earth-settings-segmented-btn is-active" data-motion-provider="browser_camera" aria-pressed="true">浏览器摄像头</button>
<button type="button" class="earth-settings-segmented-btn" data-motion-provider="motion_agent" aria-pressed="false">Motion Agent</button>
<div class="earth-settings-slider-row">
<input
id="default-earth-size-slider"
class="earth-settings-slider"
type="range"
min="0.5"
max="5"
step="0.01"
value="1"
aria-label="调整地球默认大小"
>
<span id="default-earth-size-value" class="earth-settings-slider-value">100%</span>
</div>
</div>
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">地形透明度</span>
<span class="earth-settings-item-subtitle">调高后会呈现更明显的绿色地形覆盖效果</span>
</div>
<div class="earth-settings-slider-row">
<input
id="terrain-opacity-slider"
class="earth-settings-slider"
type="range"
min="0.05"
max="1"
step="0.01"
value="0.62"
aria-label="调整地形透明度"
>
<span id="terrain-opacity-value" class="earth-settings-slider-value">62%</span>
</div>
</div>
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
<span class="earth-settings-item-title" id="boundary-precision-status" data-boundary-precision-status>国界精度</span>
<span class="earth-settings-item-subtitle" id="boundary-precision-detail" data-boundary-precision-detail>正在读取高清国界状态...</span>
</div>
<div id="boundary-precision-progress-wrap" class="earth-boundary-progress" data-boundary-precision-progress-wrap hidden>
<div class="earth-boundary-progress__track">
<div id="boundary-precision-progress-bar" class="earth-boundary-progress__bar" data-boundary-precision-progress-bar></div>
</div>
<span id="boundary-precision-progress-value" class="earth-boundary-progress__value" data-boundary-precision-progress-value>0%</span>
</div>
<div class="earth-settings-boundary-action-row">
<div class="earth-settings-segmented earth-settings-segmented--boundary" role="group" aria-label="选择国界精度">
<button id="boundary-precision-disable" type="button" class="earth-settings-segmented-btn is-active" data-boundary-precision-disable aria-pressed="true">低精</button>
<button id="boundary-precision-build" type="button" class="earth-settings-segmented-btn" data-boundary-precision-build aria-pressed="false">高精</button>
</div>
<button id="boundary-precision-rebuild" type="button" class="earth-settings-reload-action" data-boundary-precision-rebuild aria-label="重新获取并构建高清国界" title="重新获取并构建" hidden disabled>
<span class="material-symbols-rounded">refresh</span>
</button>
</div>
</div>
</div>
</section>
<section class="earth-settings-section" data-settings-tab-panel="panels" hidden>
<div class="earth-settings-section-title">面板</div>
<div class="earth-settings-list">
<label class="earth-settings-item" for="toggle-view-layers">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">图层控制</span>
@@ -1372,81 +1502,54 @@
</label>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">视图</div>
<section class="earth-settings-section" data-settings-tab-panel="motion" hidden>
<div class="earth-settings-section-title">动捕</div>
<div class="earth-settings-list">
<div class="earth-settings-item earth-settings-item--stacked">
<label
class="earth-settings-item"
for="toggle-motion-debug"
data-gatekeeper-permission="earth.motion_debug"
>
<div class="earth-settings-copy">
<span class="earth-settings-item-title">地球默认大小</span>
<span class="earth-settings-item-subtitle">用于重置视角、缩放重置和巡航视图的默认缩放比例</span>
<span class="earth-settings-item-title">动捕调试模式</span>
<span class="earth-settings-item-subtitle">打开骨架连线面板;未匹配为红线,匹配动作后变绿</span>
</div>
<div class="earth-settings-slider-row">
<input
id="default-earth-size-slider"
class="earth-settings-slider"
type="range"
min="0.5"
max="5"
step="0.01"
value="1"
aria-label="调整地球默认大小"
>
<span id="default-earth-size-value" class="earth-settings-slider-value">100%</span>
<span class="earth-settings-switch">
<input id="toggle-motion-debug" type="checkbox" data-motion-debug-toggle>
<span class="earth-settings-switch-track"></span>
</span>
</label>
<div
class="earth-settings-item earth-settings-item--stacked"
data-gatekeeper-permission="earth.motion_debug"
>
<div class="earth-settings-copy">
<span class="earth-settings-item-title">动捕输入源</span>
<span class="earth-settings-item-subtitle">浏览器摄像头适合网页/SaaSMotion Agent 适合双摄、RTSP/HTTP 和客户端</span>
</div>
<div class="earth-settings-segmented" role="group" aria-label="选择动捕输入源">
<button type="button" class="earth-settings-segmented-btn is-active" data-motion-provider="browser_camera" aria-pressed="true">浏览器摄像头</button>
<button type="button" class="earth-settings-segmented-btn" data-motion-provider="motion_agent" aria-pressed="false">Motion Agent</button>
</div>
</div>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">地形</div>
<section class="earth-settings-section" data-settings-tab-panel="shortcuts" hidden>
<div class="earth-settings-section-title">快捷键</div>
<div class="earth-settings-list">
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">地形透明度</span>
<span class="earth-settings-item-subtitle">调高后会呈现更明显的绿色地形覆盖效果</span>
<span class="earth-settings-item-title">键盘控制</span>
<span class="earth-settings-item-subtitle">点击按键后按下新的快捷键Esc 取消录入,冲突快捷键不会保存</span>
</div>
<div class="earth-settings-slider-row">
<input
id="terrain-opacity-slider"
class="earth-settings-slider"
type="range"
min="0.05"
max="1"
step="0.01"
value="0.62"
aria-label="调整地形透明度"
>
<span id="terrain-opacity-value" class="earth-settings-slider-value">62%</span>
<div class="earth-shortcut-list" data-shortcut-list></div>
<div class="earth-settings-shortcut-actions">
<button class="earth-settings-reload-action earth-settings-reload-action--text" type="button" data-shortcut-reset-all>恢复默认</button>
</div>
</div>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">国界精度</div>
<div class="earth-settings-list">
<div class="earth-settings-item earth-settings-item--stacked">
<div class="earth-settings-copy">
<span class="earth-settings-item-title" id="boundary-precision-status">国界精度</span>
<span class="earth-settings-item-subtitle" id="boundary-precision-detail">正在读取高清国界状态...</span>
</div>
<div id="boundary-precision-progress-wrap" class="earth-boundary-progress" hidden>
<div class="earth-boundary-progress__track">
<div id="boundary-precision-progress-bar" class="earth-boundary-progress__bar"></div>
</div>
<span id="boundary-precision-progress-value" class="earth-boundary-progress__value">0%</span>
</div>
<div class="earth-settings-boundary-action-row">
<div class="earth-settings-segmented earth-settings-segmented--boundary" role="group" aria-label="选择国界精度">
<button id="boundary-precision-disable" type="button" class="earth-settings-segmented-btn is-active" aria-pressed="true">低精</button>
<button id="boundary-precision-build" type="button" class="earth-settings-segmented-btn" aria-pressed="false">高精</button>
</div>
<button id="boundary-precision-rebuild" type="button" class="earth-settings-reload-action" aria-label="重新获取并构建高清国界" title="重新获取并构建" hidden disabled>
<span class="material-symbols-rounded">refresh</span>
</button>
</div>
</div>
</div>
</section>
<section class="earth-settings-section">
<section class="earth-settings-section" data-settings-tab-panel="system" hidden>
<div class="earth-settings-section-title">系统</div>
<div class="earth-settings-list">
<a

View File

@@ -1,57 +1,110 @@
const DEFAULT_BRAND_LANGUAGE = "zh";
const EARTH_BRAND_API = "/api/v1/earth/brand";
const BRANDS = {
zh: {
ariaLabel: "智能星球计划品牌标识",
titleAlt: "智能星球计划",
titleSrc: "assets/brand/title-zh.png",
logoSrc: "/earth/assets/brand/earth-logo.png",
titleSrc: "/earth/assets/brand/title-zh.png",
titleText: "智能星球计划",
subtitle: "现实层宇宙全息感知系统",
description: "卫星 · 海底光缆 · 算力基础设施",
},
en: {
ariaLabel: "Intelligent Planet Program brand banner",
titleAlt: "Intelligent Planet Program",
titleSrc: "assets/brand/title-en.png",
logoSrc: "/earth/assets/brand/earth-logo.png",
titleSrc: "/earth/assets/brand/title-en.png",
titleText: "Intelligent Planet Program",
subtitle: "Physical-Universe Holography",
description: "Satellites · Cables · Compute Infra",
},
};
function getBrandConfig(variant = DEFAULT_BRAND_LANGUAGE) {
export function getDefaultBrandConfig(variant = DEFAULT_BRAND_LANGUAGE) {
return BRANDS[variant] ?? BRANDS[DEFAULT_BRAND_LANGUAGE];
}
export function renderBrand(variant = DEFAULT_BRAND_LANGUAGE) {
const config = getBrandConfig(variant);
function escapeHtml(value = "") {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function normalizeBrandConfig(config = {}, variant = DEFAULT_BRAND_LANGUAGE) {
const defaults = getDefaultBrandConfig(variant);
const normalized = {
...defaults,
...config,
ariaLabel: config.aria_label ?? config.ariaLabel ?? defaults.ariaLabel,
titleAlt: config.title_alt ?? config.titleAlt ?? defaults.titleAlt,
logoSrc: config.logo_src ?? config.logoSrc ?? defaults.logoSrc,
titleSrc: config.title_src ?? config.titleSrc ?? defaults.titleSrc,
titleText: config.title_text ?? config.titleText ?? defaults.titleText,
};
if (!normalized.titleText) normalized.titleText = defaults.titleText;
if (!normalized.ariaLabel) normalized.ariaLabel = normalized.titleText;
if (!normalized.titleAlt) normalized.titleAlt = normalized.titleText;
return normalized;
}
export function renderBrand(configOrVariant = DEFAULT_BRAND_LANGUAGE) {
const variant =
typeof configOrVariant === "string"
? configOrVariant
: configOrVariant.variant ?? DEFAULT_BRAND_LANGUAGE;
const config =
typeof configOrVariant === "string"
? normalizeBrandConfig({}, variant)
: normalizeBrandConfig(configOrVariant, variant);
const titleMarkup = config.titleSrc
? `
<img
class="earth-brand__title"
src="${escapeHtml(config.titleSrc)}"
alt="${escapeHtml(config.titleAlt)}"
fetchpriority="high"
loading="eager"
>
`
: `<div class="earth-brand__title-text">${escapeHtml(config.titleText)}</div>`;
return `
<div class="earth-brand earth-brand--${variant}" aria-label="${config.ariaLabel}">
<div class="earth-brand earth-brand--${escapeHtml(variant)}" aria-label="${escapeHtml(config.ariaLabel)}">
<img
class="earth-brand__logo"
src="assets/brand/earth-logo.png"
src="${escapeHtml(config.logoSrc)}"
alt=""
aria-hidden="true"
fetchpriority="high"
loading="eager"
>
<div class="earth-brand__copy">
<img
class="earth-brand__title"
src="${config.titleSrc}"
alt="${config.titleAlt}"
fetchpriority="high"
loading="eager"
>
${titleMarkup}
<div class="earth-brand__meta">
<span class="earth-brand__subtitle">${config.subtitle}</span>
<span class="earth-brand__description">${config.description}</span>
<span class="earth-brand__subtitle">${escapeHtml(config.subtitle)}</span>
<span class="earth-brand__description">${escapeHtml(config.description)}</span>
</div>
</div>
</div>
`.trim();
}
export function mountBrand(target, variant = DEFAULT_BRAND_LANGUAGE) {
export function mountBrand(target, configOrVariant = DEFAULT_BRAND_LANGUAGE) {
if (!target) return;
target.innerHTML = renderBrand(variant);
target.innerHTML = renderBrand(configOrVariant);
}
export async function fetchEarthBrandConfig() {
const response = await fetch(EARTH_BRAND_API, { cache: "no-store" });
if (!response.ok) {
throw new Error(`Earth brand config request failed: ${response.status}`);
}
const payload = await response.json();
return payload?.brand ?? getDefaultBrandConfig();
}

View File

@@ -153,7 +153,7 @@ const SETTINGS_SHEET_MAX_SCALE_X = 0.22;
const SETTINGS_SHEET_MAX_SCALE_Y = 0.18;
const EARTH_SETTINGS_STORAGE_KEY = "planet.earth.settings.v2";
const LEGACY_EARTH_SETTINGS_STORAGE_KEY = "planet.earth.settings.v1";
const EARTH_SETTINGS_VERSION = 11;
const EARTH_SETTINGS_VERSION = 13;
const GRID_LINES_DEFAULT_VERSION = 3;
const SATELLITE_DISPLAY_DEFAULT_VERSION = 4;
const MEDIA_PANEL_DEFAULT_VERSION = 5;
@@ -163,8 +163,14 @@ const MOTION_DEBUG_SKELETON_ONLY_DEFAULT_VERSION = 8;
const MEDIA_PANEL_ACTIVE_TAB_DEFAULT_VERSION = 9;
const VISUAL_PREFERENCES_DEFAULT_VERSION = 10;
const SURFACE_HOVER_INFO_DEFAULT_VERSION = 11;
const KEYBOARD_SHORTCUTS_DEFAULT_VERSION = 13;
const DEFAULT_EARTH_ZOOM_STEP = 0.01;
const ZOOM_STATUS_UPDATE_INTERVAL_MS = 90;
const KEYBOARD_ROTATION_ACCELERATION = 3.2;
const KEYBOARD_ROTATION_MAX_SPEED = 2.8;
const KEYBOARD_ROTATION_FRICTION = 5.4;
const KEYBOARD_ROTATION_STOP_SPEED = 0.012;
const KEYBOARD_ZOOM_STEP = 0.1;
const TARGET_SWITCH_ZOOM_IN_PHASE = 0.28;
const TARGET_SWITCH_ROTATE_PHASE = 0.5;
let settingsModalTimer = null;
@@ -186,6 +192,13 @@ let mobileDrawerOpen = false;
let mobileDrawerCard = "layers";
let mobileDrawerHintTimer = null;
let toolbarHubController = null;
let keyboardShortcuts = {};
let capturingShortcutActionId = null;
let keyboardRotationFrameId = null;
let keyboardRotationLastFrameAt = 0;
let keyboardRotationOriginalAutoRotate = null;
const activeKeyboardRotationActions = new Set();
const keyboardRotationVelocity = { x: 0, y: 0 };
const ALLOWED_CRUISE_MODULES = new Set(Object.values(CRUISE_MODULES));
const ALLOWED_SATELLITE_DISPLAY_STYLES = new Set(
Object.values(SATELLITE_DISPLAY_STYLES),
@@ -202,6 +215,36 @@ const CRUISE_MODULE_LABELS = {
[CRUISE_MODULES.SATELLITES]: "卫星",
};
const KEYBOARD_SHORTCUT_DEFINITIONS = [
{ id: "rotateUp", label: "向上旋转", category: "视角控制", defaultBinding: "W", aliases: ["ArrowUp"] },
{ id: "rotateLeft", label: "向左旋转", category: "视角控制", defaultBinding: "A", aliases: ["ArrowLeft"] },
{ id: "rotateDown", label: "向下旋转", category: "视角控制", defaultBinding: "S", aliases: ["ArrowDown"] },
{ id: "rotateRight", label: "向右旋转", category: "视角控制", defaultBinding: "D", aliases: ["ArrowRight"] },
{ id: "zoomIn", label: "放大", category: "视角控制", defaultBinding: "Plus", aliases: ["="] },
{ id: "zoomOut", label: "缩小", category: "视角控制", defaultBinding: "-", aliases: ["_"] },
{ id: "closeFocus", label: "关闭当前焦点菜单", category: "工具", defaultBinding: "Escape" },
{ id: "openSearch", label: "打开搜索", category: "工具", defaultBinding: "F" },
{ id: "resetView", label: "重置视角", category: "工具", defaultBinding: "R" },
{ id: "toggleLayerPanel", label: "打开/关闭图层面板", category: "工具", defaultBinding: "L" },
{ id: "toggleLayoutExpanded", label: "最大化布局", category: "工具", defaultBinding: "M" },
{ id: "toggleMediaPanel", label: "打开/关闭新闻直播", category: "工具", defaultBinding: "Ctrl+M" },
{ id: "toggleAutoRotate", label: "暂停/恢复旋转", category: "工具", defaultBinding: "Space" },
{ id: "toggleLayer:cables", label: "切换海缆", category: "图层", defaultBinding: "Ctrl+1" },
{ id: "toggleLayer:satellites", label: "切换卫星", category: "图层", defaultBinding: "Ctrl+2" },
{ id: "toggleLayer:computeCenters", label: "切换算力中心", category: "图层", defaultBinding: "Ctrl+3" },
{ id: "toggleLayer:vessels", label: "切换船只", category: "图层", defaultBinding: "Ctrl+4" },
{ id: "toggleLayer:bgp", label: "切换 BGP观测", category: "图层", defaultBinding: "Ctrl+5" },
{ id: "toggleLayer:terrain", label: "切换地形", category: "图层", defaultBinding: "Ctrl+6" },
{ id: "toggleLayer:earthHighResTexture", label: "切换高清材质", category: "图层", defaultBinding: "Ctrl+7" },
{ id: "toggleLayer:atmosphereClouds", label: "切换大气云图", category: "图层", defaultBinding: "Ctrl+8" },
{ id: "toggleLayer:countryBoundaries", label: "切换国界线", category: "图层", defaultBinding: "Ctrl+9" },
{ id: "toggleLayer:gridLines", label: "切换经纬线", category: "图层", defaultBinding: "Ctrl+0" },
];
const KEYBOARD_SHORTCUT_DEFINITION_BY_ID = new Map(
KEYBOARD_SHORTCUT_DEFINITIONS.map((definition) => [definition.id, definition]),
);
function normalizeMediaPanelActiveTab(tab) {
return tab === "news" ? "news" : "live";
}
@@ -212,6 +255,431 @@ function normalizeSurfaceHoverInfoMode(mode) {
: DEFAULT_SURFACE_HOVER_INFO_MODE;
}
function getDefaultKeyboardShortcuts() {
return Object.fromEntries(
KEYBOARD_SHORTCUT_DEFINITIONS.map((definition) => [
definition.id,
{
binding: definition.defaultBinding,
enabled: true,
},
]),
);
}
function normalizeShortcutBinding(binding) {
if (typeof binding !== "string") return "";
if (binding.trim() === "+") return "+";
const parts = binding
.split("+")
.map((part) => part.trim())
.filter(Boolean);
if (parts.length === 0) return "";
const key = parts[parts.length - 1];
const modifiers = new Set(
parts.slice(0, -1).map((part) => {
const normalized = part.toLowerCase();
if (normalized === "control") return "Ctrl";
if (normalized === "cmd" || normalized === "command") return "Meta";
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}),
);
const orderedModifiers = ["Ctrl", "Alt", "Shift", "Meta"].filter((modifier) =>
modifiers.has(modifier),
);
return [...orderedModifiers, normalizeShortcutKeyName(key)].join("+");
}
function normalizeShortcutKeyName(key) {
if (!key) return "";
if (key === " ") return "Space";
if (key === "+") return "Plus";
if (key === "_") return "-";
if (key.length === 1) return key.toUpperCase();
const lowered = key.toLowerCase();
if (lowered === "esc") return "Escape";
if (lowered === "spacebar") return "Space";
if (lowered.startsWith("arrow")) {
return `Arrow${lowered.slice(5, 6).toUpperCase()}${lowered.slice(6)}`;
}
return key.charAt(0).toUpperCase() + key.slice(1);
}
function normalizeKeyboardShortcuts(rawShortcuts = {}) {
const defaults = getDefaultKeyboardShortcuts();
const source =
rawShortcuts && typeof rawShortcuts === "object"
? rawShortcuts
: {};
return Object.fromEntries(
KEYBOARD_SHORTCUT_DEFINITIONS.map((definition) => {
const rawShortcut = source[definition.id] || {};
const binding = normalizeShortcutBinding(rawShortcut.binding);
return [
definition.id,
{
binding: binding || defaults[definition.id].binding,
enabled:
typeof rawShortcut.enabled === "boolean"
? rawShortcut.enabled
: defaults[definition.id].enabled,
},
];
}),
);
}
function getShortcutDisplayLabel(binding) {
const normalized = normalizeShortcutBinding(binding);
if (!normalized) return "未设置";
return normalized
.replaceAll("ArrowUp", "↑")
.replaceAll("ArrowDown", "↓")
.replaceAll("ArrowLeft", "←")
.replaceAll("ArrowRight", "→")
.replaceAll("Space", "空格")
.replaceAll("Escape", "Esc")
.replaceAll("Plus", "+")
.replaceAll("Ctrl", "Ctrl")
.replaceAll("Meta", "⌘");
}
function getShortcutAliasesLabel(definition) {
const aliases = Array.isArray(definition?.aliases) ? definition.aliases : [];
if (aliases.length === 0) return "";
return `备用:${aliases.map(getShortcutDisplayLabel).join(" / ")}`;
}
function getShortcutForAction(actionId) {
return keyboardShortcuts[actionId] || getDefaultKeyboardShortcuts()[actionId] || {
binding: "",
enabled: false,
};
}
function getShortcutOwnerByBinding(binding, { excludeActionId = null } = {}) {
const normalized = normalizeShortcutBinding(binding);
if (!normalized) return null;
for (const definition of KEYBOARD_SHORTCUT_DEFINITIONS) {
if (definition.id === excludeActionId) continue;
const shortcut = getShortcutForAction(definition.id);
if (!shortcut.enabled) continue;
const bindings = [
shortcut.binding,
...(Array.isArray(definition.aliases) ? definition.aliases : []),
].map(normalizeShortcutBinding);
if (bindings.includes(normalized)) {
return definition;
}
}
return null;
}
function getShortcutChordFromEvent(event) {
if (!(event instanceof KeyboardEvent)) return "";
let key = event.key;
if (!key || key === "Unidentified" || key === "Dead") return "";
if (key === " ") key = "Space";
const ignoreShift = key === "+" || key === "_";
if (key.length === 1) key = key.toUpperCase();
const modifiers = [];
if (event.ctrlKey) modifiers.push("Ctrl");
if (event.altKey) modifiers.push("Alt");
if (event.shiftKey && key.length !== 1 && !ignoreShift) modifiers.push("Shift");
if (event.metaKey) modifiers.push("Meta");
return [...modifiers, normalizeShortcutKeyName(key)].join("+");
}
function isEditableShortcutTarget(target) {
if (!(target instanceof Element)) return false;
if (target.closest("[data-shortcut-capture]")) return false;
return Boolean(
target.closest("input, textarea, select, [contenteditable='true'], [contenteditable='']"),
);
}
function isShortcutSuppressedBySettingsUi(target) {
if (!(target instanceof Element)) return false;
return Boolean(target.closest("#settings-modal, .earth-mobile-page--settings"));
}
function getShortcutDefinitionForChord(chord) {
const normalizedChord = normalizeShortcutBinding(chord);
if (!normalizedChord) return null;
for (const definition of KEYBOARD_SHORTCUT_DEFINITIONS) {
const shortcut = getShortcutForAction(definition.id);
if (!shortcut.enabled) continue;
const bindings = [
shortcut.binding,
...(Array.isArray(definition.aliases) ? definition.aliases : []),
].map(normalizeShortcutBinding);
if (bindings.includes(normalizedChord)) {
return definition;
}
}
return null;
}
function closeCurrentFocusOverlay() {
if (capturingShortcutActionId) {
capturingShortcutActionId = null;
syncShortcutCaptureUi();
return true;
}
if (isSearchPanelOpen()) {
closeSearchPanel();
return true;
}
if (isSettingsModalOpen()) {
closeSettingsModal();
return true;
}
if (isMobileLayout() && mobileDrawerOpen) {
setMobileDrawerState({ open: false });
return true;
}
if (isFloatingMenuVisible()) {
closeFloatingMenus();
return true;
}
if (toolbarHubController?.isOpen?.()) {
toolbarHubController.close();
return true;
}
clearLockedObjectAndInfo();
return true;
}
function isKeyboardRotationAction(actionId) {
return (
actionId === "rotateUp" ||
actionId === "rotateDown" ||
actionId === "rotateLeft" ||
actionId === "rotateRight"
);
}
function getKeyboardRotationDirection() {
const direction = { x: 0, y: 0 };
if (activeKeyboardRotationActions.has("rotateUp")) direction.x -= 1;
if (activeKeyboardRotationActions.has("rotateDown")) direction.x += 1;
if (activeKeyboardRotationActions.has("rotateLeft")) direction.y -= 1;
if (activeKeyboardRotationActions.has("rotateRight")) direction.y += 1;
const magnitude = Math.hypot(direction.x, direction.y);
if (magnitude > 1) {
direction.x /= magnitude;
direction.y /= magnitude;
}
return direction;
}
function clampKeyboardRotationVelocity() {
const speed = Math.hypot(keyboardRotationVelocity.x, keyboardRotationVelocity.y);
if (speed <= KEYBOARD_ROTATION_MAX_SPEED) return;
const scale = KEYBOARD_ROTATION_MAX_SPEED / speed;
keyboardRotationVelocity.x *= scale;
keyboardRotationVelocity.y *= scale;
}
function applyKeyboardRotationFrame(timestamp) {
keyboardRotationFrameId = null;
if (!earthObj) {
stopKeyboardRotationControl({ restoreAutoRotate: true, clearVelocity: true });
return;
}
const deltaSeconds = keyboardRotationLastFrameAt
? Math.min((timestamp - keyboardRotationLastFrameAt) / 1000, 0.05)
: 0.016;
keyboardRotationLastFrameAt = timestamp;
const direction = getKeyboardRotationDirection();
const hasInput = direction.x !== 0 || direction.y !== 0;
if (hasInput) {
keyboardRotationVelocity.x += direction.x * KEYBOARD_ROTATION_ACCELERATION * deltaSeconds;
keyboardRotationVelocity.y += direction.y * KEYBOARD_ROTATION_ACCELERATION * deltaSeconds;
clampKeyboardRotationVelocity();
} else {
const decay = Math.exp(-KEYBOARD_ROTATION_FRICTION * deltaSeconds);
keyboardRotationVelocity.x *= decay;
keyboardRotationVelocity.y *= decay;
}
const speed = Math.hypot(keyboardRotationVelocity.x, keyboardRotationVelocity.y);
if (!hasInput && speed < KEYBOARD_ROTATION_STOP_SPEED) {
keyboardRotationVelocity.x = 0;
keyboardRotationVelocity.y = 0;
restoreKeyboardRotationAutoRotate();
keyboardRotationLastFrameAt = 0;
return;
}
earthObj.rotation.x = THREE.MathUtils.clamp(
earthObj.rotation.x + keyboardRotationVelocity.x * deltaSeconds,
-Math.PI / 2,
Math.PI / 2,
);
earthObj.rotation.y += keyboardRotationVelocity.y * deltaSeconds;
keyboardRotationFrameId = window.requestAnimationFrame(applyKeyboardRotationFrame);
}
function ensureKeyboardRotationFrame() {
if (keyboardRotationFrameId !== null) return;
keyboardRotationFrameId = window.requestAnimationFrame(applyKeyboardRotationFrame);
}
function restoreKeyboardRotationAutoRotate() {
if (keyboardRotationOriginalAutoRotate === true && !autoRotate) {
setAutoRotate(true);
}
keyboardRotationOriginalAutoRotate = null;
}
function startKeyboardRotationControl(actionId) {
if (!earthObj || !isKeyboardRotationAction(actionId)) return;
if (keyboardRotationOriginalAutoRotate === null) {
keyboardRotationOriginalAutoRotate = autoRotate;
}
if (autoRotate) {
setAutoRotate(false);
}
activeKeyboardRotationActions.add(actionId);
clearLockedObject();
ensureKeyboardRotationFrame();
}
function stopKeyboardRotationControl({ actionId = null, restoreAutoRotate = false, clearVelocity = false } = {}) {
if (actionId) {
activeKeyboardRotationActions.delete(actionId);
} else {
activeKeyboardRotationActions.clear();
}
if (clearVelocity) {
keyboardRotationVelocity.x = 0;
keyboardRotationVelocity.y = 0;
}
if (restoreAutoRotate && activeKeyboardRotationActions.size === 0) {
restoreKeyboardRotationAutoRotate();
}
if (clearVelocity && keyboardRotationFrameId !== null) {
window.cancelAnimationFrame(keyboardRotationFrameId);
keyboardRotationFrameId = null;
keyboardRotationLastFrameAt = 0;
} else if (activeKeyboardRotationActions.size === 0 && Math.hypot(keyboardRotationVelocity.x, keyboardRotationVelocity.y) > 0) {
ensureKeyboardRotationFrame();
}
}
function applyKeyboardZoom(direction) {
setZoomLevel(zoomLevel + direction * KEYBOARD_ZOOM_STEP, activeCamera);
showZoomStatusCapsule({ force: true });
}
function openSearchFromShortcut() {
if (isMobileLayout()) {
setMobileDrawerState({ open: true, card: "search" });
return;
}
closeTransientMobileOverlays({ except: "search" });
openSearchPanel();
}
function toggleLayerPanelFromShortcut() {
if (isMobileLayout()) {
const nextOpen = !(mobileDrawerOpen && mobileDrawerCard === "layers");
setMobileDrawerState({ open: nextOpen, card: "layers" });
return;
}
const panel = document.getElementById("layer-toggles");
const currentlyVisible = !panel?.classList.contains("hud-panel-hidden");
setHudPanelVisibility("layer-toggles", !currentlyVisible);
}
function toggleMediaPanelFromShortcut() {
if (isMobileLayout()) {
const nextOpen = !(mobileDrawerOpen && (mobileDrawerCard === "tv" || mobileDrawerCard === "news"));
setMobileDrawerState({ open: nextOpen, card: "tv" });
return;
}
const nextVisible = !isTVPanelVisible();
setTVPanelVisible(nextVisible);
}
function toggleLayoutExpandedFromShortcut() {
const container = document.getElementById("container");
if (!(container instanceof HTMLElement)) return;
const expanded = toggleLayoutExpanded(container);
showStatusMessage(expanded ? "布局已最大化" : "布局已恢复", "info");
}
async function toggleLayerFromShortcut(layerId) {
const definition = getLayerDefinition(layerId);
if (!definition) return;
const button = getLayerButton(layerId);
if (button?.disabled || button?.classList.contains("is-disabled")) {
showStatusMessage(`${definition.label}当前不可用`, "warning");
return;
}
await definition.setVisible(!definition.getVisible());
showStatusMessage(`${definition.label}${definition.getVisible() ? "已显示" : "已隐藏"}`, "info");
}
function executeKeyboardShortcut(actionId) {
if (actionId === "closeFocus") {
closeCurrentFocusOverlay();
return;
}
if (isKeyboardRotationAction(actionId)) {
startKeyboardRotationControl(actionId);
return;
}
if (actionId === "zoomIn") {
applyKeyboardZoom(1);
return;
}
if (actionId === "zoomOut") {
applyKeyboardZoom(-1);
return;
}
if (actionId === "openSearch") {
openSearchFromShortcut();
return;
}
if (actionId === "resetView") {
resetView(activeCamera);
return;
}
if (actionId === "toggleLayerPanel") {
toggleLayerPanelFromShortcut();
return;
}
if (actionId === "toggleLayoutExpanded") {
toggleLayoutExpandedFromShortcut();
return;
}
if (actionId === "toggleMediaPanel") {
toggleMediaPanelFromShortcut();
return;
}
if (actionId === "toggleAutoRotate") {
const isRotating = toggleAutoRotate();
showStatusMessage(isRotating ? "旋转已恢复" : "旋转已暂停", "info");
return;
}
if (actionId.startsWith("toggleLayer:")) {
void toggleLayerFromShortcut(actionId.slice("toggleLayer:".length));
}
}
function detectLayoutMode() {
const width = window.innerWidth;
const height = window.innerHeight;
@@ -819,6 +1287,7 @@ function getCurrentSharedSettingsSnapshot() {
satelliteRealAltitudeEnabled: getSatelliteRealAltitudeEnabled(),
interactableCompactDotsEnabled: getInteractableCompactDotsEnabled(),
surfaceHoverInfoMode: getSurfaceHoverInfoMode(),
keyboardShortcuts: normalizeKeyboardShortcuts(keyboardShortcuts),
};
}
@@ -879,6 +1348,7 @@ function cloneEarthSettings(settings) {
surfaceHoverInfoMode: normalizeSurfaceHoverInfoMode(
settings.shared.surfaceHoverInfoMode,
),
keyboardShortcuts: normalizeKeyboardShortcuts(settings.shared.keyboardShortcuts),
layerVisibility: { ...(settings.shared.layerVisibility || {}) },
},
views: {
@@ -1017,6 +1487,10 @@ function normalizeEarthSettings(rawSettings, defaults) {
(rawSettings?.version || 0) >= SURFACE_HOVER_INFO_DEFAULT_VERSION
? normalizeSurfaceHoverInfoMode(sharedSettings?.surfaceHoverInfoMode)
: defaults.shared.surfaceHoverInfoMode;
const nextKeyboardShortcuts =
(rawSettings?.version || 0) >= KEYBOARD_SHORTCUTS_DEFAULT_VERSION
? normalizeKeyboardShortcuts(sharedSettings?.keyboardShortcuts)
: defaults.shared.keyboardShortcuts;
const nextTrailsEnabled = typeof sharedSettings?.trailsEnabled === "boolean"
? sharedSettings.trailsEnabled
: typeof inputLayerVisibility.trails === "boolean"
@@ -1046,6 +1520,7 @@ function normalizeEarthSettings(rawSettings, defaults) {
satelliteRealAltitudeEnabled: nextSatelliteRealAltitudeEnabled,
interactableCompactDotsEnabled: nextInteractableCompactDotsEnabled,
surfaceHoverInfoMode: nextSurfaceHoverInfoMode,
keyboardShortcuts: nextKeyboardShortcuts,
},
views: {
desktop: {
@@ -1550,6 +2025,8 @@ async function applyEarthSettings(settings, { applyLayers = true } = {}) {
suppressStatus: true,
});
setActiveTVTab(settings.shared.mediaPanelActiveTab);
keyboardShortcuts = normalizeKeyboardShortcuts(settings.shared.keyboardShortcuts);
renderShortcutSettings();
if (!applyLayers) {
const layerVisibility = { ...(settings.shared.layerVisibility || {}) };
@@ -2659,12 +3136,14 @@ function applyCurrentViewportPanelVisibility({ persist = false } = {}) {
}
function syncSettingsToggle(panelId, visible) {
const input = document.querySelector(
const inputs = document.querySelectorAll(
`[data-settings-panel="${panelId}"]`,
);
if (input instanceof HTMLInputElement) {
input.checked = visible;
}
inputs.forEach((input) => {
if (input instanceof HTMLInputElement) {
input.checked = visible;
}
});
}
function syncAllHudPanelToggles() {
@@ -2717,14 +3196,14 @@ export function setDayNightInteractable(enabled) {
function getBoundaryPrecisionEls() {
return {
status: document.getElementById("boundary-precision-status"),
detail: document.getElementById("boundary-precision-detail"),
progressWrap: document.getElementById("boundary-precision-progress-wrap"),
progressBar: document.getElementById("boundary-precision-progress-bar"),
progressValue: document.getElementById("boundary-precision-progress-value"),
buildButton: document.getElementById("boundary-precision-build"),
rebuildButton: document.getElementById("boundary-precision-rebuild"),
disableButton: document.getElementById("boundary-precision-disable"),
statuses: Array.from(document.querySelectorAll("[data-boundary-precision-status]")),
details: Array.from(document.querySelectorAll("[data-boundary-precision-detail]")),
progressWraps: Array.from(document.querySelectorAll("[data-boundary-precision-progress-wrap]")),
progressBars: Array.from(document.querySelectorAll("[data-boundary-precision-progress-bar]")),
progressValues: Array.from(document.querySelectorAll("[data-boundary-precision-progress-value]")),
buildButtons: Array.from(document.querySelectorAll("[data-boundary-precision-build]")),
rebuildButtons: Array.from(document.querySelectorAll("[data-boundary-precision-rebuild]")),
disableButtons: Array.from(document.querySelectorAll("[data-boundary-precision-disable]")),
};
}
@@ -2764,45 +3243,48 @@ function renderBoundaryPrecisionStatus(payload = {}) {
? `高清国界更新源未配置完整:${job.message || job.code}`
: `高清国界下载失败:${job.message || job.code || "请检查更新源"}`;
if (els.status) {
els.status.textContent = "国界精度";
}
if (els.detail) {
els.detail.textContent = running
els.statuses.forEach((status) => {
status.textContent = "国界精度";
});
els.details.forEach((detail) => {
detail.textContent = running
? (job.message || "正在准备高清国界")
: failed
? failureMessage
: highReady
? (enabled ? "当前使用高精国界;可重新获取并构建。" : "高精国界已就绪,切到高精会立即应用。")
: "当前使用低精国界;切到高精会下载并构建。";
}
if (els.progressWrap) {
els.progressWrap.hidden = !running;
}
if (els.progressBar) {
els.progressBar.style.width = `${running || job.status === "succeeded" ? progress || 100 : progress}%`;
}
if (els.progressValue) {
els.progressValue.textContent = job.status === "failed"
});
els.progressWraps.forEach((progressWrap) => {
progressWrap.hidden = !running;
});
els.progressBars.forEach((progressBar) => {
progressBar.style.width = `${running || job.status === "succeeded" ? progress || 100 : progress}%`;
});
els.progressValues.forEach((progressValue) => {
progressValue.textContent = job.status === "failed"
? `失败:${job.message || job.code || "构建失败"}`
: `${running || job.status === "succeeded" ? progress || 100 : progress}%`;
}
if (els.buildButton instanceof HTMLButtonElement) {
els.buildButton.disabled = running;
els.buildButton.textContent = "高精";
els.buildButton.classList.toggle("is-active", enabled || running);
els.buildButton.setAttribute("aria-pressed", enabled || running ? "true" : "false");
}
if (els.rebuildButton instanceof HTMLButtonElement) {
});
els.buildButtons.forEach((buildButton) => {
if (!(buildButton instanceof HTMLButtonElement)) return;
buildButton.disabled = running;
buildButton.textContent = "高精";
buildButton.classList.toggle("is-active", enabled || running);
buildButton.setAttribute("aria-pressed", enabled || running ? "true" : "false");
});
els.rebuildButtons.forEach((rebuildButton) => {
if (!(rebuildButton instanceof HTMLButtonElement)) return;
const shouldShowRebuild = highReady && enabled && !running;
els.rebuildButton.hidden = !shouldShowRebuild;
els.rebuildButton.disabled = !shouldShowRebuild;
}
if (els.disableButton instanceof HTMLButtonElement) {
els.disableButton.disabled = running;
els.disableButton.classList.toggle("is-active", !enabled && !running);
els.disableButton.setAttribute("aria-pressed", !enabled && !running ? "true" : "false");
}
rebuildButton.hidden = !shouldShowRebuild;
rebuildButton.disabled = !shouldShowRebuild;
});
els.disableButtons.forEach((disableButton) => {
if (!(disableButton instanceof HTMLButtonElement)) return;
disableButton.disabled = running;
disableButton.classList.toggle("is-active", !enabled && !running);
disableButton.setAttribute("aria-pressed", !enabled && !running ? "true" : "false");
});
syncSegmentedControlSliders();
}
@@ -2855,7 +3337,7 @@ async function startBoundaryPrecisionBuild() {
async function setupBoundaryPrecisionControls() {
const els = getBoundaryPrecisionEls();
if (!els.buildButton && !els.disableButton) return;
if (els.buildButtons.length === 0 && els.disableButtons.length === 0) return;
try {
const payload = await refreshBoundaryPrecisionStatus();
const jobStatus = payload.current_job?.status;
@@ -2867,8 +3349,9 @@ async function setupBoundaryPrecisionControls() {
showStatusMessage(`高清国界状态读取失败:${error.message || error}`, "warning");
}
if (els.buildButton instanceof HTMLButtonElement) {
bindListener(els.buildButton, "click", async () => {
els.buildButtons.forEach((buildButton) => {
if (!(buildButton instanceof HTMLButtonElement)) return;
bindListener(buildButton, "click", async () => {
try {
const statusPayload = await refreshBoundaryPrecisionStatus();
const job = statusPayload.job || statusPayload.current_job || {};
@@ -2887,9 +3370,10 @@ async function setupBoundaryPrecisionControls() {
showStatusMessage(`高精国界切换失败:${error.message || error}`, "warning");
}
});
}
if (els.rebuildButton instanceof HTMLButtonElement) {
bindListener(els.rebuildButton, "click", async () => {
});
els.rebuildButtons.forEach((rebuildButton) => {
if (!(rebuildButton instanceof HTMLButtonElement)) return;
bindListener(rebuildButton, "click", async () => {
try {
await startBoundaryPrecisionBuild();
} catch (error) {
@@ -2897,9 +3381,10 @@ async function setupBoundaryPrecisionControls() {
showStatusMessage(`高精国界重建启动失败:${error.message || error}`, "warning");
}
});
}
if (els.disableButton instanceof HTMLButtonElement) {
bindListener(els.disableButton, "click", async () => {
});
els.disableButtons.forEach((disableButton) => {
if (!(disableButton instanceof HTMLButtonElement)) return;
bindListener(disableButton, "click", async () => {
try {
if (!getHighPrecisionBoundariesEnabled()) return;
setHighPrecisionBoundariesEnabled(false);
@@ -2910,7 +3395,204 @@ async function setupBoundaryPrecisionControls() {
showStatusMessage(`低精国界切换失败:${error.message || error}`, "warning");
}
});
});
}
function setSettingsTab(root, tabId) {
if (!(root instanceof HTMLElement)) return;
const nextTab = tabId || "runtime";
root.querySelectorAll("[data-settings-tab]").forEach((button) => {
if (!(button instanceof HTMLButtonElement)) return;
const active = button.dataset.settingsTab === nextTab;
button.classList.toggle("is-active", active);
button.setAttribute("aria-selected", active ? "true" : "false");
});
root.querySelectorAll("[data-settings-tab-panel]").forEach((panel) => {
if (!(panel instanceof HTMLElement)) return;
panel.hidden = panel.dataset.settingsTabPanel !== nextTab;
});
}
function setupSettingsTabs() {
const roots = document.querySelectorAll(".earth-settings-sheet, .earth-mobile-page--settings");
roots.forEach((root) => {
if (!(root instanceof HTMLElement)) return;
setSettingsTab(root, "runtime");
root.querySelectorAll("[data-settings-tab]").forEach((button) => {
if (!(button instanceof HTMLButtonElement)) return;
bindListener(button, "click", () => {
setSettingsTab(root, button.dataset.settingsTab || "runtime");
});
});
});
}
function renderShortcutSettings() {
const lists = document.querySelectorAll("[data-shortcut-list]");
if (lists.length === 0) return;
const groupedDefinitions = KEYBOARD_SHORTCUT_DEFINITIONS.reduce((groups, definition) => {
if (!groups.has(definition.category)) {
groups.set(definition.category, []);
}
groups.get(definition.category).push(definition);
return groups;
}, new Map());
lists.forEach((list) => {
if (!(list instanceof HTMLElement)) return;
list.innerHTML = "";
groupedDefinitions.forEach((definitions, category) => {
const heading = document.createElement("div");
heading.className = "earth-shortcut-category";
heading.textContent = category;
list.appendChild(heading);
definitions.forEach((definition) => {
const shortcut = getShortcutForAction(definition.id);
const row = document.createElement("div");
row.className = "earth-shortcut-row";
row.dataset.shortcutAction = definition.id;
row.innerHTML = `
<div class="earth-shortcut-copy">
<span class="earth-shortcut-label">${definition.label}</span>
<span class="earth-shortcut-alias">${getShortcutAliasesLabel(definition) || " "}</span>
</div>
<button class="earth-shortcut-key" type="button" data-shortcut-capture="${definition.id}">${getShortcutDisplayLabel(shortcut.binding)}</button>
<label class="earth-shortcut-enable" title="启用快捷键">
<input type="checkbox" data-shortcut-enabled="${definition.id}" ${shortcut.enabled ? "checked" : ""}>
<span class="earth-settings-switch-track"></span>
</label>
<button class="earth-shortcut-reset" type="button" data-shortcut-reset="${definition.id}" aria-label="重置${definition.label}" title="重置">
<span class="material-symbols-rounded">restart_alt</span>
</button>
`;
row.classList.toggle("is-disabled", !shortcut.enabled);
list.appendChild(row);
});
});
});
syncShortcutCaptureUi();
}
function syncShortcutCaptureUi() {
document.querySelectorAll("[data-shortcut-capture]").forEach((button) => {
if (!(button instanceof HTMLButtonElement)) return;
const actionId = button.dataset.shortcutCapture;
const isCapturing = Boolean(actionId && actionId === capturingShortcutActionId);
button.classList.toggle("is-capturing", isCapturing);
if (isCapturing) {
button.textContent = "按键...";
} else if (actionId) {
button.textContent = getShortcutDisplayLabel(getShortcutForAction(actionId).binding);
}
});
}
function setShortcutBinding(actionId, binding, { persist = true } = {}) {
if (!KEYBOARD_SHORTCUT_DEFINITION_BY_ID.has(actionId)) return false;
const normalizedBinding = normalizeShortcutBinding(binding);
if (!normalizedBinding) return false;
const owner = getShortcutOwnerByBinding(normalizedBinding, { excludeActionId: actionId });
if (owner) {
showStatusMessage(`快捷键已被「${owner.label}」使用`, "warning");
return false;
}
const nextShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
nextShortcuts[actionId] = {
...nextShortcuts[actionId],
binding: normalizedBinding,
enabled: true,
};
keyboardShortcuts = nextShortcuts;
if (earthSettingsState?.shared) {
earthSettingsState.shared.keyboardShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
}
renderShortcutSettings();
if (persist) persistEarthSettings();
return true;
}
function resetShortcutBinding(actionId, { persist = true } = {}) {
const definition = KEYBOARD_SHORTCUT_DEFINITION_BY_ID.get(actionId);
if (!definition) return;
const nextShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
nextShortcuts[actionId] = {
binding: definition.defaultBinding,
enabled: true,
};
keyboardShortcuts = nextShortcuts;
if (earthSettingsState?.shared) {
earthSettingsState.shared.keyboardShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
}
renderShortcutSettings();
if (persist) persistEarthSettings();
}
function setShortcutEnabled(actionId, enabled, { persist = true } = {}) {
if (!KEYBOARD_SHORTCUT_DEFINITION_BY_ID.has(actionId)) return false;
const currentShortcut = getShortcutForAction(actionId);
if (enabled) {
const owner = getShortcutOwnerByBinding(currentShortcut.binding, { excludeActionId: actionId });
if (owner) {
showStatusMessage(`快捷键已被「${owner.label}」使用`, "warning");
renderShortcutSettings();
return false;
}
}
const nextShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
nextShortcuts[actionId] = {
...nextShortcuts[actionId],
enabled: Boolean(enabled),
};
keyboardShortcuts = nextShortcuts;
if (earthSettingsState?.shared) {
earthSettingsState.shared.keyboardShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
}
renderShortcutSettings();
if (persist) persistEarthSettings();
return true;
}
function resetAllShortcutBindings() {
keyboardShortcuts = getDefaultKeyboardShortcuts();
if (earthSettingsState?.shared) {
earthSettingsState.shared.keyboardShortcuts = normalizeKeyboardShortcuts(keyboardShortcuts);
}
capturingShortcutActionId = null;
renderShortcutSettings();
persistEarthSettings();
showStatusMessage("快捷键已恢复默认", "info");
}
function setupShortcutSettingsControls() {
document.querySelectorAll("[data-shortcut-list]").forEach((list) => {
bindListener(list, "click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const captureButton = target?.closest("[data-shortcut-capture]");
const resetButton = target?.closest("[data-shortcut-reset]");
if (captureButton instanceof HTMLButtonElement) {
capturingShortcutActionId = captureButton.dataset.shortcutCapture || null;
syncShortcutCaptureUi();
} else if (resetButton instanceof HTMLButtonElement) {
const actionId = resetButton.dataset.shortcutReset;
if (actionId) resetShortcutBinding(actionId);
}
});
bindListener(list, "change", (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
const actionId = target.dataset.shortcutEnabled;
if (!actionId) return;
setShortcutEnabled(actionId, target.checked);
});
});
document.querySelectorAll("[data-shortcut-reset-all]").forEach((button) => {
bindListener(button, "click", () => {
resetAllShortcutBindings();
});
});
renderShortcutSettings();
}
function setupSettingsControls() {
@@ -2936,6 +3618,9 @@ function setupSettingsControls() {
resetEarthSettings();
});
setupSettingsTabs();
setupShortcutSettingsControls();
bindListener(settingsModal, "click", (event) => {
const sheet = event.target.closest(".earth-settings-sheet");
if (!sheet) {
@@ -4008,34 +4693,53 @@ function setupTerrainControls() {
function setupKeyboardControls() {
bindListener(document, "keydown", (event) => {
if (event.key !== "Escape") return;
if (isSearchPanelOpen()) {
closeSearchPanel();
if (capturingShortcutActionId) {
event.preventDefault();
event.stopPropagation();
if (event.key === "Escape") {
capturingShortcutActionId = null;
syncShortcutCaptureUi();
return;
}
const nextBinding = getShortcutChordFromEvent(event);
if (!nextBinding) return;
if (setShortcutBinding(capturingShortcutActionId, nextBinding)) {
const definition = KEYBOARD_SHORTCUT_DEFINITION_BY_ID.get(capturingShortcutActionId);
showStatusMessage(`${definition?.label || "快捷键"}已设置为 ${getShortcutDisplayLabel(nextBinding)}`, "info");
capturingShortcutActionId = null;
syncShortcutCaptureUi();
}
return;
}
if (isSettingsModalOpen()) {
closeSettingsModal();
const chord = getShortcutChordFromEvent(event);
const definition = getShortcutDefinitionForChord(chord);
if (!definition) return;
if (isEditableShortcutTarget(event.target) && definition.id !== "closeFocus") return;
if (
definition.id !== "closeFocus" &&
(isShortcutSuppressedBySettingsUi(event.target) ||
isSettingsModalOpen() ||
(isMobileLayout() && mobileDrawerOpen && mobileDrawerCard === "settings"))
) {
return;
}
if (isMobileLayout() && mobileDrawerOpen) {
setMobileDrawerState({ open: false });
return;
}
if (isFloatingMenuVisible()) {
closeFloatingMenus();
return;
}
if (toolbarHubController?.isOpen?.()) {
toolbarHubController.close();
return;
}
clearLockedObjectAndInfo();
event.preventDefault();
event.stopPropagation();
executeKeyboardShortcut(definition.id);
});
bindListener(document, "keyup", (event) => {
const chord = getShortcutChordFromEvent(event);
const definition = getShortcutDefinitionForChord(chord);
if (!definition || !isKeyboardRotationAction(definition.id)) return;
stopKeyboardRotationControl({ actionId: definition.id });
});
bindListener(window, "blur", () => {
stopKeyboardRotationControl({ restoreAutoRotate: true, clearVelocity: true });
});
cleanupFns.push(() => {
stopKeyboardRotationControl({ restoreAutoRotate: true, clearVelocity: true });
});
}

View File

@@ -255,7 +255,7 @@ import {
refreshLegend,
setLegendItems,
} from "./legend.js";
import { mountBrand } from "./brand.js";
import { fetchEarthBrandConfig, mountBrand } from "./brand.js";
import { initTVPanel } from "./tv.js";
import { initNewsPanel, updateNewsViewFocus } from "./news.js";
import { initSearchPanel } from "./search.js";
@@ -3569,6 +3569,13 @@ export function init() {
updateHudScale();
const brandRoot = document.getElementById("brand-root");
mountBrand(brandRoot, HUD_CONFIG.brandLanguage);
fetchEarthBrandConfig()
.then((brandConfig) => {
mountBrand(brandRoot, brandConfig);
})
.catch((error) => {
console.warn("Earth brand config unavailable, using defaults.", error);
});
initTVPanel();
initNewsPanel();
initSearchPanel({

View File

@@ -328,7 +328,7 @@ function renderTicker(nextPayload) {
.map((item) => `
<span class="earth-news-ticker__item" data-news-id="${escapeTickerText(item.id || "")}">
<span class="earth-news-ticker__source">${escapeTickerText(item.source || item.feed_name || "NEWS")}</span>
<span>${escapeTickerText(getNewsDisplayTitle(item))}</span>
<span>${escapeTickerText(getNewsDisplaySummary(item))}</span>
</span>
`)
.join("");
@@ -434,11 +434,12 @@ function renderPayload(nextPayload) {
: "news-story-card";
const title = getNewsDisplayTitle(item);
const summaryText = getNewsDisplaySummary(item);
const leadText = summaryText || title;
const regionLabel = item.display_region || getNewsRegionLabel(item.region);
const feedLabel = getNewsFeedLabel(item.feed_name);
const statusLabel = getNewsEnrichmentStatusLabel(item.enrichment_status);
const summary = summaryText
? `<div class="news-story-summary">${escapeNewsHtml(summaryText)}</div>`
const summary = title && title !== leadText
? `<div class="news-story-summary">${escapeNewsHtml(title)}</div>`
: "";
return `
<a class="${cardClass}" data-news-id="${item.id}" href="${item.url}" target="_blank" rel="noreferrer noopener">
@@ -446,7 +447,7 @@ function renderPayload(nextPayload) {
<span class="news-story-source">${escapeNewsHtml(item.source || "NEWS")}</span>
<span class="news-story-time">${formatRelativeTime(item.published_at)}</span>
</div>
<div class="news-story-title">${escapeNewsHtml(title)}</div>
<div class="news-story-title">${escapeNewsHtml(leadText)}</div>
${summary}
<div class="news-story-tags">
<span class="news-story-tag">${escapeNewsHtml(regionLabel)}</span>