release: bump version to 0.27.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
linkong
2026-04-14 12:52:05 +08:00
parent 48eb13b993
commit 93c1c1e550
12 changed files with 346 additions and 143 deletions

View File

@@ -1 +1 @@
0.27.2 0.27.3

View File

@@ -8,6 +8,17 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合) - `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## [0.27.3] — 2026-04-14
### 🔧 Improvements
- TV panel meta 折叠展开方向稳定:底部锚定时向上生长,拖拽后(顶部锚定)通过 JS 补偿 top 保持播放器底部位置不变
- 修复 TV panel 展开/折叠时视频区域跳动问题:移除面板 min-height使播放器高度在两种状态下保持一致
- 修正 TV panel meta toggle 箭头方向:展开朝下,折叠朝上
- 修复图例面板折叠按钮失效legend-bar-btn 补充进拖拽排除列表)
- 调整图层搜索框图标尺寸为 20pxBR 缩放角标改为直角 L 形
---
## [0.27.2] — 2026-04-14 ## [0.27.2] — 2026-04-14
### 🔧 Improvements ### 🔧 Improvements

View File

@@ -16,12 +16,13 @@
## Current Version ## Current Version
- `main` 当前主线历史推导到:`0.16.5` - `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.27.2` - `dev` 当前开发分支历史推导到:`0.27.3`
## Timeline ## Timeline
| Version | Type | Branch | Commit | Summary | | Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `0.27.3` | improvement | `dev` | — | TV panel 折叠方向稳定、视频跳动修复、图例折叠按钮修复、搜索图标调整 |
| `0.27.2` | improvement | `dev` | — | 修复 brand copy 宽度问题,提取 --brand-copy-width CSS 变量 | | `0.27.2` | improvement | `dev` | — | 修复 brand copy 宽度问题,提取 --brand-copy-width CSS 变量 |
| `0.27.1` | improvement | `dev` | — | HUD 面板拖拽 L 形边界约束、brand 组件整体缩放、图层面板宽度优化、搜索叉叉修复 | | `0.27.1` | improvement | `dev` | — | HUD 面板拖拽 L 形边界约束、brand 组件整体缩放、图层面板宽度优化、搜索叉叉修复 |
| `0.27.0` | feature | `dev` | — | Earth HUD 重构图层面板、信息卡片悬浮定位、Fresnel 大气层渲染 | | `0.27.0` | feature | `dev` | — | Earth HUD 重构图层面板、信息卡片悬浮定位、Fresnel 大气层渲染 |

View File

@@ -1,6 +1,6 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.27.2", "version": "0.27.3",
"private": true, "private": true,
"packageManager": "bun@1", "packageManager": "bun@1",
"dependencies": { "dependencies": {

View File

@@ -77,6 +77,8 @@
.hud-panel-header .hud-panel-title { .hud-panel-header .hud-panel-title {
margin-bottom: 0; margin-bottom: 0;
color: var(--hud-text-soft);
font-size: calc(0.82rem * var(--hud-scale));
} }
.hud-panel-drag-handle { .hud-panel-drag-handle {

View File

@@ -38,7 +38,7 @@
.layer-panel-title { .layer-panel-title {
flex: 1 1 auto; flex: 1 1 auto;
margin: 0; margin: 0;
color: var(--hud-title); color: var(--hud-text-soft);
font-size: calc(0.82rem * var(--hud-scale)); font-size: calc(0.82rem * var(--hud-scale));
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@@ -76,30 +76,51 @@
transition: transform 0.22s ease; transition: transform 0.22s ease;
} }
/* Chevron rotates when collapsed */ /* Chevron:展开时朝上(可折叠),折叠时朝下(可展开) */
.layer-panel--collapsed .layer-panel-btn .material-symbols-rounded { .layer-panel-btn .material-symbols-rounded {
transform: rotate(180deg); transform: rotate(180deg);
} }
.layer-panel--collapsed .layer-panel-btn .material-symbols-rounded {
transform: rotate(0deg);
}
/* ── Search bar ───────────────────────────────────────────────── */ /* ── Search bar ───────────────────────────────────────────────── */
.layer-panel-search { .layer-panel-search {
padding: calc(6px * var(--hud-scale)) calc(8px * var(--hud-scale));
border-bottom: 1px solid var(--hud-line);
}
.layer-panel-search-box {
display: flex; display: flex;
align-items: center; align-items: center;
gap: calc(5px * var(--hud-scale)); gap: calc(5px * var(--hud-scale));
padding: calc(6px * var(--hud-scale)) calc(10px * var(--hud-scale)); padding: calc(5px * var(--hud-scale)) calc(8px * var(--hud-scale));
border-bottom: 1px solid var(--hud-line); border: 1px solid rgba(201, 225, 247, 0.14);
border-radius: calc(8px * var(--hud-scale));
background: rgba(255, 255, 255, 0.04);
transition: border-color 0.18s ease;
box-sizing: border-box;
height: calc(38px * var(--hud-scale));
}
.layer-panel-search-box:focus-within {
border-color: rgba(201, 225, 247, 0.28);
} }
.layer-panel-search-icon { .layer-panel-search-icon {
flex-shrink: 0; flex-shrink: 0;
font-size: calc(14px * var(--hud-scale));
color: var(--hud-text-soft); color: var(--hud-text-soft);
line-height: 1; line-height: 1;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
pointer-events: none; pointer-events: none;
} }
.layer-panel-search-icon.material-symbols-rounded {
font-size: calc(20px * var(--hud-scale));
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
}
.layer-panel-search-input { .layer-panel-search-input {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;

View File

@@ -6,17 +6,34 @@
width: calc(420px * var(--hud-scale)); width: calc(420px * var(--hud-scale));
max-width: calc(100vw - 32px); max-width: calc(100vw - 32px);
min-width: calc(300px * var(--hud-scale)); min-width: calc(300px * var(--hud-scale));
min-height: calc(340px * var(--hud-scale)); padding: calc(10px * var(--hud-scale));
padding: calc(18px * var(--hud-scale));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--hud-gap-sm); gap: var(--hud-gap-sm);
z-index: 18; z-index: 18;
} }
.tv-panel-header-copy { /* header 内嵌 select + actions */
display: grid; .hud-panel-tv .hud-panel-header {
gap: calc(3px * var(--hud-scale)); align-items: center;
gap: var(--hud-gap-xs);
}
.hud-panel-tv .hud-panel-header .hud-panel-close {
align-self: center;
}
.tv-panel-header-title {
flex: 0 0 auto;
white-space: nowrap;
margin: 0;
}
/* select / action 在 drag-handle 内,恢复正常指针 */
.hud-panel-tv .hud-panel-header .tv-panel-select,
.hud-panel-tv .hud-panel-header .tv-panel-action {
cursor: pointer;
user-select: auto;
} }
.tv-panel-status { .tv-panel-status {
@@ -26,13 +43,6 @@
text-transform: uppercase; text-transform: uppercase;
} }
.tv-panel-controls {
display: flex;
gap: var(--hud-gap-sm);
align-items: center;
min-width: 0;
}
.tv-panel-select { .tv-panel-select {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
@@ -59,28 +69,25 @@
} }
.tv-panel-action { .tv-panel-action {
border: 1px solid rgba(201, 225, 247, 0.12); border: 1px solid transparent;
border-radius: calc(12px * var(--hud-scale)); border-radius: calc(4px * var(--hud-scale));
background: rgba(255, 255, 255, 0.05); background: transparent;
color: var(--hud-text); color: var(--hud-text-muted);
padding: calc(10px * var(--hud-scale)) calc(12px * var(--hud-scale)); padding: calc(7px * var(--hud-scale));
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
white-space: nowrap;
font-size: calc(0.84rem * var(--hud-scale));
line-height: 1;
cursor: pointer; cursor: pointer;
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; transition:
} background 0.18s ease,
border-color 0.18s ease,
.tv-panel-action--icon { color 0.18s ease,
padding: calc(9px * var(--hud-scale)); transform 0.18s ease,
border-radius: calc(10px * var(--hud-scale)); opacity 0.18s ease;
} }
.tv-panel-action--icon .material-symbols-rounded { .tv-panel-action--icon .material-symbols-rounded {
font-size: calc(18px * var(--hud-scale)); font-size: calc(16px * var(--hud-scale));
line-height: 1; line-height: 1;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
pointer-events: none; pointer-events: none;
@@ -88,7 +95,7 @@
.tv-panel-action:hover:not(:disabled) { .tv-panel-action:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
border-color: rgba(225, 239, 255, 0.2); border-color: rgba(225, 239, 255, 0.14);
color: var(--hud-accent-strong); color: var(--hud-accent-strong);
} }
@@ -97,6 +104,32 @@
cursor: not-allowed; cursor: not-allowed;
} }
/* toggle 箭头:展开时朝下,折叠时朝上 */
.tv-panel-meta-toggle .material-symbols-rounded {
transition: transform 0.22s ease;
transform: rotate(0deg);
}
.tv-panel-meta-toggle.is-collapsed .material-symbols-rounded {
transform: rotate(180deg);
}
/* meta wrap折叠时用负 margin 抵消 flex gap无死区 */
.tv-panel-meta-wrap {
overflow: hidden;
max-height: calc(120px * var(--hud-scale));
opacity: 1;
transition: max-height 0.22s ease, opacity 0.18s ease, margin 0.22s ease;
}
.tv-panel-meta-wrap.is-collapsed {
max-height: 0;
opacity: 0;
pointer-events: none;
margin-top: calc(-1 * var(--hud-gap-sm));
margin-bottom: calc(-1 * var(--hud-gap-sm));
}
.tv-panel-meta { .tv-panel-meta {
display: grid; display: grid;
gap: calc(4px * var(--hud-scale)); gap: calc(4px * var(--hud-scale));
@@ -129,7 +162,7 @@
.tv-panel-player { .tv-panel-player {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 0 auto;
min-height: calc(220px * var(--hud-scale)); min-height: calc(220px * var(--hud-scale));
border-radius: calc(16px * var(--hud-scale)); border-radius: calc(16px * var(--hud-scale));
overflow: hidden; overflow: hidden;
@@ -163,33 +196,65 @@
background: #050a14; background: #050a14;
} }
.tv-panel-resize-handle { /* ── Multi-edge resize handles ───────────────────────────────── */
.tv-panel-edge {
position: absolute; position: absolute;
right: calc(8px * var(--hud-scale)); z-index: 10;
bottom: calc(8px * var(--hud-scale));
width: calc(18px * var(--hud-scale));
height: calc(18px * var(--hud-scale));
border: 0;
padding: 0;
background: transparent;
cursor: nwse-resize;
z-index: 2;
} }
.tv-panel-resize-handle::before { .tv-panel-edge[data-edge="r"] {
right: 0;
top: calc(12px * var(--hud-scale));
bottom: calc(12px * var(--hud-scale));
width: calc(6px * var(--hud-scale));
cursor: ew-resize;
}
.tv-panel-edge[data-edge="b"] {
bottom: 0;
left: calc(12px * var(--hud-scale));
right: calc(12px * var(--hud-scale));
height: calc(6px * var(--hud-scale));
cursor: ns-resize;
}
.tv-panel-edge[data-edge="l"] {
left: 0;
top: calc(12px * var(--hud-scale));
bottom: calc(12px * var(--hud-scale));
width: calc(6px * var(--hud-scale));
cursor: ew-resize;
}
.tv-panel-edge[data-edge="br"] {
right: 0;
bottom: 0;
width: calc(20px * var(--hud-scale));
height: calc(20px * var(--hud-scale));
cursor: nwse-resize;
}
.tv-panel-edge[data-edge="bl"] {
left: 0;
bottom: 0;
width: calc(20px * var(--hud-scale));
height: calc(20px * var(--hud-scale));
cursor: nesw-resize;
}
/* 右下角视觉标记 */
.tv-panel-edge[data-edge="br"]::before {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: calc(4px * var(--hud-scale));
border-right: 2px solid rgba(223, 235, 248, 0.46); border-right: 2px solid rgba(223, 235, 248, 0.4);
border-bottom: 2px solid rgba(223, 235, 248, 0.46); border-bottom: 2px solid rgba(223, 235, 248, 0.4);
border-bottom-right-radius: calc(10px * var(--hud-scale)); transition: border-color 0.18s ease;
opacity: 0.78;
transition: opacity 0.18s ease, border-color 0.18s ease;
} }
.tv-panel-resize-handle:hover::before { .tv-panel-edge[data-edge="br"]:hover::before {
opacity: 1; border-color: rgba(244, 249, 255, 0.75);
border-color: rgba(244, 249, 255, 0.78);
} }
.hud-panel-tv.is-resizing { .hud-panel-tv.is-resizing {
@@ -197,6 +262,25 @@
user-select: none; user-select: none;
} }
/* Whole panel is draggable; player overrides back to default */
.hud-panel-tv:not(.is-resizing) {
cursor: grab;
}
.hud-panel-tv.is-dragging {
cursor: grabbing;
}
.hud-panel-tv .tv-panel-player {
cursor: default;
}
/* Disable iframe/video pointer capture while dragging so mouse events pass through */
.hud-panel-tv.is-dragging .tv-panel-iframe,
.hud-panel-tv.is-dragging .tv-panel-video {
pointer-events: none;
}
.earth-app.layout-expanded .hud-panel-tv:not([data-dragged="true"]) { .earth-app.layout-expanded .hud-panel-tv:not([data-dragged="true"]) {
bottom: var(--hud-offset); bottom: var(--hud-offset);
right: var(--hud-offset); right: var(--hud-offset);

View File

@@ -50,7 +50,7 @@
</defs> </defs>
</svg> </svg>
<div id="container" class="earth-app"> <div id="container" class="earth-app">
<div class="earth-left-column"> <div id="left-column" class="earth-left-column">
<div id="brand-panel" class="hud-panel hud-panel-brand"> <div id="brand-panel" class="hud-panel hud-panel-brand">
<div id="brand-root"></div> <div id="brand-root"></div>
</div> </div>
@@ -69,18 +69,20 @@
<div class="layer-panel-body" id="layer-panel-body"> <div class="layer-panel-body" id="layer-panel-body">
<!-- Search --> <!-- Search -->
<div class="layer-panel-search"> <div class="layer-panel-search">
<span class="material-symbols-rounded layer-panel-search-icon">search</span> <div class="layer-panel-search-box">
<input <span class="material-symbols-rounded layer-panel-search-icon">search</span>
type="text" <input
id="layer-search-input" type="text"
class="layer-panel-search-input" id="layer-search-input"
placeholder="搜索图层..." class="layer-panel-search-input"
autocomplete="off" placeholder="搜索图层..."
spellcheck="false" autocomplete="off"
> spellcheck="false"
<button id="layer-search-clear" class="layer-panel-btn layer-search-clear" type="button" aria-label="清除搜索" title="清除" hidden> >
<span class="material-symbols-rounded">close</span> <button id="layer-search-clear" class="layer-panel-btn layer-search-clear" type="button" aria-label="清除搜索" title="清除" hidden>
</button> <span class="material-symbols-rounded">close</span>
</button>
</div>
</div> </div>
<!-- Layer rows --> <!-- Layer rows -->
@@ -299,17 +301,9 @@
<span id="camera-distance" hidden></span> <span id="camera-distance" hidden></span>
</div> </div>
<div id="tv-panel" class="hud-panel hud-panel-tv hud-panel-draggable" data-panel-key="tv-panel"> <div id="tv-panel" class="hud-panel hud-panel-tv hud-panel-draggable" data-panel-key="tv-panel" data-drag-self="true">
<div class="hud-panel-header hud-panel-drag-handle"> <div class="hud-panel-header hud-panel-drag-handle">
<div class="tv-panel-header-copy"> <span class="hud-panel-title tv-panel-header-title">新闻直播</span>
<h3 class="hud-panel-title">新闻直播</h3>
<span id="tv-source-status" class="tv-panel-status">等待加载直播源</span>
</div>
<button class="hud-panel-close" type="button" data-close-panel="tv-panel" aria-label="关闭电视直播">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="tv-panel-controls">
<select id="tv-source-select" class="tv-panel-select" aria-label="选择新闻直播源"></select> <select id="tv-source-select" class="tv-panel-select" aria-label="选择新闻直播源"></select>
<div class="tv-panel-actions"> <div class="tv-panel-actions">
<button id="tv-refresh" class="tv-panel-action tv-panel-action--icon" type="button" title="刷新直播源" aria-label="刷新直播源"> <button id="tv-refresh" class="tv-panel-action tv-panel-action--icon" type="button" title="刷新直播源" aria-label="刷新直播源">
@@ -318,13 +312,22 @@
<button id="tv-open-external" class="tv-panel-action tv-panel-action--icon" type="button" title="访问官网" aria-label="访问官网"> <button id="tv-open-external" class="tv-panel-action tv-panel-action--icon" type="button" title="访问官网" aria-label="访问官网">
<span class="material-symbols-rounded">open_in_new</span> <span class="material-symbols-rounded">open_in_new</span>
</button> </button>
<button id="tv-meta-toggle" class="tv-panel-action tv-panel-action--icon tv-panel-meta-toggle" type="button" title="频道信息" aria-label="频道信息">
<span class="material-symbols-rounded">expand_more</span>
</button>
</div> </div>
<button class="hud-panel-close" type="button" data-close-panel="tv-panel" aria-label="关闭电视直播">
<span class="material-symbols-rounded">close</span>
</button>
</div> </div>
<div class="tv-panel-meta"> <div class="tv-panel-meta-wrap" id="tv-meta-wrap">
<div id="tv-source-title" class="tv-panel-title">暂无可用频道</div> <div class="tv-panel-meta" id="tv-panel-meta">
<div id="tv-source-meta" class="tv-panel-subtitle">当前未配置可播放新闻直播源</div> <span id="tv-source-status" class="tv-panel-status">等待加载直播源</span>
<div id="tv-source-catalog" class="tv-panel-catalog">频道目录待同步</div> <div id="tv-source-title" class="tv-panel-title">暂无可用频道</div>
<div id="tv-source-notes" class="tv-panel-notes">支持后台配置默认源与采集器补充源。</div> <div id="tv-source-meta" class="tv-panel-subtitle">当前未配置可播放新闻直播源</div>
<div id="tv-source-catalog" class="tv-panel-catalog">频道目录待同步</div>
<div id="tv-source-notes" class="tv-panel-notes">支持后台配置默认源与采集器补充源。</div>
</div>
</div> </div>
<div class="tv-panel-player"> <div class="tv-panel-player">
<div id="tv-empty-state" class="tv-panel-empty">暂无可播放直播源,请先在系统配置中添加频道。</div> <div id="tv-empty-state" class="tv-panel-empty">暂无可播放直播源,请先在系统配置中添加频道。</div>
@@ -338,13 +341,11 @@
></iframe> ></iframe>
<video id="tv-video" class="tv-panel-video" hidden controls autoplay muted playsinline></video> <video id="tv-video" class="tv-panel-video" hidden controls autoplay muted playsinline></video>
</div> </div>
<button <div class="tv-panel-edge" data-edge="r"></div>
id="tv-resize-handle" <div class="tv-panel-edge" data-edge="b"></div>
class="tv-panel-resize-handle" <div class="tv-panel-edge" data-edge="l"></div>
type="button" <div class="tv-panel-edge" data-edge="br"></div>
aria-label="调整电视直播窗口大小" <div class="tv-panel-edge" data-edge="bl"></div>
title="调整大小"
></button>
</div> </div>
<div id="loading" class="earth-loading"> <div id="loading" class="earth-loading">

View File

@@ -174,7 +174,9 @@ function setupDraggableHudPanels() {
if (!app || draggablePanels.length === 0) return; if (!app || draggablePanels.length === 0) return;
draggablePanels.forEach((panel) => { draggablePanels.forEach((panel) => {
const handle = panel.querySelector(".hud-panel-drag-handle"); const handle = panel.dataset.dragSelf === "true"
? panel
: panel.querySelector(".hud-panel-drag-handle");
if (!handle) return; if (!handle) return;
let isDragging = false; let isDragging = false;
@@ -228,7 +230,7 @@ function setupDraggableHudPanels() {
}; };
bindListener(handle, "pointerdown", (event) => { bindListener(handle, "pointerdown", (event) => {
if (event.target.closest(".hud-panel-close, .layer-panel-btn, .info-card-close")) return; if (event.target.closest(".hud-panel-close, .layer-panel-btn, .info-card-close, .tv-panel-select, .tv-panel-action, .tv-panel-player, .tv-panel-edge, .legend-bar-btn")) return;
isDragging = true; isDragging = true;
startPointerX = event.clientX; startPointerX = event.clientX;
startPointerY = event.clientY; startPointerY = event.clientY;
@@ -238,6 +240,8 @@ function setupDraggableHudPanels() {
// If panel is inside a flow container (not a direct child of app), reparent // If panel is inside a flow container (not a direct child of app), reparent
// it so absolute positioning is relative to the app container. // it so absolute positioning is relative to the app container.
if (panel.parentElement !== app) { if (panel.parentElement !== app) {
panel.dataset.originalParentId = panel.parentElement?.id || "";
panel.dataset.originalNextSiblingId = panel.nextElementSibling?.id || "";
const capturedWidth = panelRect.width; const capturedWidth = panelRect.width;
panel.style.position = "absolute"; panel.style.position = "absolute";
panel.style.width = `${capturedWidth}px`; panel.style.width = `${capturedWidth}px`;
@@ -929,11 +933,28 @@ function updateLayoutUI(container) {
} }
function resetPanelInlineLayout(panel) { function resetPanelInlineLayout(panel) {
const originalParentId = panel.dataset.originalParentId;
if (originalParentId) {
const originalParent = document.getElementById(originalParentId);
if (originalParent) {
const nextId = panel.dataset.originalNextSiblingId;
const nextSibling = nextId ? document.getElementById(nextId) : null;
if (nextSibling) {
originalParent.insertBefore(panel, nextSibling);
} else {
originalParent.appendChild(panel);
}
}
delete panel.dataset.originalParentId;
delete panel.dataset.originalNextSiblingId;
}
panel.style.left = ""; panel.style.left = "";
panel.style.top = ""; panel.style.top = "";
panel.style.right = ""; panel.style.right = "";
panel.style.bottom = ""; panel.style.bottom = "";
panel.style.transform = ""; panel.style.transform = "";
panel.style.position = "";
panel.style.width = "";
delete panel.dataset.dragged; delete panel.dataset.dragged;
} }

View File

@@ -20,6 +20,9 @@ let initialized = false;
let refreshPromise = null; let refreshPromise = null;
let hlsPlayer = null; let hlsPlayer = null;
let hlsRecoveryAttempts = 0; let hlsRecoveryAttempts = 0;
let metaAutoCollapseTimer = null;
const META_AUTO_COLLAPSE_DELAY = 2500;
const HLS_MAX_RECOVERY_ATTEMPTS = 3; const HLS_MAX_RECOVERY_ATTEMPTS = 3;
const HLS_RETRY_CONFIG = { const HLS_RETRY_CONFIG = {
@@ -33,7 +36,7 @@ function getElements() {
return { return {
panel: document.getElementById("tv-panel"), panel: document.getElementById("tv-panel"),
toggleBtn: document.getElementById("toggle-tv"), toggleBtn: document.getElementById("toggle-tv"),
resizeHandle: document.getElementById("tv-resize-handle"), resizeHandle: null,
select: document.getElementById("tv-source-select"), select: document.getElementById("tv-source-select"),
title: document.getElementById("tv-source-title"), title: document.getElementById("tv-source-title"),
meta: document.getElementById("tv-source-meta"), meta: document.getElementById("tv-source-meta"),
@@ -45,9 +48,51 @@ function getElements() {
empty: document.getElementById("tv-empty-state"), empty: document.getElementById("tv-empty-state"),
refreshBtn: document.getElementById("tv-refresh"), refreshBtn: document.getElementById("tv-refresh"),
openBtn: document.getElementById("tv-open-external"), openBtn: document.getElementById("tv-open-external"),
metaPanel: document.getElementById("tv-panel-meta"),
metaWrap: document.getElementById("tv-meta-wrap"),
metaToggle: document.getElementById("tv-meta-toggle"),
}; };
} }
function setMetaCollapsed(collapsed) {
const { metaWrap, metaToggle, panel } = getElements();
if (!metaWrap) return;
const isDragged = panel?.dataset.dragged === "true" && panel.style.top;
if (isDragged) {
// Top-anchored panel: toggle instantly and compensate top so the player
// (panel bottom) stays visually fixed.
const bottomBefore = panel.getBoundingClientRect().bottom;
metaWrap.style.transition = "none";
metaWrap.classList.toggle("is-collapsed", collapsed);
metaToggle?.classList.toggle("is-collapsed", collapsed);
// Force synchronous reflow to get updated panel height
void panel.offsetHeight;
const delta = panel.getBoundingClientRect().bottom - bottomBefore;
if (delta !== 0) {
panel.style.top = `${parseFloat(panel.style.top) - delta}px`;
}
// Restore CSS transition after this paint
requestAnimationFrame(() => {
metaWrap.style.transition = "";
});
} else {
metaWrap.classList.toggle("is-collapsed", collapsed);
metaToggle?.classList.toggle("is-collapsed", collapsed);
}
}
function autoExpandMeta() {
clearTimeout(metaAutoCollapseTimer);
setMetaCollapsed(false);
metaAutoCollapseTimer = setTimeout(() => setMetaCollapsed(true), META_AUTO_COLLAPSE_DELAY);
}
function clearPanelPositioningForResize(panel) { function clearPanelPositioningForResize(panel) {
panel.style.left = `${panel.offsetLeft}px`; panel.style.left = `${panel.offsetLeft}px`;
panel.style.top = `${panel.offsetTop}px`; panel.style.top = `${panel.offsetTop}px`;
@@ -65,71 +110,80 @@ function getHudScale() {
} }
function setupResizeHandle() { function setupResizeHandle() {
const { panel, resizeHandle } = getElements(); const { panel } = getElements();
const container = document.getElementById("container"); const container = document.getElementById("container");
if (!(panel instanceof HTMLElement) || !(resizeHandle instanceof HTMLElement) || !(container instanceof HTMLElement)) { if (!(panel instanceof HTMLElement) || !(container instanceof HTMLElement)) return;
return;
}
let resizing = false; let resizing = false;
let activeEdge = "";
let startX = 0; let startX = 0;
let startY = 0; let startY = 0;
let startWidth = 0; let startWidth = 0;
let startHeight = 0; let startHeight = 0;
let startLeft = 0;
let startTop = 0;
const stopResize = () => { const stopResize = () => {
resizing = false; resizing = false;
activeEdge = "";
panel.classList.remove("is-resizing"); panel.classList.remove("is-resizing");
document.body.style.userSelect = ""; document.body.style.userSelect = "";
}; };
resizeHandle.addEventListener("pointerdown", (event) => { const onMove = (event) => {
if (document.getElementById("container")?.classList.contains("layout-expanded")) {
return;
}
event.preventDefault();
event.stopPropagation();
resizing = true;
startX = event.clientX;
startY = event.clientY;
clearPanelPositioningForResize(panel);
const rect = panel.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
panel.classList.add("is-resizing");
document.body.style.userSelect = "none";
resizeHandle.setPointerCapture?.(event.pointerId);
});
resizeHandle.addEventListener("pointermove", (event) => {
if (!resizing) return; if (!resizing) return;
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
const currentLeft = panelRect.left - containerRect.left;
const currentTop = panelRect.top - containerRect.top;
const hudScale = getHudScale(); const hudScale = getHudScale();
const minWidth = Math.max(320, Math.round(360 * hudScale)); const minWidth = Math.max(320, Math.round(360 * hudScale));
const minHeight = Math.max(260, Math.round(340 * hudScale)); const minHeight = Math.max(260, Math.round(340 * hudScale));
const maxWidth = Math.max(minWidth, containerRect.width - currentLeft - 12); const dx = event.clientX - startX;
const maxHeight = Math.max(minHeight, containerRect.height - currentTop - 12); const dy = event.clientY - startY;
const nextWidth = Math.min(
maxWidth,
Math.max(minWidth, startWidth + (event.clientX - startX)),
);
const nextHeight = Math.min(
maxHeight,
Math.max(minHeight, startHeight + (event.clientY - startY)),
);
panel.style.width = `${nextWidth}px`; if (activeEdge.includes("r")) {
panel.style.minHeight = `${nextHeight}px`; const maxW = containerRect.width - startLeft - 12;
panel.style.width = `${Math.min(maxW, Math.max(minWidth, startWidth + dx))}px`;
}
if (activeEdge.includes("l")) {
const newW = Math.max(minWidth, startWidth - dx);
panel.style.width = `${newW}px`;
panel.style.left = `${Math.max(0, startLeft + startWidth - newW)}px`;
}
if (activeEdge.includes("b")) {
const maxH = containerRect.height - startTop - 12;
panel.style.minHeight = `${Math.min(maxH, Math.max(minHeight, startHeight + dy))}px`;
}
};
panel.querySelectorAll(".tv-panel-edge[data-edge]").forEach((edgeEl) => {
edgeEl.addEventListener("pointerdown", (event) => {
if (container.classList.contains("layout-expanded")) return;
event.preventDefault();
event.stopPropagation();
activeEdge = edgeEl.dataset.edge ?? "";
resizing = true;
startX = event.clientX;
startY = event.clientY;
clearPanelPositioningForResize(panel);
const rect = panel.getBoundingClientRect();
const cRect = container.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
startLeft = rect.left - cRect.left;
startTop = rect.top - cRect.top;
panel.classList.add("is-resizing");
document.body.style.userSelect = "none";
edgeEl.setPointerCapture?.(event.pointerId);
});
edgeEl.addEventListener("pointermove", onMove);
edgeEl.addEventListener("pointerup", stopResize);
edgeEl.addEventListener("pointercancel", stopResize);
edgeEl.addEventListener("lostpointercapture", stopResize);
}); });
resizeHandle.addEventListener("pointerup", stopResize);
resizeHandle.addEventListener("pointercancel", stopResize);
resizeHandle.addEventListener("lostpointercapture", stopResize);
} }
function updateToggleButton(visible) { function updateToggleButton(visible) {
@@ -502,6 +556,7 @@ function renderSource(source) {
source.id === tvPayload?.default_source_id ? "当前正在播放默认源" : "当前正在播放已选频道", source.id === tvPayload?.default_source_id ? "当前正在播放默认源" : "当前正在播放已选频道",
); );
updateOpenButton(source); updateOpenButton(source);
autoExpandMeta();
} }
function resolveInitialSourceId() { function resolveInitialSourceId() {
@@ -592,6 +647,13 @@ export function initTVPanel() {
renderSource(findSourceById(currentSourceId)); renderSource(findSourceById(currentSourceId));
}); });
const { metaToggle } = getElements();
metaToggle?.addEventListener("click", () => {
clearTimeout(metaAutoCollapseTimer);
const isNowCollapsed = !metaToggle.classList.contains("is-collapsed");
setMetaCollapsed(isNowCollapsed);
});
refreshBtn?.addEventListener("click", () => { refreshBtn?.addEventListener("click", () => {
refreshTVPanel(); refreshTVPanel();
}); });

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "planet" name = "planet"
version = "0.27.2" version = "0.27.3"
description = "智能星球计划 - 态势感知系统" description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]] [[package]]
name = "planet" name = "planet"
version = "0.27.2" version = "0.27.3"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },