From 42a0bbba356de0221d2fafa0c6a8ab178f7ad39d Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 11:03:43 +0800 Subject: [PATCH 01/29] style(webview): clean IDE-tool look with softer chrome Tune shared tokens and controls toward more whitespace and fewer gray border boxes: lighter --bg-row, softer --border, slightly larger spacing scale, lighter secondary/mini/icon buttons, and cards that rely more on background than heavy outlines. Accent stays on primary actions and status. Panel CSS polished to match; design-system spacing hard rules updated. --- docs/design-system.en.md | 2 +- docs/design-system.md | 2 +- media/annotationPanel/conflict.css | 10 +-- media/annotationPanel/style.css | 12 ++-- media/characterManager/characterManager.css | 48 ++++++------- media/console/console.css | 79 ++++++++++----------- media/shared/controls.css | 40 +++++++---- media/shared/tokens.css | 26 +++---- media/tempScreenshots/style.css | 26 +++---- media/templateAssetPanel/style.css | 16 ++--- media/templatePanel/style.css | 10 +-- 11 files changed, 139 insertions(+), 132 deletions(-) diff --git a/docs/design-system.en.md b/docs/design-system.en.md index 3daa363a..9226c824 100644 --- a/docs/design-system.en.md +++ b/docs/design-system.en.md @@ -55,7 +55,7 @@ Buttons of the same kind share height (`--control-h-sm/md/lg`), padding (`--spac ## 4. Shared Size Scale -- Spacing: `--space-xs(4) / sm(6) / md(10) / lg(14) / xl(20)`. +- Spacing: `--space-xs(4) / sm(8) / md(12) / lg(16) / xl(24)`. - Control height: `--control-h-sm(24) / md(30) / lg(34)`. - Radius: `--radius-sm(5) / md(8) / lg(11) / pill`. - Font size: `--font-xs(11) / sm(12) / md(13) / lg(15)`; weights only `--weight-regular(400)` / `--weight-medium(500)`. diff --git a/docs/design-system.md b/docs/design-system.md index 658228ba..23d275d8 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -59,7 +59,7 @@ Dialog·Modal / 状态提示 / Loading / 空状态 / 错误状态。同类组件 ## 4. 统一尺寸体系 -- 间距:`--space-xs(4) / sm(6) / md(10) / lg(14) / xl(20)` +- 间距:`--space-xs(4) / sm(8) / md(12) / lg(16) / xl(24)` - 控件高度:`--control-h-sm(24) / md(30) / lg(34)` - 圆角:`--radius-sm(5) / md(8) / lg(11) / pill` - 字号:`--font-xs(11) / sm(12) / md(13) / lg(15)`;字重仅 `--weight-regular(400)` / `--weight-medium(500)` diff --git a/media/annotationPanel/conflict.css b/media/annotationPanel/conflict.css index 490be09c..8f160448 100644 --- a/media/annotationPanel/conflict.css +++ b/media/annotationPanel/conflict.css @@ -10,9 +10,9 @@ flex-direction: column; gap: var(--space-sm); max-height: 52%; - padding: var(--space-sm); - border: var(--border-width) solid var(--border); - border-radius: var(--radius-sm); + padding: var(--space-md); + border: var(--border-width) solid color-mix(in srgb, var(--border) 50%, transparent); + border-radius: var(--radius-md); background: var(--bg-container-raised); overflow: hidden; } @@ -39,8 +39,8 @@ display: flex; flex-direction: column; gap: var(--space-xs); - padding: var(--space-xs); - border: var(--border-width) solid var(--border); + padding: var(--space-sm); + border: var(--border-width) solid transparent; border-radius: var(--radius-sm); background: var(--bg-row); } diff --git a/media/annotationPanel/style.css b/media/annotationPanel/style.css index b9f6db6e..452b0fcc 100644 --- a/media/annotationPanel/style.css +++ b/media/annotationPanel/style.css @@ -26,7 +26,7 @@ body { display: inline-flex; gap: 2px; padding: 2px; - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-sm); background: var(--bg-container); } @@ -77,8 +77,8 @@ body { display: flex; flex-direction: column; gap: var(--space-sm); - padding: var(--space-sm); - border-left: var(--border-width) solid var(--border); + padding: var(--space-md); + border-left: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); background: var(--bg-container); min-height: 0; } @@ -107,7 +107,7 @@ body { align-items: center; gap: var(--space-sm); min-height: var(--control-h-sm); - padding: 0 var(--space-xs); + padding: var(--space-xs) var(--space-sm); background: var(--bg-row); border-radius: var(--radius-sm); font-size: var(--font-sm); @@ -188,9 +188,9 @@ canvas { .modal { background: var(--bg-container-raised); - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-lg); - padding: var(--space-lg) 22px; + padding: var(--space-lg) var(--space-xl); min-width: 340px; box-shadow: var(--shadow-lg); } diff --git a/media/characterManager/characterManager.css b/media/characterManager/characterManager.css index 5323e71c..4ab3b7c8 100644 --- a/media/characterManager/characterManager.css +++ b/media/characterManager/characterManager.css @@ -27,31 +27,29 @@ button.icon { padding: 4px 7px; min-width: 28px; } button.utility-button { width: 30px; height: 30px; - border-color: var(--border); + border-color: color-mix(in srgb, var(--border) 55%, transparent); background: var(--bg-control); color: var(--text-primary); padding: 0; font-size: var(--font-lg); - box-shadow: var(--shadow-sm); } button.utility-button:hover { - border-color: color-mix(in srgb, var(--accent) 65%, var(--border)); + border-color: var(--border); background: var(--bg-control-hover); } button.action-button { display: inline-flex; align-items: center; justify-content: center; - gap: 5px; + gap: var(--space-xs); min-height: 30px; - border-color: var(--border); + border-color: color-mix(in srgb, var(--border) 55%, transparent); padding: 4px 10px; font-weight: var(--weight-medium); line-height: 1.2; white-space: nowrap; - box-shadow: var(--shadow-sm); } -button.action-button:hover { box-shadow: var(--shadow); } +button.action-button:hover { border-color: var(--border); } button.action-button:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; } button.action-button.compact { min-height: 26px; padding: 3px 8px; font-size: var(--font-xs); } button.action-add, button.action-save { @@ -61,14 +59,14 @@ button.action-add, button.action-save { } button.action-add:hover, button.action-save:hover { background: var(--btn-bg-hover); } button.action-edit { - border-color: color-mix(in srgb, var(--accent) 65%, var(--border)); + border-color: color-mix(in srgb, var(--border) 60%, transparent); background: var(--btn-secondary-bg); color: var(--btn-secondary-fg); } button.action-edit:hover { background: var(--btn-secondary-bg-hover); } button.action-open { - border-color: color-mix(in srgb, var(--info) 60%, var(--border)); - background: color-mix(in srgb, var(--info) 9%, var(--surface)); + border-color: color-mix(in srgb, var(--border) 55%, transparent); + background: color-mix(in srgb, var(--info) 8%, var(--surface)); color: var(--text-primary); } button.action-open:hover { background: color-mix(in srgb, var(--info) 18%, var(--hover)); } @@ -94,7 +92,7 @@ input::placeholder, textarea::placeholder { color: var(--input-placeholder); } border-bottom: 1px solid var(--border); background: var(--bg-container-raised); } -.title-row { min-height: 42px; display: flex; gap: 10px; align-items: center; padding: 7px 12px; } +.title-row { min-height: 42px; display: flex; gap: var(--space-md); align-items: center; padding: var(--space-sm) var(--space-md); } .title { font-size: var(--font-lg); font-weight: var(--weight-bold); white-space: nowrap; } #project { color: var(--muted); font-size: var(--font-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .spacer { flex: 1; } @@ -126,12 +124,12 @@ input::placeholder, textarea::placeholder { color: var(--input-placeholder); } flex: 0 0 auto; display: grid; grid-template-columns: repeat(7, minmax(92px, 1fr)); - gap: 6px; - padding: 8px 10px; - border-bottom: 1px solid var(--border); + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); background: var(--surface2); } -.metric { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); padding: 6px 8px; } +.metric { border: 1px solid color-mix(in srgb, var(--border) 40%, transparent); border-radius: var(--radius-sm); background: var(--bg-row); padding: var(--space-sm) var(--space-sm); } .metric-value { font-size: var(--font-xl); font-weight: var(--weight-bold); } .metric-label { color: var(--muted); font-size: var(--font-2xs); margin-top: 1px; } .metric.error .metric-value { color: var(--danger); } @@ -225,10 +223,10 @@ input::placeholder, textarea::placeholder { color: var(--input-placeholder); } .chip { display: inline-flex; align-items: center; - gap: 4px; - border: 1px solid var(--border); + gap: var(--space-xs); + border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-pill); - background: var(--surface); + background: var(--bg-row); padding: 2px 7px; font-size: var(--font-xs); } @@ -262,7 +260,7 @@ input::placeholder, textarea::placeholder { color: var(--input-placeholder); } .locale-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .section-title { display: flex; align-items: center; gap: 6px; font-size: var(--font-md); font-weight: var(--weight-bold); margin: 13px 0 7px; } .section-title > .action-button { margin-left: 3px; } -.skill-card { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); margin-bottom: 8px; overflow: hidden; } +.skill-card { border: 1px solid color-mix(in srgb, var(--border) 45%, transparent); border-radius: var(--radius-md); background: var(--surface); margin-bottom: var(--space-sm); overflow: hidden; } .skill-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); } .skill-name { font-size: var(--font-lg); font-weight: var(--weight-bold); } .skill-id { color: var(--muted); font-family: var(--font-mono); font-size: var(--font-2xs); } @@ -335,10 +333,10 @@ input::placeholder, textarea::placeholder { color: var(--input-placeholder); } } .enhancement { border-left: 3px solid var(--accent); - padding: 7px 9px; - margin-top: 8px; - background: color-mix(in srgb, var(--accent) 7%, transparent); - border-radius: 0 4px 4px 0; + padding: var(--space-sm) var(--space-md); + margin-top: var(--space-sm); + background: color-mix(in srgb, var(--accent) 6%, transparent); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; } .enhancement-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; min-height: 30px; } .enhancement-name { flex: 1 1 auto; min-width: 0; font-weight: var(--weight-bold); } @@ -415,7 +413,7 @@ th.sticky { z-index: 4; background: var(--bg-container); } max-height: 90vh; display: flex; flex-direction: column; - border: 1px solid var(--border); + border: 1px solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-lg); background: var(--bg-container); box-shadow: var(--shadow-lg); @@ -457,7 +455,7 @@ th.sticky { z-index: 4; background: var(--bg-container); } .effect-select { min-height: 210px; font-family: var(--font-mono); font-size: var(--font-xs); } .effect-editor-hint { color: var(--muted); font-size: var(--font-2xs); margin-top: 4px; } .effect-params { display: flex; flex-direction: column; gap: 6px; max-height: 310px; overflow: auto; } -.effect-param-row { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 7px; background: var(--surface); } +.effect-param-row { border: 1px solid color-mix(in srgb, var(--border) 45%, transparent); border-radius: var(--radius-sm); padding: var(--space-sm); background: var(--bg-row); } .effect-param-title { font-family: var(--font-mono); font-size: var(--font-xs); font-weight: var(--weight-bold); margin-bottom: 5px; } .effect-param-desc { color: var(--muted); font-size: var(--font-2xs); margin-bottom: 5px; } .effect-param-grid { display: grid; grid-template-columns: repeat(4, minmax(70px, 1fr)); gap: 5px; } diff --git a/media/console/console.css b/media/console/console.css index 225216ff..cdce18c1 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -5,7 +5,7 @@ .console { width: min(100%, 1080px); margin: 0 auto; - padding: 0 12px 28px; + padding: 0 var(--space-md) var(--space-xl); } /* ── 全局状态条:游戏连接 + 常驻执行器(两个分段共享) ─────────── */ @@ -124,7 +124,7 @@ } .seg__btn.is-active { - border-color: var(--border); + border-color: color-mix(in srgb, var(--border) 60%, transparent); background: var(--bg-control); color: var(--text-primary); } @@ -199,22 +199,21 @@ .group-head.is-collapsed .group-head__chev { transform: rotate(-90deg); } .group-head__count { color: color-mix(in srgb, var(--muted) 72%, transparent); font-weight: 500; } -.group-body { display: grid; gap: 8px; } +.group-body { display: grid; gap: var(--space-sm); } .group-body.is-collapsed { display: none; } /* ── 任务卡片(新布局:单行主区 + 参数徽标 + 状态徽标) ─────────── */ .task-card { overflow: hidden; - border: 1px solid var(--border); + border: 1px solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-lg); background: var(--surface); - box-shadow: 0 1px 0 color-mix(in srgb, var(--text-primary) 4%, transparent); - transition: border-color .14s ease, box-shadow .14s ease; + transition: border-color .14s ease, background-color .14s ease; } .task-card:hover { - border-color: color-mix(in srgb, var(--border-strong) 58%, var(--border)); - box-shadow: var(--shadow); + border-color: var(--border); + background: color-mix(in srgb, var(--surface) 92%, var(--surface-hover)); } .task-card.is-active { border-color: var(--border-strong); } @@ -223,9 +222,9 @@ .task-card__header { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 10px; + gap: var(--space-md); align-items: start; - padding: 12px 13px 10px; + padding: var(--space-md) var(--space-lg) var(--space-sm); } .task-card__identity { min-width: 0; } @@ -241,11 +240,11 @@ .task-card__kind { flex: 0 0 auto; - border: 1px solid var(--border); + border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-pill); padding: 1px 7px; color: var(--muted); - background: color-mix(in srgb, var(--surface-raised) 82%, transparent); + background: var(--bg-row); font-size: var(--font-2xs); font-weight: 500; } @@ -317,7 +316,7 @@ align-items: center; gap: 5px; min-height: var(--control-h-sm, 24px); - border: var(--border-width, 1px) solid var(--border); + border: var(--border-width, 1px) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-pill, 999px); padding: 2px 10px; color: var(--text-primary); @@ -327,7 +326,7 @@ } .task-card__config:hover:not(:disabled) { background: var(--bg-control-hover); - border-color: var(--border-strong); + border-color: var(--border); } .task-card__config.has-overrides { border-color: var(--border-strong); @@ -343,9 +342,9 @@ /* ── 游戏分段 ─────────────────────────────────────────────────── */ .connect-card { - margin-top: 12px; - padding: 18px 16px 16px; - border: 1px solid var(--border); + margin-top: var(--space-md); + padding: var(--space-xl) var(--space-lg) var(--space-lg); + border: 1px solid color-mix(in srgb, var(--border) 45%, transparent); border-radius: var(--radius-lg); background: var(--surface); text-align: center; @@ -403,10 +402,10 @@ .tool-row { display: flex; align-items: center; - gap: 10px; - margin-top: 10px; - padding: 11px 13px; - border: 1px solid var(--border); + gap: var(--space-md); + margin-top: var(--space-md); + padding: var(--space-md) var(--space-lg); + border: 1px solid color-mix(in srgb, var(--border) 45%, transparent); border-radius: var(--radius-lg); background: var(--surface); } @@ -655,7 +654,7 @@ .config-group { overflow: hidden; - border: 1px solid var(--border); + border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-md); background: color-mix(in srgb, var(--surface-raised) 72%, transparent); } @@ -711,10 +710,10 @@ } .status-banner { - margin-top: 8px; - border: 1px solid var(--border); + margin-top: var(--space-sm); + border: 1px solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-md); - padding: 8px 10px; + padding: var(--space-sm) var(--space-md); font-size: var(--font-xs); line-height: 1.45; background: var(--surface-raised); @@ -878,7 +877,7 @@ } .gconfig-list { display: flex; flex-direction: column; gap: 10px; } .gconfig-card { - border: 1px solid var(--border); + border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-md); overflow: hidden; background: var(--surface); @@ -969,9 +968,9 @@ display: flex; flex-direction: column; gap: 2px; - border: 1px solid var(--border); + border: 1px solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-sm); - padding: 4px; + padding: var(--space-xs); background: color-mix(in srgb, var(--surface) 70%, transparent); } .ms-list__item { @@ -1003,7 +1002,7 @@ flex: 0 0 auto; max-height: 100vh; padding: 6px 6px 6px 0; - border-right: 1px solid var(--border); + border-right: 1px solid color-mix(in srgb, var(--border) 55%, transparent); background: var(--bg-page); } .lay-side__item { @@ -1040,10 +1039,10 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 8px 18px; - margin: 0 0 10px; - padding: 9px 12px; - border: 1px solid var(--border); + gap: var(--space-sm) var(--space-lg); + margin: 0 0 var(--space-md); + padding: var(--space-sm) var(--space-md); + border: 1px solid color-mix(in srgb, var(--border) 45%, transparent); border-radius: var(--radius-md); background: var(--surface); } @@ -1061,10 +1060,10 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 8px 12px; - margin: 0 0 10px; - padding: 8px 12px; - border: 1px dashed var(--border-strong); + gap: var(--space-sm) var(--space-md); + margin: 0 0 var(--space-md); + padding: var(--space-sm) var(--space-md); + border: 1px dashed color-mix(in srgb, var(--border-strong) 55%, var(--border)); border-radius: var(--radius-md); background: var(--surface); } @@ -1253,12 +1252,12 @@ .gconfig-card__head--toggle:hover { background: var(--bg-row-hover); } .gconfig-card__head--toggle:focus-visible { outline: 1px solid var(--border-strong); outline-offset: -1px; } -/* 配置分组折叠按键:只有 ▲/▼ 符号,描边 + 静态底色使其成为可见控件 */ +/* 配置分组折叠按键:只有 ▲/▼ 符号,轻描边 + 静态底色使其成为可见控件 */ .config-group__toggle { - border: var(--border-width, 1px) solid var(--border); + border: var(--border-width, 1px) solid color-mix(in srgb, var(--border) 55%, transparent); background: var(--bg-control); } -.config-group__toggle:hover:not(:disabled) { border-color: var(--border-strong); } +.config-group__toggle:hover:not(:disabled) { border-color: var(--border); } /* kind 级任务组头(触发任务/一次性):行级可点击区(同上) */ .group-head { diff --git a/media/shared/controls.css b/media/shared/controls.css index 59a242c4..efd0afd6 100644 --- a/media/shared/controls.css +++ b/media/shared/controls.css @@ -26,21 +26,29 @@ button:disabled { cursor: not-allowed; opacity: .45; } button.secondary { color: var(--btn-secondary-fg); background: var(--btn-secondary-bg); + border-color: color-mix(in srgb, var(--border) 55%, transparent); +} +button.secondary:hover:not(:disabled) { + background: var(--btn-secondary-bg-hover); + border-color: var(--border); } -button.secondary:hover:not(:disabled) { background: var(--btn-secondary-bg-hover); } /* ── Button(迷你 / 工具条内)───────────────────────────────────────── */ .mini-btn { min-height: var(--control-h-sm); padding: 2px 10px; border-radius: var(--radius-sm); + border-color: color-mix(in srgb, var(--border) 40%, transparent); font-size: var(--font-xs); font-weight: var(--weight-medium); white-space: nowrap; color: var(--btn-secondary-fg); background: var(--btn-secondary-bg); } -.mini-btn:hover:not(:disabled) { background: var(--btn-secondary-bg-hover); } +.mini-btn:hover:not(:disabled) { + background: var(--btn-secondary-bg-hover); + border-color: var(--border); +} .mini-btn--primary { color: var(--btn-fg); background: var(--btn-bg); @@ -61,14 +69,14 @@ button.secondary:hover:not(:disabled) { background: var(--btn-secondary-bg-hover /* ── Button(描边幽灵,静态底色 + 描边)────────────────────────────── */ .btn-mini { - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-sm); padding: 3px 8px; font-size: var(--font-xs); color: var(--text-muted); background: var(--bg-control); } -.btn-mini:hover { background: var(--bg-control-hover); color: var(--text-primary); border-color: var(--border-strong); } +.btn-mini:hover { background: var(--bg-control-hover); color: var(--text-primary); border-color: var(--border); } /* ── IconButton ─────────────────────────────────────────────────────── */ .icon-btn { @@ -82,10 +90,10 @@ button.secondary:hover:not(:disabled) { background: var(--btn-secondary-bg-hover border: var(--border-width) solid transparent; border-radius: var(--radius-sm); color: var(--text-muted); - background: var(--bg-control); + background: var(--bg-row); cursor: pointer; } -.icon-btn:hover { background: var(--bg-control-hover); color: var(--text-primary); } +.icon-btn:hover { background: var(--bg-row-hover); color: var(--text-primary); } /* ── 表单控件 ───────────────────────────────────────────────────────── */ input, select, textarea { font: inherit; } @@ -120,12 +128,12 @@ input[type='radio'] { accent-color: var(--border-strong); } padding: 1px 8px; font-size: var(--font-xs); color: var(--text-muted); - background: var(--bg-control); + background: var(--bg-row); } /* ── Card ───────────────────────────────────────────────────────────── */ .card { - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-lg); background: var(--bg-container); } @@ -139,7 +147,7 @@ input[type='radio'] { accent-color: var(--border-strong); } flex-wrap: wrap; gap: var(--space-sm); align-items: center; - padding: var(--space-sm) 0; + padding: var(--space-sm) 0 var(--space-md); background: var(--bg-page); } @@ -147,8 +155,8 @@ input[type='radio'] { accent-color: var(--border-strong); } .panel-section-title { display: flex; align-items: center; - gap: 7px; - margin: var(--space-md) 1px var(--space-sm); + gap: var(--space-sm); + margin: var(--space-lg) 1px var(--space-sm); color: var(--text-muted); font-size: var(--font-xs); font-weight: var(--weight-medium); @@ -198,13 +206,15 @@ input[type='radio'] { accent-color: var(--border-strong); } [role='button'] { cursor: pointer; } [role='button']:focus-visible { outline: 1px solid var(--border-strong); outline-offset: 1px; } -/* 真按钮:控件面 + 描边 */ +/* 真按钮:控件面(危险/迷你/分组折叠);图标按钮用更轻的行级面,避免工具条变灰块墙 */ .mini-btn--danger, .btn-mini, -.icon-btn, .config-group__toggle { background: var(--bg-control); } +.icon-btn { + background: var(--bg-row); +} .thumbnail-actions { position: absolute; @@ -219,7 +229,7 @@ input[type='radio'] { accent-color: var(--border-strong); } height: 24px; min-height: 24px; padding: 0; - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-sm); background: var(--bg-control); color: var(--text-primary); @@ -228,7 +238,7 @@ input[type='radio'] { accent-color: var(--border-strong); } } .thumbnail-actions button:hover, .thumbnail-actions button:focus-visible { background: var(--bg-control-hover); - border-color: var(--border-strong); + border-color: var(--border); } /* 行级可点击区(组头 / 折叠标题行 / tab):极浅色调,不参与按钮权重 */ diff --git a/media/shared/tokens.css b/media/shared/tokens.css index e76dd968..9a5e8c98 100644 --- a/media/shared/tokens.css +++ b/media/shared/tokens.css @@ -23,14 +23,14 @@ --surface-raised: var(--bg-container-raised); /* ── 2. 控件面与交互态 ───────────────────────────────────────────── */ - --bg-control: color-mix(in srgb, var(--vscode-button-secondaryBackground, var(--bg-container-raised)) 80%, var(--vscode-foreground) 14%); - --bg-control-hover: color-mix(in srgb, var(--bg-control) 82%, var(--vscode-foreground) 18%); - --bg-control-active: color-mix(in srgb, var(--bg-control) 72%, var(--vscode-foreground) 26%); + --bg-control: color-mix(in srgb, var(--vscode-button-secondaryBackground, var(--bg-container-raised)) 72%, var(--vscode-foreground) 10%); + --bg-control-hover: color-mix(in srgb, var(--bg-control) 78%, var(--vscode-foreground) 16%); + --bg-control-active: color-mix(in srgb, var(--bg-control) 68%, var(--vscode-foreground) 22%); --surface-hover: var(--vscode-list-hoverBackground, color-mix(in srgb, var(--bg-container) 86%, var(--vscode-focusBorder) 14%)); /* 行级可点击区(组头 / 折叠标题行):极浅色调——默认可识别,但不形成方块墙 */ - --bg-row: color-mix(in srgb, var(--bg-control) 42%, transparent); - --bg-row-hover: color-mix(in srgb, var(--bg-control) 70%, transparent); + --bg-row: color-mix(in srgb, var(--bg-control) 26%, transparent); + --bg-row-hover: color-mix(in srgb, var(--bg-control) 48%, transparent); /* ── 6. VS Code 主题映射(本文件是全项目唯一允许直引 --vscode-* 的地方)── 面板 CSS 一律用下面这些语义 token,便于换主题/换宿主时一处调整。 */ @@ -40,9 +40,9 @@ --btn-bg: var(--vscode-button-background, #0e639c); --btn-fg: var(--vscode-button-foreground, #ffffff); --btn-bg-hover: var(--vscode-button-hoverBackground, #1177bb); - --btn-secondary-bg: var(--vscode-button-secondaryBackground, color-mix(in srgb, var(--text-primary) 12%, transparent)); + --btn-secondary-bg: var(--vscode-button-secondaryBackground, color-mix(in srgb, var(--text-primary) 8%, transparent)); --btn-secondary-fg: var(--vscode-button-secondaryForeground, var(--text-primary)); - --btn-secondary-bg-hover: var(--vscode-button-secondaryHoverBackground, color-mix(in srgb, var(--text-primary) 18%, transparent)); + --btn-secondary-bg-hover: var(--vscode-button-secondaryHoverBackground, color-mix(in srgb, var(--text-primary) 14%, transparent)); --input-bg: var(--vscode-input-background, var(--bg-container)); --input-fg: var(--vscode-input-foreground, var(--text-primary)); --input-border: var(--vscode-input-border, var(--border)); @@ -68,7 +68,7 @@ --clickable-bg-hover: var(--bg-control-hover); /* ── 3. 边框 ────────────────────────────────────────────────────── */ - --border: var(--vscode-panel-border, color-mix(in srgb, var(--vscode-foreground) 18%, transparent)); + --border: var(--vscode-panel-border, color-mix(in srgb, var(--vscode-foreground) 11%, transparent)); --border-strong: var(--vscode-focusBorder, #007acc); --border-width: 1px; @@ -90,17 +90,17 @@ /* ── 6. 间距(spacing scale)─────────────────────────────────────── */ --space-xs: 4px; - --space-sm: 6px; - --space-md: 10px; - --space-lg: 14px; - --space-xl: 20px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + --space-xl: 24px; /* ── 7. 圆角与阴影 ──────────────────────────────────────────────── */ --radius-sm: 5px; --radius-md: 8px; --radius-lg: 11px; --radius-pill: 999px; - --shadow: 0 4px 14px color-mix(in srgb, #000 15%, transparent); + --shadow: 0 4px 16px color-mix(in srgb, #000 12%, transparent); /* ── 8. 控件尺寸(高度)──────────────────────────────────────────── */ --control-h-sm: 24px; diff --git a/media/tempScreenshots/style.css b/media/tempScreenshots/style.css index 9d2e40f7..ae087df3 100644 --- a/media/tempScreenshots/style.css +++ b/media/tempScreenshots/style.css @@ -42,9 +42,9 @@ body { position: relative; flex: 1 1 45%; min-height: 110px; - margin: 0 6px; + margin: 0 var(--space-sm); overflow: hidden; - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 50%, transparent); border-radius: var(--radius-md); background: repeating-conic-gradient( color-mix(in srgb, var(--text-muted) 12%, transparent) 0% 25%, transparent 0% 50% @@ -142,10 +142,10 @@ body { flex: 1 1 55%; overflow-y: auto; overflow-x: hidden; - padding: 0 6px var(--space-sm); + padding: 0 var(--space-sm) var(--space-md); display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); - gap: var(--space-sm); + gap: var(--space-md); align-content: start; } .card { @@ -200,15 +200,15 @@ body { /* ── 提示条 ─────────────────────────────────────────────────────────── */ .toast { position: fixed; - left: 6px; - right: 6px; - bottom: 6px; - padding: 5px var(--space-sm); + left: var(--space-sm); + right: var(--space-sm); + bottom: var(--space-sm); + padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-md); font-size: var(--font-xs); background: var(--bg-container-raised); color: var(--text-primary); - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); box-shadow: var(--shadow); opacity: 0; transform: translateY(6px); @@ -222,9 +222,9 @@ body { /* ── 内联确认条(webview 中不使用 window.confirm)───────────────────── */ .confirm { position: fixed; - left: 6px; - right: 6px; - bottom: 6px; + left: var(--space-sm); + right: var(--space-sm); + bottom: var(--space-sm); display: none; flex-direction: column; gap: var(--space-sm); @@ -234,7 +234,7 @@ body { line-height: 1.5; background: var(--bg-container-raised); color: var(--text-primary); - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); box-shadow: var(--shadow); z-index: 20; } diff --git a/media/templateAssetPanel/style.css b/media/templateAssetPanel/style.css index ae834947..05a2e9db 100644 --- a/media/templateAssetPanel/style.css +++ b/media/templateAssetPanel/style.css @@ -5,7 +5,7 @@ body { margin: 0; - padding: var(--space-md) 12px var(--space-xl); + padding: var(--space-md) var(--space-md) var(--space-xl); color: var(--text-primary); background: var(--bg-page); font-family: var(--font-ui); @@ -31,7 +31,7 @@ body { #grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); - gap: var(--space-sm); + gap: var(--space-md); margin-top: var(--space-sm); } @@ -57,7 +57,7 @@ body { .thumb-box img { max-width: 100%; max-height: 100%; image-rendering: pixelated; } .placeholder { color: var(--text-muted); font-size: var(--font-xs); } -.meta { padding: 5px 7px 6px; } +.meta { padding: var(--space-xs) var(--space-sm) var(--space-sm); } .name { font-weight: var(--weight-medium); white-space: nowrap; @@ -117,9 +117,9 @@ body.drop-active .drop-hint { display: flex; } display: flex; flex-direction: column; background: var(--bg-container-raised); - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-lg); - padding: var(--space-lg) 22px; + padding: var(--space-lg) var(--space-xl); width: min(560px, 92vw); max-height: 82vh; box-shadow: var(--shadow-lg); @@ -137,15 +137,15 @@ body.drop-active .drop-hint { display: flex; } } .swap-item { padding: var(--space-xs); - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 45%, transparent); border-radius: var(--radius-sm); - background: var(--bg-control); + background: var(--bg-row); color: var(--text-primary); font-family: inherit; cursor: pointer; text-align: center; } -.swap-item:hover { background: var(--bg-control-hover); border-color: var(--border-strong); } +.swap-item:hover { background: var(--bg-row-hover); border-color: var(--border); } .swap-thumb { height: 56px; display: flex; diff --git a/media/templatePanel/style.css b/media/templatePanel/style.css index 4a31aa6a..eca31dd4 100644 --- a/media/templatePanel/style.css +++ b/media/templatePanel/style.css @@ -4,7 +4,7 @@ body { margin: 0; - padding: var(--space-md) 12px var(--space-xl); + padding: var(--space-md) var(--space-md) var(--space-xl); color: var(--text-primary); background: var(--bg-page); font-family: var(--font-ui); @@ -15,7 +15,7 @@ body { display: inline-flex; gap: 2px; padding: 2px; - border: var(--border-width) solid var(--border); + border: var(--border-width) solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-sm); background: var(--bg-container); } @@ -27,8 +27,8 @@ body { #grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); - gap: var(--space-sm); - margin-top: var(--space-xs); + gap: var(--space-md); + margin-top: var(--space-sm); } .card { @@ -52,7 +52,7 @@ body { .thumb-box img { max-width: 100%; max-height: 100%; image-rendering: pixelated; } .placeholder { color: var(--text-muted); font-size: var(--font-xs); } -.meta { padding: 5px 7px 6px; } +.meta { padding: var(--space-xs) var(--space-sm) var(--space-sm); } .name { font-weight: var(--weight-medium); white-space: nowrap; From 4830cbc7b745976419935aa817f51ba466a28d8a Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:13:32 +0800 Subject: [PATCH 02/29] chore: remove dead box-panel legacy The standalone box panel/gallery (media/boxPanel) was folded into the unified annotation editor long ago, but leftovers stayed behind: openBoxEditor() had no caller, view.boxAssets/boxGallery nls keys and the boxAssetsTitle webview key had no consumer, and the design-system audit still registered a media/boxPanel host that no longer exists. Keep the position-preview helpers used by hover/completion and move them to src/positionPreview.ts so the file name says what it does. --- docs/design-system.en.md | 4 +-- docs/design-system.md | 4 +-- l10n/webview.en.json | 1 - l10n/webview.es.json | 1 - l10n/webview.ja.json | 1 - l10n/webview.ko.json | 1 - l10n/webview.zh-cn.json | 1 - l10n/webview.zh-tw.json | 1 - package.nls.es.json | 4 --- package.nls.ja.json | 4 --- package.nls.json | 4 --- package.nls.ko.json | 4 --- package.nls.zh-cn.json | 4 --- package.nls.zh-tw.json | 4 --- scripts/test_design_system.js | 2 -- src/{boxPanels.ts => positionPreview.ts} | 33 +----------------------- src/providers.ts | 2 +- 17 files changed, 6 insertions(+), 69 deletions(-) rename src/{boxPanels.ts => positionPreview.ts} (71%) diff --git a/docs/design-system.en.md b/docs/design-system.en.md index 9226c824..f9039f44 100644 --- a/docs/design-system.en.md +++ b/docs/design-system.en.md @@ -2,7 +2,7 @@ [简体中文](design-system.md) | [English](design-system.en.md) -> This is the authoritative UI specification for the entire project. All webview panels (console / templatePanel / templateAssetPanel / tempScreenshots / annotationPanel / characterManager / boxPanel) must follow it. +> This is the authoritative UI specification for the entire project. All webview panels (console / templatePanel / templateAssetPanel / tempScreenshots / annotationPanel / characterManager) must follow it. > Implementation sources: `media/shared/tokens.css` (design tokens) and `media/shared/controls.css` (shared controls). > **Pages compose these classes; they must not independently redefine visual rules.** @@ -122,7 +122,7 @@ Connect a new panel in **three steps; omitting any step silently breaks styling* ## Implementation Progress -2026-10-01 addendum: box panels/galleries share `media/boxPanel`, connected through `src/boxPanels.ts` and included in the static audit. The table and six-panel record below describe the 2026-09-23 migration baseline, not a reason to omit newer panels. +2026-10-09 addendum: the standalone box panel/gallery (`media/boxPanel`, `src/boxPanels.ts`) was folded into the unified annotation editor and resource preview and has been removed; the position-preview helpers used by hover and completion now live in `src/positionPreview.ts`. The table and six-panel record below describe the 2026-09-23 migration baseline; new panels must still be registered in the static audit. | Stage | Work | Status | |---|---|---| diff --git a/docs/design-system.md b/docs/design-system.md index 23d275d8..1ac01c67 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -3,7 +3,7 @@ [简体中文](design-system.md) | [English](design-system.en.md) > 本文件是全项目 UI 的唯一权威规范。所有 webview 面板(console / templatePanel / -> templateAssetPanel / tempScreenshots / annotationPanel / characterManager / boxPanel)必须遵循。 +> templateAssetPanel / tempScreenshots / annotationPanel / characterManager)必须遵循。 > 实现真源:`media/shared/tokens.css`(Design Tokens)+ `media/shared/controls.css`(共享控件), > **页面只负责组合这些类,不得自行重定义视觉规则。** @@ -138,7 +138,7 @@ media/shared/controls.css Button(主/次/迷你/幽灵/图标) · Input · Se ## 落地进度 -2026-10-01 补充:框面板与框画廊共用 `media/boxPanel`,由 `src/boxPanels.ts` 接入共享层,静态审计已包含此目录。下表与“六个面板”记录的是 2026-09-23 的迁移基线,不能据此漏掉新增面板。 +2026-10-09 补充:独立的框面板 / 框画廊(`media/boxPanel`、`src/boxPanels.ts`)已并入统一标注编辑器与资源预览并被移除;悬停与补全使用的位置预览逻辑移到 `src/positionPreview.ts`。下表与“六个面板”记录的是 2026-09-23 的迁移基线,新增面板仍须登记进静态审计。 | 阶段 | 内容 | 状态 | |---|---|---| diff --git a/l10n/webview.en.json b/l10n/webview.en.json index 3398866f..cfac6a54 100644 --- a/l10n/webview.en.json +++ b/l10n/webview.en.json @@ -257,7 +257,6 @@ "templateSize": "Size: {width}×{height}", "templateSource": "Source: {path}", "templateAssetsTitle": "Template Assets", - "boxAssetsTitle": "Box Assets", "templateAssetsHint": "Click an image or its edit button to open annotations", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", diff --git a/l10n/webview.es.json b/l10n/webview.es.json index 74ebe990..47efcf0f 100644 --- a/l10n/webview.es.json +++ b/l10n/webview.es.json @@ -257,7 +257,6 @@ "templateSize": "Tamaño: {width}×{height}", "templateSource": "Origen: {path}", "templateAssetsTitle": "Template Assets", - "boxAssetsTitle": "Box Assets", "templateAssetsHint": "Pulse la imagen o el botón de editar para abrir anotaciones", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", diff --git a/l10n/webview.ja.json b/l10n/webview.ja.json index 05c6ee8f..d6f6c5fe 100644 --- a/l10n/webview.ja.json +++ b/l10n/webview.ja.json @@ -257,7 +257,6 @@ "templateSize": "サイズ: {width}×{height}", "templateSource": "ソース: {path}", "templateAssetsTitle": "Template Assets", - "boxAssetsTitle": "Box Assets", "templateAssetsHint": "画像または編集ボタンをクリックしてアノテーションを開く", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", diff --git a/l10n/webview.ko.json b/l10n/webview.ko.json index da49596a..1df49470 100644 --- a/l10n/webview.ko.json +++ b/l10n/webview.ko.json @@ -257,7 +257,6 @@ "templateSize": "크기: {width}×{height}", "templateSource": "소스: {path}", "templateAssetsTitle": "Template Assets", - "boxAssetsTitle": "Box Assets", "templateAssetsHint": "이미지 또는 편집 버튼을 클릭하여 주석 편집기 열기", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", diff --git a/l10n/webview.zh-cn.json b/l10n/webview.zh-cn.json index 98019830..6195f224 100644 --- a/l10n/webview.zh-cn.json +++ b/l10n/webview.zh-cn.json @@ -257,7 +257,6 @@ "templateSize": "尺寸: {width}×{height}", "templateSource": "来源: {path}", "templateAssetsTitle": "模板素材管理", - "boxAssetsTitle": "框资源管理", "templateAssetsHint": "单击或点击编辑按钮打开标注编辑器", "noTemplateAssets": "暂无模板素材。\n点击 Import 导入图片。", "noTemplateAssetsHint": "暂无模板素材。", diff --git a/l10n/webview.zh-tw.json b/l10n/webview.zh-tw.json index 2c273ce5..13403b84 100644 --- a/l10n/webview.zh-tw.json +++ b/l10n/webview.zh-tw.json @@ -257,7 +257,6 @@ "templateSize": "尺寸:{width}×{height}", "templateSource": "來源:{path}", "templateAssetsTitle": "模板素材管理", - "boxAssetsTitle": "框資源管理", "templateAssetsHint": "單擊或點擊編輯按鈕開啟標註編輯器", "noTemplateAssets": "暂无模板素材。\n点击 Import 导入图片。", "noTemplateAssetsHint": "暂无模板素材。", diff --git a/package.nls.es.json b/package.nls.es.json index 794b530e..89f0dcde 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -38,10 +38,6 @@ "view.console.title": "Herramientas de ok-script: Consola", "view.templateGallery.title": "Plantillas de ok-script: Galería de plantillas", "view.templateAssets.title": "Plantillas de ok-script: Recursos de plantillas", - "view.boxAssets": "Recursos de cuadros", - "view.boxAssets.title": "Plantillas de ok-script: Recursos de cuadros", - "view.boxGallery": "Cuadros", - "view.boxGallery.title": "Plantillas de ok-script: Cuadros", "view.tempScreenshots": "Capturas temporales", "view.tempScreenshots.title": "Capturas temporales de ok-script", "configuration.title": "Sugerencias de idioma de ok-script", diff --git a/package.nls.ja.json b/package.nls.ja.json index 87b0da7b..62ddaecf 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -38,10 +38,6 @@ "view.console.title": "ok-script ツール: コンソール", "view.templateGallery.title": "ok-script テンプレート: テンプレートギャラリー", "view.templateAssets.title": "ok-script テンプレート: テンプレートアセット", - "view.boxAssets": "ボックス素材", - "view.boxAssets.title": "ok-script テンプレート: ボックス素材", - "view.boxGallery": "ボックス", - "view.boxGallery.title": "ok-script テンプレート: ボックス", "view.tempScreenshots": "一時スクリーンショット", "view.tempScreenshots.title": "ok-script 一時スクリーンショット", "configuration.title": "ok-script 言語ヒント", diff --git a/package.nls.json b/package.nls.json index 98ef6e55..6ce7a099 100644 --- a/package.nls.json +++ b/package.nls.json @@ -38,10 +38,6 @@ "view.console.title": "ok-script Tools: Console", "view.templateAssets": "Annotation Management", "view.templateAssets.title": "ok-script Resources: Annotation Management", - "view.boxAssets": "Box Assets", - "view.boxAssets.title": "ok-script Resources: Box Assets", - "view.boxGallery": "Boxes", - "view.boxGallery.title": "ok-script Resources: Boxes", "view.tempScreenshots": "Temp Screenshots", "view.tempScreenshots.title": "ok-script Temp Shots", "configuration.title": "ok-script Lang Hints", diff --git a/package.nls.ko.json b/package.nls.ko.json index 718452a3..f6b07240 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -38,10 +38,6 @@ "view.console.title": "ok-script 도구: 콘솔", "view.templateGallery.title": "ok-script 템플릿: 템플릿 갤러리", "view.templateAssets.title": "ok-script 템플릿: 템플릿 에셋", - "view.boxAssets": "박스 에셋", - "view.boxAssets.title": "ok-script 템플릿: 박스 에셋", - "view.boxGallery": "박스", - "view.boxGallery.title": "ok-script 템플릿: 박스", "view.tempScreenshots": "임시 스크린샷", "view.tempScreenshots.title": "ok-script 임시 스크린샷", "configuration.title": "ok-script 언어 힌트", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 6ef14596..bd98e741 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -38,10 +38,6 @@ "view.console.title": "ok-script 工具: 控制台", "view.templateGallery.title": "ok-script 资源: 资源预览", "view.templateAssets.title": "ok-script 资源: 标注管理", - "view.boxAssets": "框资源", - "view.boxAssets.title": "ok-script 资源: 框资源", - "view.boxGallery": "框", - "view.boxGallery.title": "ok-script 资源: 框", "view.tempScreenshots": "临时截图", "view.tempScreenshots.title": "ok-script 临时截图", "configuration.title": "ok-script 语言提示", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 1cdc0cb8..9bac37c3 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -38,10 +38,6 @@ "view.console.title": "ok-script 工具: 控制台", "view.templateGallery.title": "ok-script 模板: 模板面板", "view.templateAssets.title": "ok-script 模板: 模板素材", - "view.boxAssets": "框資源", - "view.boxAssets.title": "ok-script 模板: 框資源", - "view.boxGallery": "框", - "view.boxGallery.title": "ok-script 模板: 框", "view.tempScreenshots": "臨時截圖", "view.tempScreenshots.title": "ok-script 臨時截圖", "configuration.title": "ok-script 語言提示", diff --git a/scripts/test_design_system.js b/scripts/test_design_system.js index 52f91950..4a156041 100644 --- a/scripts/test_design_system.js +++ b/scripts/test_design_system.js @@ -30,8 +30,6 @@ const PANEL_HOST = { tempScreenshots: 'src/tempScreenshotPanel.ts', annotationPanel: 'src/annotationPanel.ts', characterManager: 'src/characterPanel.ts', - // 框面板与框资源画廊共用 media/boxPanel,宿主都在 src/boxPanels.ts - boxPanel: 'src/boxPanels.ts', }; const read = (p) => fs.readFileSync(p, 'utf8'); diff --git a/src/boxPanels.ts b/src/positionPreview.ts similarity index 71% rename from src/boxPanels.ts rename to src/positionPreview.ts index 06be009e..27ad63a8 100644 --- a/src/boxPanels.ts +++ b/src/positionPreview.ts @@ -1,42 +1,11 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { AnnotationPanel } from './annotationPanel'; import { readAuthoringFile } from './boxResourceStore'; -import { AUTHORING_FILE_NAME, PixelBox } from './boxResourcePure'; +import { PixelBox } from './boxResourcePure'; import { templatesDirectory } from './projectConfig'; import { readImageSize } from './pngCrop'; -import { TemplateAssetData } from './templateAssetData'; import { readPoints } from './pointResourceStore'; -/** Cross-image rect path occupancy used by the unified annotation editor. */ -export function boxPathOccupancy(authoring: ReturnType): Record { - const occupied: Record = {}; - for (const box of authoring.boxes) occupied[box.path] = box.image; - return occupied; -} - -/** Same editor/controller as template annotations; only the source file and name rule differ. */ -export function openBoxEditor( - extensionUri: vscode.Uri, - data: TemplateAssetData, - imagePath: string, - thumbDir = '', -): void { - const boxes = data.fileName === AUTHORING_FILE_NAME - ? data - : new TemplateAssetData(data.root, AUTHORING_FILE_NAME); - AnnotationPanel.show( - extensionUri, - boxes, - thumbDir || boxes.templatesDir, - imagePath, - boxes.listImages(), - () => {}, - true, - ); -} - /** * Position preview source comes directly from current authoring data. There is no separately * indexed legacy box runtime file anymore. Rects use their exact bbox; points use a tiny marker diff --git a/src/providers.ts b/src/providers.ts index a810730a..71806261 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -13,7 +13,7 @@ import { DEFAULT_FEATURE_ALIASES, ideSetting, labelEnumAliases, loadProjectConfi import { annotatedImageToDataUrlCached, cropTemplateToDataUrlCached } from './pngCrop'; import { selectedProjectLocale, tr } from './localization'; import { getProjectConfig } from './screenshotCapture'; -import { posPaths, previewRectForPath } from './boxPanels'; +import { posPaths, previewRectForPath } from './positionPreview'; /** 匹配 self.lang.<模块>.(支持 Unicode 标识符,如中文 OCR 文本;负向后视避免匹配 self.langx 之类) */ const EXPR_RE = /(? Date: Fri, 9 Oct 2026 13:14:32 +0800 Subject: [PATCH 03/29] feat(l10n): unify view names and split settings into categories - Console / Resource Preview / Annotation Management / Annotation Editor used consistently across package.json, host bundles and webviews (6 langs) - contributes.configuration split into 5 titled categories - tests flatten the configuration array --- l10n/bundle.l10n.es.json | 11 +- l10n/bundle.l10n.ja.json | 11 +- l10n/bundle.l10n.json | 11 +- l10n/bundle.l10n.ko.json | 11 +- l10n/bundle.l10n.zh-cn.json | 11 +- l10n/bundle.l10n.zh-tw.json | 11 +- l10n/webview.en.json | 12 +- l10n/webview.es.json | 12 +- l10n/webview.ja.json | 10 +- l10n/webview.ko.json | 10 +- l10n/webview.zh-cn.json | 6 +- l10n/webview.zh-tw.json | 6 +- package.json | 354 ++++++++++++++------------ package.nls.es.json | 34 +-- package.nls.ja.json | 34 +-- package.nls.json | 16 +- package.nls.ko.json | 38 +-- package.nls.zh-cn.json | 16 +- package.nls.zh-tw.json | 36 +-- scripts/test_annotation_mode_cycle.js | 2 +- scripts/test_convention_sources.js | 2 +- src/consolePanel.ts | 2 +- src/tempScreenshotPanel.ts | 4 +- src/templateAssetPanel.ts | 6 +- 24 files changed, 358 insertions(+), 308 deletions(-) diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index c138962c..5c7fdb93 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -71,7 +71,6 @@ "No Python editor is available; copied instead: {text}": "No hay un editor de Python disponible; se copió en su lugar: {text}", "ok-script-toolkit: Generating source image annotation…": "ok-script-toolkit: Generando anotación de la imagen original…", "Failed to generate annotated image: source image could not be decoded or was missing": "No se pudo generar la imagen anotada: no se pudo decodificar la imagen original o falta el archivo", - "Template Gallery": "Galería de plantillas", "Category": "Categoría", "Description": "Descripción", "Current": "Actual", @@ -140,14 +139,12 @@ "Connecting to game window…": "Conectando a la ventana del juego (inicia el juego automáticamente si no está en ejecución)…", "Failed to connect game window: {error}": "Error al conectar con la ventana del juego: {error}", "Failed to disconnect game window: {error}": "Error al desconectar la ventana del juego: {error}", - "Reusing game connection from toolbox: {title} (PID {pid})": "Reutilizando la conexión del juego del kit de herramientas: {title} (PID {pid})", "▶ Enabling debug overlay…": "▶ Activando la superposición de depuración…", "⏹ Disabling debug overlay…": "⏹ Desactivando la superposición de depuración…", "⏹ Debug overlay: disabled": "⏹ Superposición de depuración: desactivada", "ok-script Overlay Host": "Anfitrión de superposición de ok-script", "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.": "Superposición residente: pulse Alt+clic derecho en dos esquinas del juego para copiar las coordenadas.", "Overlay host stopped.": "Anfitrión de superposición detenido.", - "Added to template assets: {name}": "Añadido a recursos de plantillas: {name}", "Annotation Editor": "Editor de anotaciones", "Auto-detected window config: {config}": "Configuración de ventana detectada: {config}", "Built-in default": "Valor predeterminado", @@ -157,7 +154,6 @@ "Deleted: {name}": "Eliminado: {name}", "Effective value and where it comes from. Use the button to drop your override.": "Valor efectivo y su origen. Usa el botón para quitar tu anulación.", "Enter game window title pattern (regex), or leave empty for all windows": "Introduce el patrón de título de la ventana (regex), o déjalo vacío para todas las ventanas", - "Failed to add to template assets: {error}": "No se pudo añadir a recursos de plantillas: {error}", "Failed to delete: {name}": "No se pudo eliminar: {name}", "Deleted the record for {name}, but the temporary file is still at {path}.": "Se eliminó el registro de {name}, pero el archivo temporal sigue en {path}.", "Failed to save screenshot.": "No se pudo guardar la captura.", @@ -181,7 +177,6 @@ "Screenshot saved: {name}": "Captura guardada: {name}", "Screenshot script not found. capture_game_window.py is missing from the extension.": "No se encontró el script de captura. Falta capture_game_window.py en la extensión.", "Source: {layer}": "Origen: {layer}", - "Template Assets": "Recursos de plantillas", "The dragged screenshot is no longer available.": "La captura arrastrada ya no está disponible.", "enhancementFallbackName": "Mejora {index}", "importImagesFilter": "Imágenes", @@ -260,5 +255,9 @@ "Could not publish positions: {error}": "No se pudieron publicar las posiciones: {error}", "Published positions: {files}": "Posiciones publicadas: {files}", "Resource Preview": "Vista previa de recursos", - "No annotations available to publish.": "No hay anotaciones disponibles para publicar." + "No annotations available to publish.": "No hay anotaciones disponibles para publicar.", + "Annotation Management": "Gestión de anotaciones", + "Added to Annotation Management: {name}": "Añadido a Gestión de anotaciones: {name}", + "Failed to add to Annotation Management: {error}": "No se pudo añadir a Gestión de anotaciones: {error}", + "Reusing the Console game connection: {title} (PID {pid})": "Reutilizando la conexión del juego de la consola: {title} (PID {pid})" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 1898721d..24b6a2a1 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -71,7 +71,6 @@ "No Python editor is available; copied instead: {text}": "利用可能な Python エディターがないためコピーしました: {text}", "ok-script-toolkit: Generating source image annotation…": "ok-script-toolkit: 元画像の注釈を生成しています…", "Failed to generate annotated image: source image could not be decoded or was missing": "注釈画像を生成できませんでした。元画像をデコードできないか、ファイルがありません。", - "Template Gallery": "テンプレートギャラリー", "Category": "カテゴリ", "Description": "説明", "Current": "現在", @@ -140,14 +139,12 @@ "Connecting to game window…": "ゲームウィンドウに接続中(未起動なら自動で起動します)…", "Failed to connect game window: {error}": "ゲームウィンドウへの接続に失敗しました: {error}", "Failed to disconnect game window: {error}": "ゲームウィンドウの接続解除に失敗しました: {error}", - "Reusing game connection from toolbox: {title} (PID {pid})": "ツールボックスのゲーム接続を再利用: {title}(PID {pid})", "▶ Enabling debug overlay…": "▶ デバッグ オーバーレイを有効化中…", "⏹ Disabling debug overlay…": "⏹ デバッグ オーバーレイを無効化中…", "⏹ Debug overlay: disabled": "⏹ デバッグ オーバーレイ: 無効", "ok-script Overlay Host": "ok-script オーバーレイホスト", "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.": "オーバーレイ常駐中:ゲーム画面の2点を Alt+右クリックすると枠座標がクリップボードにコピーされます。", "Overlay host stopped.": "オーバーレイホストを停止しました。", - "Added to template assets: {name}": "テンプレートアセットに追加しました: {name}", "Annotation Editor": "アノテーションエディター", "Auto-detected window config: {config}": "ウィンドウ設定を自動検出: {config}", "Built-in default": "組み込みの既定値", @@ -157,7 +154,6 @@ "Deleted: {name}": "削除しました: {name}", "Effective value and where it comes from. Use the button to drop your override.": "有効値とその由来。ボタンで自分の上書きを解除できます。", "Enter game window title pattern (regex), or leave empty for all windows": "ゲームウィンドウのタイトル正規表現を入力(空欄ですべてのウィンドウ)", - "Failed to add to template assets: {error}": "テンプレートアセットへの追加に失敗: {error}", "Failed to delete: {name}": "削除に失敗: {name}", "Deleted the record for {name}, but the temporary file is still at {path}.": "{name} の記録は削除しましたが、一時ファイルが {path} に残っています。", "Failed to save screenshot.": "スクリーンショットの保存に失敗しました。", @@ -181,7 +177,6 @@ "Screenshot saved: {name}": "スクリーンショットを保存しました: {name}", "Screenshot script not found. capture_game_window.py is missing from the extension.": "スクリーンショットスクリプトが見つかりません。拡張機能に capture_game_window.py がありません。", "Source: {layer}": "由来: {layer}", - "Template Assets": "テンプレートアセット", "The dragged screenshot is no longer available.": "ドラッグされたスクリーンショットは存在しません。", "enhancementFallbackName": "強化 {index}", "importImagesFilter": "画像", @@ -260,5 +255,9 @@ "Could not publish positions: {error}": "位置リソースを公開できませんでした: {error}", "Published positions: {files}": "位置リソースを公開しました: {files}", "Resource Preview": "リソースプレビュー", - "No annotations available to publish.": "公開できるアノテーションはありません。" + "No annotations available to publish.": "公開できるアノテーションはありません。", + "Annotation Management": "アノテーション管理", + "Added to Annotation Management: {name}": "アノテーション管理に追加しました: {name}", + "Failed to add to Annotation Management: {error}": "アノテーション管理への追加に失敗: {error}", + "Reusing the Console game connection: {title} (PID {pid})": "コンソールのゲーム接続を再利用: {title}(PID {pid})" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 657ce40a..9087b6a1 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -120,7 +120,6 @@ "No Python editor is available; copied instead: {text}": "No Python editor is available; copied instead: {text}", "ok-script-toolkit: Generating source image annotation…": "ok-script-toolkit: Generating source image annotation…", "Failed to generate annotated image: source image could not be decoded or was missing": "Failed to generate annotated image: source image could not be decoded or was missing", - "Template Gallery": "Template Gallery", "Category": "Category", "Description": "Description", "Current": "Current", @@ -149,14 +148,12 @@ "Connecting to game window…": "Connecting to game window (auto-starts the game if not running)…", "Failed to connect game window: {error}": "Failed to connect game window: {error}", "Failed to disconnect game window: {error}": "Failed to disconnect game window: {error}", - "Reusing game connection from toolbox: {title} (PID {pid})": "Reusing game connection from toolbox: {title} (PID {pid})", "▶ Enabling debug overlay…": "▶ Enabling debug overlay…", "⏹ Disabling debug overlay…": "⏹ Disabling debug overlay…", "⏹ Debug overlay: disabled": "⏹ Debug overlay: disabled", "ok-script Overlay Host": "ok-script Overlay Host", "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.": "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.", "Overlay host stopped.": "Overlay host stopped.", - "Added to template assets: {name}": "Added to template assets: {name}", "Annotation Editor": "Annotation Editor", "Auto-detected window config: {config}": "Auto-detected window config: {config}", "Built-in default": "Built-in default", @@ -166,7 +163,6 @@ "Deleted: {name}": "Deleted: {name}", "Effective value and where it comes from. Use the button to drop your override.": "Effective value and where it comes from. Use the button to drop your override.", "Enter game window title pattern (regex), or leave empty for all windows": "Enter game window title pattern (regex), or leave empty for all windows", - "Failed to add to template assets: {error}": "Failed to add to template assets: {error}", "Failed to delete: {name}": "Failed to delete: {name}", "Deleted the record for {name}, but the temporary file is still at {path}.": "Deleted the record for {name}, but the temporary file is still at {path}.", "Failed to save screenshot.": "Failed to save screenshot.", @@ -190,7 +186,6 @@ "Screenshot saved: {name}": "Screenshot saved: {name}", "Source: {layer}": "Source: {layer}", "Screenshot script not found. capture_game_window.py is missing from the extension.": "Screenshot script not found. capture_game_window.py is missing from the extension.", - "Template Assets": "Template Assets", "The dragged screenshot is no longer available.": "The dragged screenshot is no longer available.", "enhancementFallbackName": "Enhancement {index}", "importImagesFilter": "Images", @@ -260,5 +255,9 @@ "Could not publish positions: {error}": "Could not publish positions: {error}", "Published positions: {files}": "Published positions: {files}", "Resource Preview": "Resource Preview", - "No annotations available to publish.": "No annotations available to publish." + "No annotations available to publish.": "No annotations available to publish.", + "Annotation Management": "Annotation Management", + "Added to Annotation Management: {name}": "Added to Annotation Management: {name}", + "Failed to add to Annotation Management: {error}": "Failed to add to Annotation Management: {error}", + "Reusing the Console game connection: {title} (PID {pid})": "Reusing the Console game connection: {title} (PID {pid})" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 62bf6bbe..bd91c150 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -71,7 +71,6 @@ "No Python editor is available; copied instead: {text}": "사용 가능한 Python 편집기가 없어 대신 복사했습니다: {text}", "ok-script-toolkit: Generating source image annotation…": "ok-script-toolkit: 원본 이미지 주석 생성 중…", "Failed to generate annotated image: source image could not be decoded or was missing": "주석 이미지를 생성하지 못했습니다. 원본 이미지를 디코딩할 수 없거나 파일이 없습니다.", - "Template Gallery": "템플릿 갤러리", "Category": "분류", "Description": "설명", "Current": "현재", @@ -140,14 +139,12 @@ "Connecting to game window…": "게임 창에 연결하는 중(실행 중이 아니면 자동으로 시작)…", "Failed to connect game window: {error}": "게임 창 연결 실패: {error}", "Failed to disconnect game window: {error}": "게임 창 연결 해제 실패: {error}", - "Reusing game connection from toolbox: {title} (PID {pid})": "도구 상자의 게임 연결 재사용: {title}(PID {pid})", "▶ Enabling debug overlay…": "▶ 디버그 오버레이 활성화 중…", "⏹ Disabling debug overlay…": "⏹ 디버그 오버레이 비활성화 중…", "⏹ Debug overlay: disabled": "⏹ 디버그 오버레이: 꺼짐", "ok-script Overlay Host": "ok-script 오버레이 호스트", "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.": "오버레이 상주 중: 게임 화면의 두 모서리를 Alt+오른쪽 클릭하면 좌표가 클립보드에 복사됩니다.", "Overlay host stopped.": "오버레이 호스트가 중지되었습니다.", - "Added to template assets: {name}": "템플릿 에셋에 추가됨: {name}", "Annotation Editor": "어노테이션 편집기", "Auto-detected window config: {config}": "창 설정 자동 감지: {config}", "Built-in default": "내장 기본값", @@ -157,7 +154,6 @@ "Deleted: {name}": "삭제됨: {name}", "Effective value and where it comes from. Use the button to drop your override.": "적용 값과 그 출처입니다. 버튼으로 내 재정의를 해제할 수 있습니다.", "Enter game window title pattern (regex), or leave empty for all windows": "게임 창 제목 정규식을 입력하세요(비우면 모든 창)", - "Failed to add to template assets: {error}": "템플릿 에셋 추가 실패: {error}", "Failed to delete: {name}": "삭제 실패: {name}", "Deleted the record for {name}, but the temporary file is still at {path}.": "{name} 기록은 삭제했지만 임시 파일이 {path}에 남아 있습니다.", "Failed to save screenshot.": "스크린샷 저장에 실패했습니다.", @@ -181,7 +177,6 @@ "Screenshot saved: {name}": "스크린샷 저장됨: {name}", "Screenshot script not found. capture_game_window.py is missing from the extension.": "스크린샷 스크립트를 찾을 수 없습니다. 확장에 capture_game_window.py 가 없습니다.", "Source: {layer}": "출처: {layer}", - "Template Assets": "템플릿 에셋", "The dragged screenshot is no longer available.": "드래그한 스크린샷이 더 이상 존재하지 않습니다.", "enhancementFallbackName": "강화 {index}", "importImagesFilter": "이미지", @@ -260,5 +255,9 @@ "Could not publish positions: {error}": "위치 리소스를 게시할 수 없습니다: {error}", "Published positions: {files}": "게시된 위치 리소스: {files}", "Resource Preview": "리소스 미리보기", - "No annotations available to publish.": "게시할 주석이 없습니다." + "No annotations available to publish.": "게시할 주석이 없습니다.", + "Annotation Management": "어노테이션 관리", + "Added to Annotation Management: {name}": "어노테이션 관리에 추가됨: {name}", + "Failed to add to Annotation Management: {error}": "어노테이션 관리 추가 실패: {error}", + "Reusing the Console game connection: {title} (PID {pid})": "콘솔의 게임 연결 재사용: {title}(PID {pid})" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index ecd7c75a..d753f36a 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -120,7 +120,6 @@ "No Python editor is available; copied instead: {text}": "没有可用的 Python 编辑器,已改为复制:{text}", "ok-script-toolkit: Generating source image annotation…": "ok-script-toolkit:正在生成原图标注…", "Failed to generate annotated image: source image could not be decoded or was missing": "生成标注图失败:原图解码失败或文件缺失", - "Template Gallery": "模板面板", "Category": "分类", "Description": "描述", "Current": "当前", @@ -140,14 +139,12 @@ "Connecting to game window…": "正在连接游戏窗口(游戏未运行时会自动启动)…", "Failed to connect game window: {error}": "连接游戏窗口失败:{error}", "Failed to disconnect game window: {error}": "断开游戏窗口失败:{error}", - "Reusing game connection from toolbox: {title} (PID {pid})": "复用工具箱游戏连接:{title}(PID {pid})", "▶ Enabling debug overlay…": "▶ 正在开启调试浮层…", "⏹ Disabling debug overlay…": "⏹ 正在关闭调试浮层…", "⏹ Debug overlay: disabled": "⏹ 调试浮层:已关闭", "ok-script Overlay Host": "ok-script 浮层宿主", "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.": "浮层已常驻:Alt+右键点游戏画面两个角,框选坐标自动复制到剪贴板。", "Overlay host stopped.": "浮层宿主已停止。", - "Added to template assets: {name}": "已添加到模板素材: {name}", "Annotation Editor": "标注编辑器", "Auto-detected window config: {config}": "自动识别窗口配置: {config}", "Built-in default": "内置默认", @@ -157,7 +154,6 @@ "Deleted: {name}": "已删除: {name}", "Effective value and where it comes from. Use the button to drop your override.": "生效值及其来源。点按钮可清除你的覆盖。", "Enter game window title pattern (regex), or leave empty for all windows": "输入游戏窗口标题正则,留空表示匹配所有窗口", - "Failed to add to template assets: {error}": "添加到模板素材失败: {error}", "Failed to delete: {name}": "删除失败: {name}", "Deleted the record for {name}, but the temporary file is still at {path}.": "已删除 {name} 的记录,但临时文件还在 {path}。", "Failed to save screenshot.": "截图保存失败。", @@ -181,7 +177,6 @@ "Screenshot saved: {name}": "截图已保存: {name}", "Screenshot script not found. capture_game_window.py is missing from the extension.": "未找到截图脚本,扩展缺少 capture_game_window.py。", "Source: {layer}": "来源:{layer}", - "Template Assets": "模板素材", "The dragged screenshot is no longer available.": "被拖拽的截图已不存在。", "enhancementFallbackName": "强化 {index}", "importImagesFilter": "图片", @@ -260,5 +255,9 @@ "Could not publish positions: {error}": "无法发布位置资源:{error}", "Published positions: {files}": "已发布位置资源:{files}", "Resource Preview": "资源预览", - "No annotations available to publish.": "没有可发布的标注。" + "No annotations available to publish.": "没有可发布的标注。", + "Annotation Management": "标注管理", + "Added to Annotation Management: {name}": "已添加到标注管理:{name}", + "Failed to add to Annotation Management: {error}": "添加到标注管理失败:{error}", + "Reusing the Console game connection: {title} (PID {pid})": "复用控制台的游戏连接:{title}(PID {pid})" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 47f5500e..ecdbfc1f 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -71,7 +71,6 @@ "No Python editor is available; copied instead: {text}": "沒有可用的 Python 編輯器,已改為複製:{text}", "ok-script-toolkit: Generating source image annotation…": "ok-script-toolkit:正在產生原圖標註…", "Failed to generate annotated image: source image could not be decoded or was missing": "產生標註圖失敗:原圖無法解碼或檔案遺失", - "Template Gallery": "模板面板", "Category": "分類", "Description": "描述", "Current": "目前", @@ -140,14 +139,12 @@ "Connecting to game window…": "正在連線遊戲視窗(遊戲未執行時會自動啟動)…", "Failed to connect game window: {error}": "連線遊戲視窗失敗:{error}", "Failed to disconnect game window: {error}": "中斷遊戲視窗連線失敗:{error}", - "Reusing game connection from toolbox: {title} (PID {pid})": "複用工具箱遊戲連線:{title}(PID {pid})", "▶ Enabling debug overlay…": "▶ 正在開啟除錯浮層…", "⏹ Disabling debug overlay…": "⏹ 正在關閉除錯浮層…", "⏹ Debug overlay: disabled": "⏹ 除錯浮層:已關閉", "ok-script Overlay Host": "ok-script 浮層宿主", "Overlay host ready: Alt+Right-click two corners in the game to copy box coordinates.": "浮層已常駐:Alt+右鍵點遊戲畫面兩個角,框選座標自動複製到剪貼簿。", "Overlay host stopped.": "浮層宿主已停止。", - "Added to template assets: {name}": "已加入模板素材: {name}", "Annotation Editor": "標註編輯器", "Auto-detected window config: {config}": "自動辨識視窗設定: {config}", "Built-in default": "內建預設", @@ -157,7 +154,6 @@ "Deleted: {name}": "已刪除: {name}", "Effective value and where it comes from. Use the button to drop your override.": "生效值及其來源。點按鈕可清除你的覆寫。", "Enter game window title pattern (regex), or leave empty for all windows": "輸入遊戲視窗標題正則,留空表示符合所有視窗", - "Failed to add to template assets: {error}": "加入模板素材失敗: {error}", "Failed to delete: {name}": "刪除失敗: {name}", "Deleted the record for {name}, but the temporary file is still at {path}.": "已刪除 {name} 的記錄,但暫存檔還在 {path}。", "Failed to save screenshot.": "截圖儲存失敗。", @@ -181,7 +177,6 @@ "Screenshot saved: {name}": "截圖已儲存: {name}", "Screenshot script not found. capture_game_window.py is missing from the extension.": "找不到截圖腳本,擴充功能缺少 capture_game_window.py。", "Source: {layer}": "來源:{layer}", - "Template Assets": "模板素材", "The dragged screenshot is no longer available.": "被拖曳的截圖已不存在。", "enhancementFallbackName": "強化 {index}", "importImagesFilter": "圖片", @@ -260,5 +255,9 @@ "Could not publish positions: {error}": "無法發佈位置資源:{error}", "Published positions: {files}": "已發佈位置資源:{files}", "Resource Preview": "資源預覽", - "No annotations available to publish.": "沒有可發佈的標註。" + "No annotations available to publish.": "沒有可發佈的標註。", + "Annotation Management": "標註管理", + "Added to Annotation Management: {name}": "已加入標註管理:{name}", + "Failed to add to Annotation Management: {error}": "加入標註管理失敗:{error}", + "Reusing the Console game connection: {title} (PID {pid})": "複用控制台的遊戲連線:{title}(PID {pid})" } diff --git a/l10n/webview.en.json b/l10n/webview.en.json index cfac6a54..6e248dd9 100644 --- a/l10n/webview.en.json +++ b/l10n/webview.en.json @@ -123,7 +123,7 @@ "valueLabel": "Value", "confirmDelete": "Delete “{name}”?\nA .bak backup is created before saving.", "copied": "Copied: {text}", - "taskTitle": "ok-script Task Launcher", + "taskTitle": "ok-script Console", "noTasks": "No tasks found.\nConfigure the project path first.", "parameters": "Parameters", "collapseParameters": "Collapse Parameters", @@ -244,7 +244,7 @@ "accountReadFailed": "Store file could not be parsed (see executor output).", "overriddenTasksLabel": "Overridden tasks", "templatesSearch": "Search template names…", - "templatesTitle": "Template Gallery", + "templatesTitle": "Resource Preview", "templatesHint": "Click=insert · Double-click=copy · Use the bottom-right buttons for direct actions", "templatesCount": "{shown}/{total} templates", "noTemplates": "No templates found.", @@ -256,7 +256,7 @@ "thumbnailLoadFailed": "Thumbnail failed to load", "templateSize": "Size: {width}×{height}", "templateSource": "Source: {path}", - "templateAssetsTitle": "Template Assets", + "templateAssetsTitle": "Annotation Management", "templateAssetsHint": "Click an image or its edit button to open annotations", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", @@ -325,8 +325,8 @@ "tempEmpty": "No temp screenshots yet.\nPress Ctrl+V to paste, or click Capture.", "tempCount": "{shown}/{total}", "tempDelete": "Delete", - "tempSendToAssets": "Add to annotation manager", - "tempDragHint": "Drag onto the annotation manager to import", + "tempSendToAssets": "Add to Annotation Management", + "tempDragHint": "Drag onto Annotation Management to import", "tempCoordCopied": "Copied coordinates: {text}", "assetDropHint": "Release to import into ok_templates", "uncategorized": "Uncategorized", @@ -358,7 +358,7 @@ "publish": "Publish", "assetsHint": "Click an image or its edit button to edit boxes. Publish writes the runtime file.", "galleryHint": "Click inserts to_box(). Double-click copies the attribute path.", - "assetsEmpty": "No template images yet. Add them in Template Assets.", + "assetsEmpty": "No template images yet. Add them in Annotation Management.", "galleryEmpty": "No runtime boxes yet. Publish them from Box Assets.", "insertExpression": "Insert expression", "copyExpression": "Copy expression", diff --git a/l10n/webview.es.json b/l10n/webview.es.json index 47efcf0f..793f02d0 100644 --- a/l10n/webview.es.json +++ b/l10n/webview.es.json @@ -123,7 +123,7 @@ "valueLabel": "Valor", "confirmDelete": "¿Eliminar «{name}»?\nSe creará automáticamente una copia .bak antes de guardar.", "copied": "Copiado: {text}", - "taskTitle": "Lanzador de tareas de ok-script", + "taskTitle": "Consola ok-script", "noTasks": "No se encontraron tareas.\nConfigure primero la ruta del proyecto.", "parameters": "Parámetros", "collapseParameters": "Contraer parámetros", @@ -244,7 +244,7 @@ "accountReadFailed": "No se pudo analizar el archivo del almacén (ver la salida del ejecutor).", "overriddenTasksLabel": "Tareas con anulaciones", "templatesSearch": "Buscar nombres de plantillas…", - "templatesTitle": "Galería de plantillas", + "templatesTitle": "Vista previa de recursos", "templatesHint": "Clic=insertar · Doble clic=copiar · Use los botones inferiores derechos para acciones directas", "templatesCount": "{shown}/{total} plantillas", "noTemplates": "No se encontraron plantillas.", @@ -256,7 +256,7 @@ "thumbnailLoadFailed": "Error al cargar la miniatura", "templateSize": "Tamaño: {width}×{height}", "templateSource": "Origen: {path}", - "templateAssetsTitle": "Template Assets", + "templateAssetsTitle": "Gestión de anotaciones", "templateAssetsHint": "Pulse la imagen o el botón de editar para abrir anotaciones", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", @@ -325,8 +325,8 @@ "tempEmpty": "Aún no hay capturas temporales.\nPulsa Ctrl+V para pegar o haz clic en Capturar.", "tempCount": "{shown}/{total}", "tempDelete": "Eliminar", - "tempSendToAssets": "Añadir al gestor de anotaciones", - "tempDragHint": "Arrastra al gestor de anotaciones para importar", + "tempSendToAssets": "Añadir a Gestión de anotaciones", + "tempDragHint": "Arrastra a Gestión de anotaciones para importar", "tempCoordCopied": "Coordenadas copiadas: {text}", "assetDropHint": "Suelta para importar en ok_templates", "uncategorized": "Uncategorized", @@ -358,7 +358,7 @@ "publish": "Publish", "assetsHint": "Pulse la imagen o el botón de editar para editar cuadros. Publicar escribe el archivo de ejecución.", "galleryHint": "Click inserts to_box(). Double-click copies the attribute path.", - "assetsEmpty": "No template images yet. Add them in Template Assets.", + "assetsEmpty": "Aún no hay imágenes de plantilla. Añádelas en Gestión de anotaciones.", "galleryEmpty": "No runtime boxes yet. Publish them from Box Assets.", "insertExpression": "Insertar expresión", "copyExpression": "Copiar expresión", diff --git a/l10n/webview.ja.json b/l10n/webview.ja.json index d6f6c5fe..f4f2bedb 100644 --- a/l10n/webview.ja.json +++ b/l10n/webview.ja.json @@ -123,7 +123,7 @@ "valueLabel": "値", "confirmDelete": "「{name}」を削除しますか?\n保存前に .bak バックアップを自動作成します。", "copied": "コピーしました: {text}", - "taskTitle": "ok-script タスク起動", + "taskTitle": "ok-script コンソール", "noTasks": "タスクが見つかりません。\n先にプロジェクトパスを設定してください。", "parameters": "パラメータ", "collapseParameters": "パラメーターを折りたたむ", @@ -244,7 +244,7 @@ "accountReadFailed": "ストアファイルを解析できません(実行器の出力を参照)。", "overriddenTasksLabel": "上書きありのタスク", "templatesSearch": "テンプレート名を検索…", - "templatesTitle": "テンプレートギャラリー", + "templatesTitle": "リソースプレビュー", "templatesHint": "クリック=挿入 · ダブルクリック=コピー · 右下のボタンで直接操作", "templatesCount": "{shown}/{total} テンプレート", "noTemplates": "テンプレートが見つかりません。", @@ -256,7 +256,7 @@ "thumbnailLoadFailed": "サムネイルの読み込みに失敗", "templateSize": "サイズ: {width}×{height}", "templateSource": "ソース: {path}", - "templateAssetsTitle": "Template Assets", + "templateAssetsTitle": "アノテーション管理", "templateAssetsHint": "画像または編集ボタンをクリックしてアノテーションを開く", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", @@ -326,7 +326,7 @@ "tempCount": "{shown}/{total}", "tempDelete": "削除", "tempSendToAssets": "アノテーション管理に追加", - "tempDragHint": "アノテーション管理へドラッグして取り込みます", + "tempDragHint": "アノテーション管理へドラッグしてインポート", "tempCoordCopied": "座標をコピーしました: {text}", "assetDropHint": "ドロップして ok_templates に取り込みます", "uncategorized": "Uncategorized", @@ -358,7 +358,7 @@ "publish": "Publish", "assetsHint": "画像または編集ボタンをクリックして枠を編集。公開後に実行時ファイルへ反映されます。", "galleryHint": "Click inserts to_box(). Double-click copies the attribute path.", - "assetsEmpty": "No template images yet. Add them in Template Assets.", + "assetsEmpty": "テンプレート元画像がありません。アノテーション管理で追加してください。", "galleryEmpty": "No runtime boxes yet. Publish them from Box Assets.", "insertExpression": "式を挿入", "copyExpression": "式をコピー", diff --git a/l10n/webview.ko.json b/l10n/webview.ko.json index 1df49470..f54b1336 100644 --- a/l10n/webview.ko.json +++ b/l10n/webview.ko.json @@ -123,7 +123,7 @@ "valueLabel": "값", "confirmDelete": "“{name}”을(를) 삭제하시겠습니까?\n저장 전에 .bak 백업을 자동 생성합니다.", "copied": "복사됨: {text}", - "taskTitle": "ok-script 작업 실행", + "taskTitle": "ok-script 콘솔", "noTasks": "작업을 찾을 수 없습니다.\n먼저 프로젝트 경로를 설정하세요.", "parameters": "매개변수", "collapseParameters": "매개변수 접기", @@ -244,7 +244,7 @@ "accountReadFailed": "스토어 파일을 해석할 수 없습니다(실행기 출력 참조).", "overriddenTasksLabel": "재정의가 있는 작업", "templatesSearch": "템플릿 이름 검색…", - "templatesTitle": "템플릿 갤러리", + "templatesTitle": "리소스 미리보기", "templatesHint": "클릭=삽입 · 더블클릭=복사 · 오른쪽 아래 버튼으로 바로 실행", "templatesCount": "{shown}/{total} 템플릿", "noTemplates": "템플릿을 찾을 수 없습니다.", @@ -256,7 +256,7 @@ "thumbnailLoadFailed": "미리 보기 불러오기 실패", "templateSize": "크기: {width}×{height}", "templateSource": "소스: {path}", - "templateAssetsTitle": "Template Assets", + "templateAssetsTitle": "어노테이션 관리", "templateAssetsHint": "이미지 또는 편집 버튼을 클릭하여 주석 편집기 열기", "noTemplateAssets": "No template assets yet.\nClick Import to add images.", "noTemplateAssetsHint": "No template assets.", @@ -326,7 +326,7 @@ "tempCount": "{shown}/{total}", "tempDelete": "삭제", "tempSendToAssets": "어노테이션 관리에 추가", - "tempDragHint": "어노테이션 관리로 드래그해 가져오기", + "tempDragHint": "어노테이션 관리로 끌어다 놓아 가져오기", "tempCoordCopied": "좌표 복사됨: {text}", "assetDropHint": "놓으면 ok_templates 로 가져옵니다", "uncategorized": "Uncategorized", @@ -358,7 +358,7 @@ "publish": "Publish", "assetsHint": "이미지 또는 편집 버튼을 클릭하여 박스를 편집하세요. 게시하면 런타임 파일에 반영됩니다.", "galleryHint": "Click inserts to_box(). Double-click copies the attribute path.", - "assetsEmpty": "No template images yet. Add them in Template Assets.", + "assetsEmpty": "템플릿 원본 이미지가 없습니다. 어노테이션 관리에서 추가하세요.", "galleryEmpty": "No runtime boxes yet. Publish them from Box Assets.", "insertExpression": "표현식 삽입", "copyExpression": "표현식 복사", diff --git a/l10n/webview.zh-cn.json b/l10n/webview.zh-cn.json index 6195f224..9d60b461 100644 --- a/l10n/webview.zh-cn.json +++ b/l10n/webview.zh-cn.json @@ -123,7 +123,7 @@ "valueLabel": "值", "confirmDelete": "确定删除“{name}”吗?\n保存前会自动创建 .bak 备份。", "copied": "已复制:{text}", - "taskTitle": "ok-script 任务启动", + "taskTitle": "ok-script 控制台", "noTasks": "未找到任务。\n请先在设置中配置项目路径。", "parameters": "参数", "collapseParameters": "收起参数", @@ -244,7 +244,7 @@ "accountReadFailed": "存储文件无法解析(详情见执行器输出)。", "overriddenTasksLabel": "有覆盖的任务", "templatesSearch": "搜索模板名…", - "templatesTitle": "模板面板", + "templatesTitle": "资源预览", "templatesHint": "单击=插入 · 双击=复制 · 右下角按钮可直接操作", "templatesCount": "{shown}/{total} 个模板", "noTemplates": "未找到任何模板。", @@ -256,7 +256,7 @@ "thumbnailLoadFailed": "缩略图加载失败", "templateSize": "尺寸: {width}×{height}", "templateSource": "来源: {path}", - "templateAssetsTitle": "模板素材管理", + "templateAssetsTitle": "标注管理", "templateAssetsHint": "单击或点击编辑按钮打开标注编辑器", "noTemplateAssets": "暂无模板素材。\n点击 Import 导入图片。", "noTemplateAssetsHint": "暂无模板素材。", diff --git a/l10n/webview.zh-tw.json b/l10n/webview.zh-tw.json index 13403b84..1754ec37 100644 --- a/l10n/webview.zh-tw.json +++ b/l10n/webview.zh-tw.json @@ -123,7 +123,7 @@ "valueLabel": "值", "confirmDelete": "確定要刪除「{name}」嗎?\n儲存前會自動建立 .bak 備份。", "copied": "已複製:{text}", - "taskTitle": "ok-script 任務啟動", + "taskTitle": "ok-script 控制台", "noTasks": "找不到任務。\n請先在設定中配置專案路徑。", "parameters": "參數", "collapseParameters": "收合參數", @@ -244,7 +244,7 @@ "accountReadFailed": "儲存檔案無法解析(詳見執行器輸出)。", "overriddenTasksLabel": "有覆寫的任務", "templatesSearch": "搜尋模板名稱…", - "templatesTitle": "模板面板", + "templatesTitle": "資源預覽", "templatesHint": "單擊=插入 · 雙擊=複製 · 右下角按鈕可直接操作", "templatesCount": "{shown}/{total} 個模板", "noTemplates": "找不到任何模板。", @@ -256,7 +256,7 @@ "thumbnailLoadFailed": "縮圖載入失敗", "templateSize": "尺寸:{width}×{height}", "templateSource": "來源:{path}", - "templateAssetsTitle": "模板素材管理", + "templateAssetsTitle": "標註管理", "templateAssetsHint": "單擊或點擊編輯按鈕開啟標註編輯器", "noTemplateAssets": "暂无模板素材。\n点击 Import 导入图片。", "noTemplateAssetsHint": "暂无模板素材。", diff --git a/package.json b/package.json index a329b8da..b40b781d 100644 --- a/package.json +++ b/package.json @@ -254,173 +254,205 @@ "mac": "cmd+alt+s" } ], - "configuration": { - "title": "%configuration.title%", - "properties": { - "okScriptToolkit.langDirectory": { - "type": "string", - "default": "assets/lang", - "description": "%config.langDirectory%" - }, - "okScriptToolkit.poDirectory": { - "type": "string", - "default": "i18n", - "description": "%config.poDirectory%" - }, - "okScriptToolkit.enablePoData": { - "type": "boolean", - "default": true, - "description": "%config.enablePoData%" - }, - "okScriptToolkit.poDomains": { - "type": "array", - "items": { - "type": "string" + "configuration": [ + { + "id": "okScriptToolkit.codeHints", + "title": "%configuration.codeHints%", + "order": 1, + "properties": { + "okScriptToolkit.langDirectory": { + "type": "string", + "default": "assets/lang", + "description": "%config.langDirectory%" }, - "default": [ - "ocr" - ], - "description": "%config.poDomains%" - }, - "okScriptToolkit.displayLocale": { - "type": "string", - "default": "auto", - "enum": [ - "auto", - "zh_CN", - "zh_TW", - "en_US", - "ja_JP", - "ko_KR", - "es_ES" - ], - "description": "%config.displayLocale%" - }, - "okScriptToolkit.enableInlayHints": { - "type": "boolean", - "default": true, - "description": "%config.enableInlayHints%" - }, - "okScriptToolkit.featureAliases": { - "type": "array", - "items": { - "type": "string" + "okScriptToolkit.poDirectory": { + "type": "string", + "default": "i18n", + "description": "%config.poDirectory%" }, - "default": [ - "fL", - "FeatureList", - "Labels" - ], - "description": "%config.featureAliases%" - }, - "okScriptToolkit.labelEnumPath": { - "type": "string", - "default": "", - "scope": "resource", - "markdownDescription": "%config.labelEnumPath%" - }, - "okScriptToolkit.labelEnumName": { - "type": "string", - "default": "", - "scope": "resource", - "markdownDescription": "%config.labelEnumName%" - }, - "okScriptToolkit.positionJsonPath": { - "type": "string", - "default": "src/scene/positions.json", - "scope": "resource", - "markdownDescription": "%config.positionJsonPath%" - }, - "okScriptToolkit.positionPythonDirectory": { - "type": "string", - "default": "src/scene", - "scope": "resource", - "markdownDescription": "%config.positionPythonDirectory%" - }, - "okScriptToolkit.effectsFile": { - "type": "string", - "default": "src/data/effects.py", - "description": "%config.effectsFile%" - }, - "okScriptToolkit.okScriptProjectPath": { - "type": "string", - "default": "", - "markdownDescription": "%config.okScriptProjectPath%" - }, - "okScriptToolkit.okScriptPython": { - "type": "string", - "default": "", - "markdownDescription": "%config.okScriptPython%" - }, - "okScriptToolkit.captureMethod": { - "type": "string", - "default": "auto", - "enum": [ - "auto", - "wgc", - "bitblt", - "foreground" - ], - "markdownDescription": "%config.captureMethod%" - }, - "okScriptToolkit.characterProjectPath": { - "type": "string", - "default": "", - "markdownDescription": "%config.characterProjectPath%" - }, - "okScriptToolkit.characterMasterFile": { - "type": "string", - "default": "assets/data/characters.json", - "description": "%config.characterMasterFile%" - }, - "okScriptToolkit.characterSkillsDirectory": { - "type": "string", - "default": "assets/data/character_skills", - "description": "%config.characterSkillsDirectory%" - }, - "okScriptToolkit.characterLocaleFile": { - "type": "string", - "default": "assets/lang/characters.json", - "description": "%config.characterLocaleFile%" - }, - "okScriptToolkit.characterAvatarTemplateRegex": { - "type": "string", - "default": "^battle[_-]?icon[_-]?", - "description": "%config.characterAvatarTemplateRegex%" - }, - "okScriptToolkit.okTemplatesDirectory": { - "type": "string", - "default": "ok_templates", - "description": "%config.okTemplatesDirectory%" - }, - "okScriptToolkit.copyCoordsSpace": { - "type": "boolean", - "default": true, - "scope": "application", - "description": "%config.copyCoordsSpace%" - }, - "okScriptToolkit.annotationKeybindings": { - "type": "object", - "default": { - "drawBbox": "r", - "copyCoords": "c", - "deleteMode": "d", - "undo": "ctrl+z", - "redo": "ctrl+y", - "copy": "ctrl+c", - "paste": "ctrl+v", - "deleteSelected": "Delete", - "prevImage": "ArrowLeft", - "nextImage": "ArrowRight", - "cycleMode": "m" + "okScriptToolkit.enablePoData": { + "type": "boolean", + "default": true, + "description": "%config.enablePoData%" + }, + "okScriptToolkit.poDomains": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "ocr" + ], + "description": "%config.poDomains%" + }, + "okScriptToolkit.displayLocale": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "zh_CN", + "zh_TW", + "en_US", + "ja_JP", + "ko_KR", + "es_ES" + ], + "description": "%config.displayLocale%" + }, + "okScriptToolkit.enableInlayHints": { + "type": "boolean", + "default": true, + "description": "%config.enableInlayHints%" + }, + "okScriptToolkit.featureAliases": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "fL", + "FeatureList", + "Labels" + ], + "description": "%config.featureAliases%" + }, + "okScriptToolkit.effectsFile": { + "type": "string", + "default": "src/data/effects.py", + "description": "%config.effectsFile%" + } + } + }, + { + "id": "okScriptToolkit.resources", + "title": "%configuration.resources%", + "order": 2, + "properties": { + "okScriptToolkit.okTemplatesDirectory": { + "type": "string", + "default": "ok_templates", + "description": "%config.okTemplatesDirectory%" + }, + "okScriptToolkit.labelEnumPath": { + "type": "string", + "default": "", + "scope": "resource", + "markdownDescription": "%config.labelEnumPath%" + }, + "okScriptToolkit.labelEnumName": { + "type": "string", + "default": "", + "scope": "resource", + "markdownDescription": "%config.labelEnumName%" + }, + "okScriptToolkit.positionJsonPath": { + "type": "string", + "default": "src/scene/positions.json", + "scope": "resource", + "markdownDescription": "%config.positionJsonPath%" + }, + "okScriptToolkit.positionPythonDirectory": { + "type": "string", + "default": "src/scene", + "scope": "resource", + "markdownDescription": "%config.positionPythonDirectory%" + }, + "okScriptToolkit.annotationKeybindings": { + "type": "object", + "default": { + "drawBbox": "r", + "copyCoords": "c", + "deleteMode": "d", + "undo": "ctrl+z", + "redo": "ctrl+y", + "copy": "ctrl+c", + "paste": "ctrl+v", + "deleteSelected": "Delete", + "prevImage": "ArrowLeft", + "nextImage": "ArrowRight", + "cycleMode": "m" + }, + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "%config.annotationKeybindings%" + } + } + }, + { + "id": "okScriptToolkit.screenshots", + "title": "%configuration.screenshots%", + "order": 3, + "properties": { + "okScriptToolkit.captureMethod": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "wgc", + "bitblt", + "foreground" + ], + "markdownDescription": "%config.captureMethod%" }, - "additionalProperties": { - "type": "string" + "okScriptToolkit.copyCoordsSpace": { + "type": "boolean", + "default": true, + "scope": "application", + "description": "%config.copyCoordsSpace%" + } + } + }, + { + "id": "okScriptToolkit.console", + "title": "%configuration.console%", + "order": 4, + "properties": { + "okScriptToolkit.okScriptProjectPath": { + "type": "string", + "default": "", + "markdownDescription": "%config.okScriptProjectPath%" }, - "markdownDescription": "%config.annotationKeybindings%" + "okScriptToolkit.okScriptPython": { + "type": "string", + "default": "", + "markdownDescription": "%config.okScriptPython%" + } + } + }, + { + "id": "okScriptToolkit.characters", + "title": "%configuration.characters%", + "order": 5, + "properties": { + "okScriptToolkit.characterProjectPath": { + "type": "string", + "default": "", + "markdownDescription": "%config.characterProjectPath%" + }, + "okScriptToolkit.characterMasterFile": { + "type": "string", + "default": "assets/data/characters.json", + "description": "%config.characterMasterFile%" + }, + "okScriptToolkit.characterSkillsDirectory": { + "type": "string", + "default": "assets/data/character_skills", + "description": "%config.characterSkillsDirectory%" + }, + "okScriptToolkit.characterLocaleFile": { + "type": "string", + "default": "assets/lang/characters.json", + "description": "%config.characterLocaleFile%" + }, + "okScriptToolkit.characterAvatarTemplateRegex": { + "type": "string", + "default": "^battle[_-]?icon[_-]?", + "description": "%config.characterAvatarTemplateRegex%" + } } } - }, + ], "jsonValidation": [ { "fileMatch": "ok-script-toolkit.json", diff --git a/package.nls.es.json b/package.nls.es.json index 89f0dcde..62b9bc3e 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -3,12 +3,12 @@ "extension.description": "Autocompletado, sugerencias en línea, vista previa de plantillas, lanzamiento de tareas, herramientas de datos de personajes y más para proyectos ok-script.", "command.category": "ok-script Toolkit", "command.openGettingStarted": "Primeros pasos", - "command.showTemplates": "Abrir galería de plantillas", - "command.screenshotToTemplate": "Capturar en Recursos de Plantilla", - "command.openTemplatesEditor": "Abrir galería de plantillas en el editor", + "command.showTemplates": "Abrir vista previa de recursos", + "command.screenshotToTemplate": "Capturar y abrir gestión de anotaciones", + "command.openTemplatesEditor": "Abrir vista previa de recursos en el editor", "command.showTaskLauncher": "Abrir consola", "command.openCharacterManager": "Abrir gestor de personajes y habilidades", - "command.openTemplateAssets": "Abrir recursos de plantillas", + "command.openTemplateAssets": "Abrir gestión de anotaciones", "command.openResourcePublisher": "Abrir publicación de recursos", "command.openAnnotationEditor": "Abrir editor de anotaciones", "command.showTempScreenshots": "Abrir capturas temporales", @@ -25,22 +25,21 @@ "walkthrough.publish.title": "3. Publicar recursos", "walkthrough.publish.description": "[Abrir publicación de recursos](command:okScriptToolkit.openResourcePublisher), selecciona Template / Rect / Point y publica. Los recursos de posición pueden exportarse como JSON o Python.", "walkthrough.run.title": "4. Ejecutar y depurar", - "walkthrough.run.description": "[Abrir el ejecutor](command:okScriptToolkit.showTaskLauncher), elige una tarea, ajusta parámetros, ejecútala y consulta el estado y los registros.", + "walkthrough.run.description": "[Abrir la consola](command:okScriptToolkit.showTaskLauncher), elige una tarea, ajusta parámetros, ejecútala y consulta el estado y los registros.", "walkthrough.links.title": "Seguir explorando", "walkthrough.links.description": "[Documentación](https://github.com/AliceJump/ok-script-toolkit#readme) · [GitHub](https://github.com/AliceJump/ok-script-toolkit) · [Issues / Comentarios](https://github.com/AliceJump/ok-script-toolkit/issues) · [Notas de versión](https://github.com/AliceJump/ok-script-toolkit/releases)", "viewContainer.title": "Herramientas de ok-script", "viewContainer.tools.title": "Herramientas de ok-script", - "viewContainer.templates.title": "Plantillas de ok-script", + "viewContainer.templates.title": "Recursos de ok-script", "viewContainer.tempShots.title": "Capturas temporales de ok-script", - "view.templateGallery": "Galería de plantillas", + "view.templateGallery": "Vista previa de recursos", "view.console": "Consola", - "view.templateAssets": "Recursos de plantillas", + "view.templateAssets": "Gestión de anotaciones", "view.console.title": "Herramientas de ok-script: Consola", - "view.templateGallery.title": "Plantillas de ok-script: Galería de plantillas", - "view.templateAssets.title": "Plantillas de ok-script: Recursos de plantillas", + "view.templateGallery.title": "Recursos de ok-script: Vista previa de recursos", + "view.templateAssets.title": "Recursos de ok-script: Gestión de anotaciones", "view.tempScreenshots": "Capturas temporales", "view.tempScreenshots.title": "Capturas temporales de ok-script", - "configuration.title": "Sugerencias de idioma de ok-script", "config.langDirectory": "Directorio JSON de idiomas relativo a la raíz del espacio de trabajo.", "config.poDirectory": "Directorio gettext PO relativo a la raíz del espacio de trabajo.", "config.enablePoData": "Activa datos gettext PO para sugerencias OCR match.", @@ -51,17 +50,22 @@ "config.labelEnumPath": "Ruta del archivo LabelEnum relativa a la raíz del espacio de trabajo, con o sin .py. Sustituye labelEnum.path de ok-script-toolkit.json; déjalo vacío para seguir la convención del proyecto.", "config.labelEnumName": "Nombre de la clase LabelEnum. Sustituye labelEnum.name de ok-script-toolkit.json; déjalo vacío para seguir la convención del proyecto — solo se deriva del nombre del archivo cuando el proyecto tampoco la declara. El código del proyecto importa esta clase por su nombre, así que renombrarla rompe esos imports — se te pedirá confirmación antes de sobrescribir un archivo existente.", "config.effectsFile": "Archivo Python de EffectType y EFFECT_DESCRIPTIONS.", - "config.okScriptProjectPath": "Raíz del proyecto ok-script usada por el lanzador de tareas.", + "config.okScriptProjectPath": "Raíz del proyecto ok-script que contiene src/config.py. La usa la consola para ejecutar tareas.", "config.okScriptPython": "Intérprete de Python usado para ejecutar tareas ok-script.", "config.captureMethod": "Método de captura de la ventana del juego. auto prueba Windows Graphics Capture y recurre a BitBlt si falla. Usa foreground si la pantalla de inicio o de acceso sale en blanco: activa la ventana y lee la pantalla, lo que roba el foco.", "config.copyCoordsSpace": "Añade un espacio después de cada coma al copiar coordenadas (x, y, tox, toy). Es una preferencia personal: solo ámbito global (application), no se puede configurar por espacio de trabajo.", - "config.characterProjectPath": "Raíz del proyecto usada por el gestor de personajes y habilidades.", + "config.characterProjectPath": "Raíz del proyecto usada por el gestor de personajes y habilidades. Si está vacía, se usa okScriptProjectPath y el espacio de trabajo actual.", "config.characterMasterFile": "JSON maestro de personajes.", "config.characterSkillsDirectory": "Directorio JSON de habilidades de personajes.", "config.characterLocaleFile": "JSON de localización de nombres de personajes.", "config.characterAvatarTemplateRegex": "Expresión regular para nombres de plantillas de avatar. Usa el primer grupo si existe; en caso contrario compara el texto posterior al prefijo coincidente con el slug inglés. Admite por defecto battleicon, battle_icon y battle-icon.", - "config.okTemplatesDirectory": "Nombre de la carpeta ok_templates relativo a la raíz del espacio de trabajo. Usado por el gestor de recursos de plantillas.", + "config.okTemplatesDirectory": "Nombre de la carpeta ok_templates relativo a la raíz del espacio de trabajo. Lo usa la gestión de anotaciones.", "config.annotationKeybindings": "Atajos de teclado del editor de anotaciones. Cada valor es un nombre de tecla, opcionalmente con prefijos de modificador unidos por `+` (p. ej. `ctrl+z`, `shift+d`). Teclas admitidas: letras, dígitos, `Delete`, `ArrowLeft`, `ArrowRight`, etc.", "config.positionJsonPath": "Ruta relativa al proyecto para publicar Position JSON. Prioridad: preferencia personal del IDE → convención del proyecto en `ok-script-toolkit.json` → valor predeterminado.", - "config.positionPythonDirectory": "Directorio relativo al proyecto para generar `ScreenRatio.py` y `PositionMap.py`. Prioridad: preferencia personal del IDE → convención del proyecto en `ok-script-toolkit.json` → valor predeterminado." + "config.positionPythonDirectory": "Directorio relativo al proyecto para generar `ScreenRatio.py` y `PositionMap.py`. Prioridad: preferencia personal del IDE → convención del proyecto en `ok-script-toolkit.json` → valor predeterminado.", + "configuration.codeHints": "ok-script: Sugerencias de código", + "configuration.resources": "ok-script: Recursos y anotaciones", + "configuration.screenshots": "ok-script: Capturas", + "configuration.console": "ok-script: Consola y tareas", + "configuration.characters": "ok-script: Datos de personajes" } diff --git a/package.nls.ja.json b/package.nls.ja.json index 62ddaecf..42c5a67a 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -3,12 +3,12 @@ "extension.description": "ok-script プロジェクト向けの補完、インラインヒント、テンプレートプレビュー、タスク起動、キャラクターデータツールなど。", "command.category": "ok-script ツールキット", "command.openGettingStarted": "はじめに", - "command.showTemplates": "テンプレートギャラリーを開く", - "command.screenshotToTemplate": "スクリーンショットをテンプレート素材へ", - "command.openTemplatesEditor": "テンプレートギャラリーをエディターで開く", + "command.showTemplates": "リソースプレビューを開く", + "command.screenshotToTemplate": "スクリーンショットしてアノテーション管理を開く", + "command.openTemplatesEditor": "リソースプレビューをエディターで開く", "command.showTaskLauncher": "コンソールを開く", "command.openCharacterManager": "キャラクター・スキル管理を開く", - "command.openTemplateAssets": "テンプレートアセットを開く", + "command.openTemplateAssets": "アノテーション管理を開く", "command.openResourcePublisher": "リソース公開を開く", "command.openAnnotationEditor": "アノテーションエディターを開く", "command.showTempScreenshots": "一時スクリーンショットを開く", @@ -25,22 +25,21 @@ "walkthrough.publish.title": "3. リソースを公開する", "walkthrough.publish.description": "[リソース公開を開く](command:okScriptToolkit.openResourcePublisher)から Template / Rect / Point を選択して公開します。位置リソースは JSON または Python で出力できます。", "walkthrough.run.title": "4. 実行とデバッグ", - "walkthrough.run.description": "[ランナーを開く](command:okScriptToolkit.showTaskLauncher)からタスクを選び、パラメータを調整して起動し、実行状態とログを確認します。", + "walkthrough.run.description": "[コンソールを開く](command:okScriptToolkit.showTaskLauncher)からタスクを選び、パラメータを調整して起動し、実行状態とログを確認します。", "walkthrough.links.title": "さらに使う", "walkthrough.links.description": "[ドキュメント](https://github.com/AliceJump/ok-script-toolkit#readme) · [GitHub](https://github.com/AliceJump/ok-script-toolkit) · [Issue / フィードバック](https://github.com/AliceJump/ok-script-toolkit/issues) · [リリースノート](https://github.com/AliceJump/ok-script-toolkit/releases)", "viewContainer.title": "ok-script ツール", "viewContainer.tools.title": "ok-script ツール", - "viewContainer.templates.title": "ok-script テンプレート", + "viewContainer.templates.title": "ok-script リソース", "viewContainer.tempShots.title": "ok-script 一時スクリーンショット", - "view.templateGallery": "テンプレートギャラリー", + "view.templateGallery": "リソースプレビュー", "view.console": "コンソール", - "view.templateAssets": "テンプレートアセット", + "view.templateAssets": "アノテーション管理", "view.console.title": "ok-script ツール: コンソール", - "view.templateGallery.title": "ok-script テンプレート: テンプレートギャラリー", - "view.templateAssets.title": "ok-script テンプレート: テンプレートアセット", + "view.templateGallery.title": "ok-script リソース: リソースプレビュー", + "view.templateAssets.title": "ok-script リソース: アノテーション管理", "view.tempScreenshots": "一時スクリーンショット", "view.tempScreenshots.title": "ok-script 一時スクリーンショット", - "configuration.title": "ok-script 言語ヒント", "config.langDirectory": "ワークスペースルート相対の言語 JSON ディレクトリ。", "config.poDirectory": "ワークスペースルート相対の gettext PO ディレクトリ。", "config.enablePoData": "OCR match ヒント用の gettext PO データを有効にします。", @@ -51,17 +50,22 @@ "config.labelEnumPath": "ワークスペースルートからの LabelEnum ファイルパス(.py の有無はどちらでも可)。ok-script-toolkit.json の labelEnum.path を上書きします。空にするとプロジェクトの取り決めに従います。", "config.labelEnumName": "LabelEnum のクラス名。ok-script-toolkit.json の labelEnum.name を上書きします。空にするとプロジェクトの取り決めに従います — 取り決めも無い場合のみファイル名から導出します。プロジェクトのコードはこの名前で import するため、改名するとそれらが壊れます — 既存ファイルを上書きする前に確認します。", "config.effectsFile": "EffectType と EFFECT_DESCRIPTIONS を含む Python ファイル。", - "config.okScriptProjectPath": "タスク起動で使用する ok-script プロジェクトルート。", + "config.okScriptProjectPath": "コンソールでタスクを実行するときに使う、src/config.py を含む ok-script プロジェクトルート。", "config.okScriptPython": "ok-script タスク実行に使用する Python。", "config.captureMethod": "ゲームウィンドウのキャプチャ方式。auto は Windows Graphics Capture を試し、失敗したら BitBlt に戻します。ランチャーやログイン画面が空白になる場合は foreground を使ってください。ウィンドウを前面に出して画面を読み取るため、フォーカスが奪われます。", "config.copyCoordsSpace": "座標をコピーするときにカンマの後にスペースを入れます(x, y, tox, toy)。個人設定のため、グローバル(application スコープ)のみで、ワークスペース単位では設定できません。", - "config.characterProjectPath": "キャラクター・スキル管理で使用するプロジェクトルート。", + "config.characterProjectPath": "キャラクター・スキル管理で使用するプロジェクトルート。空の場合は okScriptProjectPath と現在のワークスペースを使います。", "config.characterMasterFile": "キャラクターマスター JSON。", "config.characterSkillsDirectory": "キャラクタースキル JSON ディレクトリ。", "config.characterLocaleFile": "キャラクター名ローカライズ JSON。", "config.characterAvatarTemplateRegex": "キャラクターアバターのテンプレート名正規表現。キャプチャグループがあれば最初のグループを、なければ一致した接頭辞より後の名前を英語 slug と照合します。既定では battleicon、battle_icon、battle-icon に対応します。", - "config.okTemplatesDirectory": "テンプレートアセットマネージャーで使用する、ワークスペースルート相対の ok_templates フォルダー名。", + "config.okTemplatesDirectory": "アノテーション管理で使用する、ワークスペースルート相対の ok_templates フォルダー名。", "config.annotationKeybindings": "アノテーションエディターのキーボードショートカット。各値はキー名で、`+` で修飾キーを接頭辞として付けられます(例: `ctrl+z`、`shift+d`)。サポートされるキー: 英字、数字、`Delete`、`ArrowLeft`、`ArrowRight` など。", "config.positionJsonPath": "Position JSON の出力先(プロジェクト相対パス)。優先順位:個人の IDE 設定 → `ok-script-toolkit.json` のプロジェクト設定 → 既定値。", - "config.positionPythonDirectory": "生成する `ScreenRatio.py` と `PositionMap.py` の出力ディレクトリ(プロジェクト相対)。優先順位:個人の IDE 設定 → `ok-script-toolkit.json` のプロジェクト設定 → 既定値。" + "config.positionPythonDirectory": "生成する `ScreenRatio.py` と `PositionMap.py` の出力ディレクトリ(プロジェクト相対)。優先順位:個人の IDE 設定 → `ok-script-toolkit.json` のプロジェクト設定 → 既定値。", + "configuration.codeHints": "ok-script: コードヒント", + "configuration.resources": "ok-script: リソースとアノテーション", + "configuration.screenshots": "ok-script: スクリーンショット", + "configuration.console": "ok-script: コンソールとタスク", + "configuration.characters": "ok-script: キャラクターデータ" } diff --git a/package.nls.json b/package.nls.json index 6ce7a099..411b6e91 100644 --- a/package.nls.json +++ b/package.nls.json @@ -25,7 +25,7 @@ "walkthrough.publish.title": "3. Publish resources", "walkthrough.publish.description": "[Open Resource Publishing](command:okScriptToolkit.openResourcePublisher), select Template / Rect / Point, then publish. Position resources can be exported as JSON or Python.", "walkthrough.run.title": "4. Run and debug", - "walkthrough.run.description": "[Open the runner](command:okScriptToolkit.showTaskLauncher), choose a task, adjust parameters, start it, and follow runtime status and logs.", + "walkthrough.run.description": "[Open the Console](command:okScriptToolkit.showTaskLauncher), choose a task, adjust parameters, start it, and follow runtime status and logs.", "walkthrough.links.title": "Keep exploring", "walkthrough.links.description": "[Documentation](https://github.com/AliceJump/ok-script-toolkit#readme) · [GitHub](https://github.com/AliceJump/ok-script-toolkit) · [Issues / Feedback](https://github.com/AliceJump/ok-script-toolkit/issues) · [Release Notes](https://github.com/AliceJump/ok-script-toolkit/releases)", "viewContainer.title": "ok-script Tools", @@ -40,7 +40,6 @@ "view.templateAssets.title": "ok-script Resources: Annotation Management", "view.tempScreenshots": "Temp Screenshots", "view.tempScreenshots.title": "ok-script Temp Shots", - "configuration.title": "ok-script Lang Hints", "config.langDirectory": "Language JSON directory relative to the workspace root.", "config.poDirectory": "gettext PO directory relative to the workspace root, using /LC_MESSAGES/*.po.", "config.enablePoData": "Enable gettext PO data for OCR match hints.", @@ -51,17 +50,22 @@ "config.labelEnumPath": "LabelEnum.py path relative to the workspace root, with or without the .py suffix. Overrides labelEnum.path in ok-script-toolkit.json; leave empty to follow the project convention.", "config.labelEnumName": "LabelEnum class name. Overrides labelEnum.name in ok-script-toolkit.json; leave empty to follow the project convention — falling back to the file name only when the project does not declare one. Project code imports this class by name, so renaming it breaks those imports — you will be asked to confirm before an existing file is overwritten.", "config.effectsFile": "EffectType and EFFECT_DESCRIPTIONS Python file relative to the workspace root.", - "config.okScriptProjectPath": "ok-script project root containing src/config.py. Used by the task launcher.", + "config.okScriptProjectPath": "ok-script project root containing src/config.py. Used by the Console to run tasks.", "config.okScriptPython": "Python interpreter used to run ok-script tasks. Empty uses the target project's .venv on Windows.", "config.captureMethod": "Game window capture method. auto tries Windows Graphics Capture and falls back to BitBlt. Use foreground for launcher or login screens that capture blank otherwise: it activates the window and reads the screen, which steals focus.", "config.copyCoordsSpace": "Add a space after each comma when copying box coordinates (x, y, tox, toy). A personal habit — application scope only, cannot be set per workspace.", - "config.characterProjectPath": "Project root used by the character and skill manager. Empty falls back to okScriptProjectPath and the current workspace.", + "config.characterProjectPath": "Project root used by the Character & Skill Manager. Empty falls back to okScriptProjectPath and the current workspace.", "config.characterMasterFile": "Character master JSON relative to the character project root.", "config.characterSkillsDirectory": "Character skill JSON directory relative to the character project root.", "config.characterLocaleFile": "Character name localization JSON relative to the character project root.", "config.characterAvatarTemplateRegex": "Regular expression for avatar template names. The first capture group is used when present; otherwise the text after the matched prefix is compared with the character's English slug. Defaults to battleicon/battle_icon/battle-icon prefixes.", - "config.okTemplatesDirectory": "ok_templates folder name relative to the workspace root. Used by annotation management.", + "config.okTemplatesDirectory": "ok_templates folder name relative to the workspace root. Used by Annotation Management.", "config.annotationKeybindings": "Keyboard shortcuts for the unified annotation editor, including template/box/point mode switching. Each value is a key name, optionally prefixed with modifier(s) joined by `+` (e.g. `ctrl+z`, `shift+d`). Supported keys: letters, digits, `Delete`, `ArrowLeft`, `ArrowRight`, etc.", "config.positionJsonPath": "Project-relative output path for Position JSON publishing. Precedence: your IDE preference → `ok-script-toolkit.json` project convention → built-in default.", - "config.positionPythonDirectory": "Project-relative directory for generated `ScreenRatio.py` and `PositionMap.py`. Precedence: your IDE preference → `ok-script-toolkit.json` project convention → built-in default." + "config.positionPythonDirectory": "Project-relative directory for generated `ScreenRatio.py` and `PositionMap.py`. Precedence: your IDE preference → `ok-script-toolkit.json` project convention → built-in default.", + "configuration.codeHints": "ok-script: Code Hints", + "configuration.resources": "ok-script: Resources & Annotation", + "configuration.screenshots": "ok-script: Screenshots", + "configuration.console": "ok-script: Console & Tasks", + "configuration.characters": "ok-script: Character Data" } diff --git a/package.nls.ko.json b/package.nls.ko.json index f6b07240..1011d357 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -3,12 +3,12 @@ "extension.description": "ok-script 프로젝트용 자동 완성, 인라인 힌트, 템플릿 미리보기, 작업 실행, 캐릭터 데이터 도구 등.", "command.category": "ok-script 툴킷", "command.openGettingStarted": "시작하기", - "command.showTemplates": "템플릿 갤러리 열기", - "command.screenshotToTemplate": "스크린샷을 템플릿 에셋으로", - "command.openTemplatesEditor": "편집기에서 템플릿 갤러리 열기", + "command.showTemplates": "리소스 미리보기 열기", + "command.screenshotToTemplate": "스크린샷 후 어노테이션 관리 열기", + "command.openTemplatesEditor": "편집기에서 리소스 미리보기 열기", "command.showTaskLauncher": "콘솔 열기", - "command.openCharacterManager": "캐릭터·스킬 관리자 열기", - "command.openTemplateAssets": "템플릿 에셋 열기", + "command.openCharacterManager": "캐릭터·스킬 관리 열기", + "command.openTemplateAssets": "어노테이션 관리 열기", "command.openResourcePublisher": "리소스 게시 열기", "command.openAnnotationEditor": "어노테이션 편집기 열기", "command.showTempScreenshots": "임시 스크린샷 열기", @@ -20,27 +20,26 @@ "walkthrough.project.title": "1. ok-script 프로젝트 불러오기", "walkthrough.project.description": "`config.py` 또는 `src/config.py`가 있는 작업 영역을 열거나 `okScriptToolkit.okScriptProjectPath`를 설정하세요. 지원 프로젝트가 감지되면 자동으로 완료됩니다.", "walkthrough.visual.title": "2. 시각 리소스 제작", - "walkthrough.visual.description": "[주석 관리 열기](command:okScriptToolkit.openTemplateAssets)에서 이미지를 캡처하거나 가져오고 하나의 편집기에서 Template, Rect, Point 주석을 만드세요.", + "walkthrough.visual.description": "[어노테이션 관리 열기](command:okScriptToolkit.openTemplateAssets)에서 이미지를 캡처하거나 가져오고 하나의 편집기에서 Template, Rect, Point 주석을 만드세요.", "walkthrough.visual.mediaAlt": "이미지에서 Template, Rect, Point 주석을 만들고 게시하는 워크플로", "walkthrough.publish.title": "3. 리소스 게시", "walkthrough.publish.description": "[리소스 게시 열기](command:okScriptToolkit.openResourcePublisher)에서 Template / Rect / Point를 선택해 게시하세요. 위치 리소스는 JSON 또는 Python으로 내보낼 수 있습니다.", "walkthrough.run.title": "4. 실행 및 디버깅", - "walkthrough.run.description": "[러너 열기](command:okScriptToolkit.showTaskLauncher)에서 작업을 선택하고 매개변수를 조정해 실행한 뒤 상태와 로그를 확인하세요.", + "walkthrough.run.description": "[콘솔 열기](command:okScriptToolkit.showTaskLauncher)에서 작업을 선택하고 매개변수를 조정해 실행한 뒤 상태와 로그를 확인하세요.", "walkthrough.links.title": "더 알아보기", "walkthrough.links.description": "[문서](https://github.com/AliceJump/ok-script-toolkit#readme) · [GitHub](https://github.com/AliceJump/ok-script-toolkit) · [Issue / 피드백](https://github.com/AliceJump/ok-script-toolkit/issues) · [릴리스 노트](https://github.com/AliceJump/ok-script-toolkit/releases)", "viewContainer.title": "ok-script 도구", "viewContainer.tools.title": "ok-script 도구", - "viewContainer.templates.title": "ok-script 템플릿", + "viewContainer.templates.title": "ok-script 리소스", "viewContainer.tempShots.title": "ok-script 임시 스크린샷", - "view.templateGallery": "템플릿 갤러리", + "view.templateGallery": "리소스 미리보기", "view.console": "콘솔", - "view.templateAssets": "템플릿 에셋", + "view.templateAssets": "어노테이션 관리", "view.console.title": "ok-script 도구: 콘솔", - "view.templateGallery.title": "ok-script 템플릿: 템플릿 갤러리", - "view.templateAssets.title": "ok-script 템플릿: 템플릿 에셋", + "view.templateGallery.title": "ok-script 리소스: 리소스 미리보기", + "view.templateAssets.title": "ok-script 리소스: 어노테이션 관리", "view.tempScreenshots": "임시 스크린샷", "view.tempScreenshots.title": "ok-script 임시 스크린샷", - "configuration.title": "ok-script 언어 힌트", "config.langDirectory": "작업 영역 루트 기준 언어 JSON 디렉터리입니다.", "config.poDirectory": "작업 영역 루트 기준 gettext PO 디렉터리입니다.", "config.enablePoData": "OCR match 힌트용 gettext PO 데이터를 활성화합니다.", @@ -51,17 +50,22 @@ "config.labelEnumPath": "워크스페이스 루트 기준 LabelEnum 파일 경로(.py 생략 가능). ok-script-toolkit.json 의 labelEnum.path 를 덮어씁니다. 비워 두면 프로젝트 규약을 따릅니다.", "config.labelEnumName": "LabelEnum 클래스 이름. ok-script-toolkit.json 의 labelEnum.name 을 덮어씁니다. 비워 두면 프로젝트 규약을 따릅니다 — 프로젝트에서도 선언하지 않았을 때만 파일 이름에서 유도합니다. 프로젝트 코드가 이 이름으로 import 하므로 이름을 바꾸면 해당 import 가 깨집니다 — 기존 파일을 덮어쓰기 전에 확인합니다.", "config.effectsFile": "EffectType 및 EFFECT_DESCRIPTIONS Python 파일입니다.", - "config.okScriptProjectPath": "작업 실행기에서 사용할 ok-script 프로젝트 루트입니다.", + "config.okScriptProjectPath": "콘솔에서 작업을 실행할 때 사용하는, src/config.py가 있는 ok-script 프로젝트 루트입니다.", "config.okScriptPython": "ok-script 작업 실행에 사용할 Python 인터프리터입니다.", "config.captureMethod": "게임 창 캡처 방식입니다. auto는 Windows Graphics Capture를 시도하고 실패하면 BitBlt로 대체합니다. 런처/로그인 화면이 비어 있게 찍힌다면 foreground를 사용하세요. 창을 전면으로 활성화한 뒤 화면을 읽으므로 포커스를 가져갑니다.", "config.copyCoordsSpace": "좌표를 복사할 때 쉼표 뒤에 공백을 추가합니다(x, y, tox, toy). 개인 취향에 해당하며 전역(application 범위) 설정으로만 가능하고 작업 영역별로는 설정할 수 없습니다.", - "config.characterProjectPath": "캐릭터·스킬 관리자가 사용할 프로젝트 루트입니다.", + "config.characterProjectPath": "캐릭터·스킬 관리에서 사용할 프로젝트 루트입니다. 비우면 okScriptProjectPath와 현재 작업 영역을 사용합니다.", "config.characterMasterFile": "캐릭터 마스터 JSON입니다.", "config.characterSkillsDirectory": "캐릭터 스킬 JSON 디렉터리입니다.", "config.characterLocaleFile": "캐릭터 이름 현지화 JSON입니다.", "config.characterAvatarTemplateRegex": "캐릭터 아바타 템플릿 이름 정규식입니다. 캡처 그룹이 있으면 첫 번째 그룹을, 없으면 일치한 접두사 뒤의 이름을 영문 slug와 대조합니다. 기본값은 battleicon, battle_icon, battle-icon 접두사를 지원합니다.", - "config.okTemplatesDirectory": "템플릿 에셋 관리자에서 사용하는 작업 영역 루트 기준 ok_templates 폴더 이름입니다.", + "config.okTemplatesDirectory": "어노테이션 관리에서 사용하는 작업 영역 루트 기준 ok_templates 폴더 이름입니다.", "config.annotationKeybindings": "어노테이션 편집기의 키보드 단축키입니다. 각 값은 키 이름이고 `+`로 연결된 수정 키 접두사를 붙일 수 있습니다(예: `ctrl+z`, `shift+d`). 지원되는 키: 알파벳, 숫자, `Delete`, `ArrowLeft`, `ArrowRight` 등.", "config.positionJsonPath": "Position JSON 출력의 프로젝트 상대 경로입니다. 우선순위: 개인 IDE 설정 → `ok-script-toolkit.json` 프로젝트 규칙 → 기본값.", - "config.positionPythonDirectory": "생성되는 `ScreenRatio.py` 및 `PositionMap.py`의 프로젝트 상대 디렉터리입니다. 우선순위: 개인 IDE 설정 → `ok-script-toolkit.json` 프로젝트 규칙 → 기본값." + "config.positionPythonDirectory": "생성되는 `ScreenRatio.py` 및 `PositionMap.py`의 프로젝트 상대 디렉터리입니다. 우선순위: 개인 IDE 설정 → `ok-script-toolkit.json` 프로젝트 규칙 → 기본값.", + "configuration.codeHints": "ok-script: 코드 힌트", + "configuration.resources": "ok-script: 리소스 및 어노테이션", + "configuration.screenshots": "ok-script: 스크린샷", + "configuration.console": "ok-script: 콘솔 및 작업", + "configuration.characters": "ok-script: 캐릭터 데이터" } diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index bd98e741..0017ab2c 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -7,7 +7,7 @@ "command.screenshotToTemplate": "截图并打开标注管理", "command.openTemplatesEditor": "在编辑器中打开资源预览", "command.showTaskLauncher": "打开控制台", - "command.openCharacterManager": "打开角色技能管理面板", + "command.openCharacterManager": "打开角色技能管理", "command.openTemplateAssets": "打开标注管理", "command.openResourcePublisher": "打开资源发布", "command.openAnnotationEditor": "打开标注编辑器", @@ -25,7 +25,7 @@ "walkthrough.publish.title": "3. 发布资源", "walkthrough.publish.description": "[打开资源发布](command:okScriptToolkit.openResourcePublisher),选择 Template / Rect / Point 后发布;位置资源可导出为 JSON 或 Python。", "walkthrough.run.title": "4. 运行和调试", - "walkthrough.run.description": "[打开运行器](command:okScriptToolkit.showTaskLauncher),选择任务、调整参数、启动,并查看运行状态与日志。", + "walkthrough.run.description": "[打开控制台](command:okScriptToolkit.showTaskLauncher),选择任务、调整参数、启动,并查看运行状态与日志。", "walkthrough.links.title": "长期入口", "walkthrough.links.description": "[文档](https://github.com/AliceJump/ok-script-toolkit#readme) · [GitHub](https://github.com/AliceJump/ok-script-toolkit) · [Issue / 反馈](https://github.com/AliceJump/ok-script-toolkit/issues) · [更新日志](https://github.com/AliceJump/ok-script-toolkit/releases)", "viewContainer.title": "ok-script 工具", @@ -40,7 +40,6 @@ "view.templateAssets.title": "ok-script 资源: 标注管理", "view.tempScreenshots": "临时截图", "view.tempScreenshots.title": "ok-script 临时截图", - "configuration.title": "ok-script 语言提示", "config.langDirectory": "lang JSON 目录(相对于工作区根目录)。", "config.poDirectory": "gettext PO 目录(相对于工作区根目录),按 /LC_MESSAGES/*.po 组织。", "config.enablePoData": "启用 gettext PO 数据源,用于 OCR match 提示。", @@ -51,11 +50,11 @@ "config.labelEnumPath": "LabelEnum 文件路径(相对于工作区根目录,带不带 .py 都可以)。覆盖 ok-script-toolkit.json 里的 labelEnum.path;留空则跟随项目约定。", "config.labelEnumName": "LabelEnum 类名。覆盖 ok-script-toolkit.json 里的 labelEnum.name;留空则跟随项目约定 —— 只有项目也没声明时才由文件名推导。项目代码按这个名字 import,改名会让那些 import 失效 —— 覆盖已有文件前会先请你确认。", "config.effectsFile": "相对于工作区根目录的 EffectType 与 EFFECT_DESCRIPTIONS Python 文件。", - "config.okScriptProjectPath": "包含 src/config.py 的 ok-script 项目根目录,供任务启动器使用。", + "config.okScriptProjectPath": "包含 src/config.py 的 ok-script 项目根目录,供控制台运行任务使用。", "config.okScriptPython": "运行 ok-script 任务的 Python 解释器;留空时 Windows 优先使用目标项目 .venv。", "config.captureMethod": "游戏窗口截图方式。auto 先尝试 Windows Graphics Capture,失败回退 BitBlt。若启动器/登录界面截出来是空白,改用 foreground:它会把窗口激活到前台再读取屏幕(会抢焦点)。", "config.copyCoordsSpace": "复制框选坐标时在逗号后加空格(x, y, tox, toy)。属个人习惯,仅全局设置(application 作用域),不能按工作区覆盖。", - "config.characterProjectPath": "角色技能管理器使用的项目根目录;留空时回退到 okScriptProjectPath 和当前工作区。", + "config.characterProjectPath": "角色技能管理使用的项目根目录;留空时回退到 okScriptProjectPath 和当前工作区。", "config.characterMasterFile": "角色主表 JSON(相对于角色项目根目录)。", "config.characterSkillsDirectory": "角色技能 JSON 目录(相对于角色项目根目录)。", "config.characterLocaleFile": "角色名称多语言 JSON(相对于角色项目根目录)。", @@ -63,5 +62,10 @@ "config.okTemplatesDirectory": "ok_templates 文件夹名(相对于工作区根目录),供标注管理使用。", "config.annotationKeybindings": "统一标注编辑器的键盘快捷键,包括模板/框/点模式切换。每个值为按键名称,可选地使用 `+` 连接修饰键前缀(如 `ctrl+z`、`shift+d`)。支持的键:字母、数字、`Delete`、`ArrowLeft`、`ArrowRight` 等。", "config.positionJsonPath": "Position JSON 发布的项目相对路径。优先级:个人 IDE 偏好 → `ok-script-toolkit.json` 项目约定 → 内置默认值。", - "config.positionPythonDirectory": "生成 `ScreenRatio.py` 和 `PositionMap.py` 的项目相对目录。优先级:个人 IDE 偏好 → `ok-script-toolkit.json` 项目约定 → 内置默认值。" + "config.positionPythonDirectory": "生成 `ScreenRatio.py` 和 `PositionMap.py` 的项目相对目录。优先级:个人 IDE 偏好 → `ok-script-toolkit.json` 项目约定 → 内置默认值。", + "configuration.codeHints": "ok-script:代码提示", + "configuration.resources": "ok-script:资源与标注", + "configuration.screenshots": "ok-script:截图", + "configuration.console": "ok-script:控制台与任务", + "configuration.characters": "ok-script:角色数据" } diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 9bac37c3..ad5c18a3 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -3,12 +3,12 @@ "extension.description": "為 ok-script 專案提供智慧補全、行內提示、模板預覽、任務啟動、角色資料工具等完整開發輔助。", "command.category": "ok-script 工具箱", "command.openGettingStarted": "入門指南", - "command.showTemplates": "開啟模板面板", - "command.screenshotToTemplate": "截圖並開啟模板素材", - "command.openTemplatesEditor": "在編輯器中開啟模板面板", + "command.showTemplates": "開啟資源預覽", + "command.screenshotToTemplate": "截圖並開啟標註管理", + "command.openTemplatesEditor": "在編輯器中開啟資源預覽", "command.showTaskLauncher": "開啟控制台", - "command.openCharacterManager": "開啟角色技能管理面板", - "command.openTemplateAssets": "開啟模板素材", + "command.openCharacterManager": "開啟角色技能管理", + "command.openTemplateAssets": "開啟標註管理", "command.openResourcePublisher": "開啟資源發布", "command.openAnnotationEditor": "開啟標註編輯器", "command.showTempScreenshots": "開啟臨時截圖", @@ -25,22 +25,21 @@ "walkthrough.publish.title": "3. 發布資源", "walkthrough.publish.description": "[開啟資源發布](command:okScriptToolkit.openResourcePublisher),選擇 Template / Rect / Point 後發布;位置資源可匯出為 JSON 或 Python。", "walkthrough.run.title": "4. 執行和除錯", - "walkthrough.run.description": "[開啟執行器](command:okScriptToolkit.showTaskLauncher),選擇任務、調整參數、啟動,並查看執行狀態與日誌。", + "walkthrough.run.description": "[開啟控制台](command:okScriptToolkit.showTaskLauncher),選擇任務、調整參數、啟動,並查看執行狀態與日誌。", "walkthrough.links.title": "長期入口", "walkthrough.links.description": "[文件](https://github.com/AliceJump/ok-script-toolkit#readme) · [GitHub](https://github.com/AliceJump/ok-script-toolkit) · [Issue / 回饋](https://github.com/AliceJump/ok-script-toolkit/issues) · [更新日誌](https://github.com/AliceJump/ok-script-toolkit/releases)", "viewContainer.title": "ok-script 工具", "viewContainer.tools.title": "ok-script 工具", - "viewContainer.templates.title": "ok-script 模板", + "viewContainer.templates.title": "ok-script 資源", "viewContainer.tempShots.title": "ok-script 臨時截圖", - "view.templateGallery": "模板面板", + "view.templateGallery": "資源預覽", "view.console": "控制台", - "view.templateAssets": "模板素材", + "view.templateAssets": "標註管理", "view.console.title": "ok-script 工具: 控制台", - "view.templateGallery.title": "ok-script 模板: 模板面板", - "view.templateAssets.title": "ok-script 模板: 模板素材", + "view.templateGallery.title": "ok-script 資源: 資源預覽", + "view.templateAssets.title": "ok-script 資源: 標註管理", "view.tempScreenshots": "臨時截圖", "view.tempScreenshots.title": "ok-script 臨時截圖", - "configuration.title": "ok-script 語言提示", "config.langDirectory": "lang JSON 目錄(相對於工作區根目錄)。", "config.poDirectory": "gettext PO 目錄(相對於工作區根目錄),按 /LC_MESSAGES/*.po 組織。", "config.enablePoData": "啟用 gettext PO 資料來源,用於 OCR match 提示。", @@ -51,17 +50,22 @@ "config.labelEnumPath": "LabelEnum 檔案路徑(相對於工作區根目錄,帶不帶 .py 都可以)。覆蓋 ok-script-toolkit.json 裡的 labelEnum.path;留空則跟隨專案約定。", "config.labelEnumName": "LabelEnum 類名。覆蓋 ok-script-toolkit.json 裡的 labelEnum.name;留空則跟隨專案約定 —— 只有專案也沒宣告時才由檔名推導。專案程式碼按這個名字 import,改名會讓那些 import 失效 —— 覆蓋既有檔案前會先請你確認。", "config.effectsFile": "相對於工作區根目錄的 EffectType 與 EFFECT_DESCRIPTIONS Python 檔案。", - "config.okScriptProjectPath": "包含 src/config.py 的 ok-script 專案根目錄,供任務啟動器使用。", + "config.okScriptProjectPath": "包含 src/config.py 的 ok-script 專案根目錄,供控制台執行任務使用。", "config.okScriptPython": "執行 ok-script 任務的 Python 直譯器;留空時 Windows 優先使用目標專案 .venv。", "config.captureMethod": "遊戲視窗截圖方式。auto 先嘗試 Windows Graphics Capture,失敗時退回 BitBlt。若啟動器/登入介面截出來是空白,改用 foreground:它會把視窗啟用到前景再讀取畫面(會搶走焦點)。", "config.copyCoordsSpace": "複製框選座標時在逗號後加空格(x, y, tox, toy)。屬個人習慣,僅全域設定(application 作用域),不能按工作區覆蓋。", - "config.characterProjectPath": "角色技能管理器使用的專案根目錄;留空時回退到 okScriptProjectPath 和目前工作區。", + "config.characterProjectPath": "角色技能管理使用的專案根目錄;留空時回退到 okScriptProjectPath 和目前工作區。", "config.characterMasterFile": "角色主表 JSON(相對於角色專案根目錄)。", "config.characterSkillsDirectory": "角色技能 JSON 目錄(相對於角色專案根目錄)。", "config.characterLocaleFile": "角色名稱多語言 JSON(相對於角色專案根目錄)。", "config.characterAvatarTemplateRegex": "角色頭像模板名稱正規表示式;有擷取群組時使用第一組,否則使用符合前綴後的剩餘名稱與角色英文 slug 比對;預設相容 battleicon、battle_icon 與 battle-icon 前綴。", - "config.okTemplatesDirectory": "ok_templates 資料夾名稱(相對於工作區根目錄),供模板素材管理器使用。", + "config.okTemplatesDirectory": "ok_templates 資料夾名稱(相對於工作區根目錄),供標註管理使用。", "config.annotationKeybindings": "標註編輯器的鍵盤快速鍵。每個值為按鍵名稱,可選擇性使用 `+` 連接修飾鍵前綴(如 `ctrl+z`、`shift+d`)。支援的鍵:字母、數字、`Delete`、`ArrowLeft`、`ArrowRight` 等。", "config.positionJsonPath": "Position JSON 發布的專案相對路徑。優先順序:個人 IDE 偏好 → `ok-script-toolkit.json` 專案約定 → 內建預設值。", - "config.positionPythonDirectory": "產生 `ScreenRatio.py` 和 `PositionMap.py` 的專案相對目錄。優先順序:個人 IDE 偏好 → `ok-script-toolkit.json` 專案約定 → 內建預設值。" + "config.positionPythonDirectory": "產生 `ScreenRatio.py` 和 `PositionMap.py` 的專案相對目錄。優先順序:個人 IDE 偏好 → `ok-script-toolkit.json` 專案約定 → 內建預設值。", + "configuration.codeHints": "ok-script:程式碼提示", + "configuration.resources": "ok-script:資源與標註", + "configuration.screenshots": "ok-script:截圖", + "configuration.console": "ok-script:控制台與任務", + "configuration.characters": "ok-script:角色資料" } diff --git a/scripts/test_annotation_mode_cycle.js b/scripts/test_annotation_mode_cycle.js index 8a9e7c78..e8c85f3d 100644 --- a/scripts/test_annotation_mode_cycle.js +++ b/scripts/test_annotation_mode_cycle.js @@ -96,7 +96,7 @@ press('q'); assert.strictEqual(activeMode(), 'rectModeBtn', 'mode switching must stay disabled while the edit dialog is open'); const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); -const defaults = pkg.contributes.configuration.properties['okScriptToolkit.annotationKeybindings'].default; +const defaults = Object.assign({}, ...[].concat(pkg.contributes.configuration).map((section) => section.properties))['okScriptToolkit.annotationKeybindings'].default; assert.strictEqual(defaults.cycleMode, 'm', 'default cycle shortcut should be M'); assert(!Object.prototype.hasOwnProperty.call(defaults, 'modeTemplate')); assert(!Object.prototype.hasOwnProperty.call(defaults, 'modeRect')); diff --git a/scripts/test_convention_sources.js b/scripts/test_convention_sources.js index da3633aa..cd31f867 100644 --- a/scripts/test_convention_sources.js +++ b/scripts/test_convention_sources.js @@ -172,7 +172,7 @@ const projectConfig = require(path.join(root, 'out', 'projectConfig.js')); // package.json 里的 default:被测逻辑必须**无视**它们(否则项目声明永远不生效) const pkg = require(path.join(root, 'package.json')); const PKG_DEFAULTS = {}; -for (const [fullKey, spec] of Object.entries(pkg.contributes.configuration.properties)) { +for (const [fullKey, spec] of Object.entries(Object.assign({}, ...[].concat(pkg.contributes.configuration).map((section) => section.properties)))) { PKG_DEFAULTS[fullKey.replace(/^okScriptToolkit\./, '')] = spec.default; } for (const [key, value] of Object.entries(PKG_DEFAULTS)) vscode.__test.defaults.set(key, value); diff --git a/src/consolePanel.ts b/src/consolePanel.ts index dc6c3dac..7d4c475f 100644 --- a/src/consolePanel.ts +++ b/src/consolePanel.ts @@ -1311,7 +1311,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { if (toolbox.game) { // 实际复用由 connect_game.py 写入的 configs/devices.json selected_hwnd 驱动, // 这里仅记录连接来源,便于确认执行器与工具箱操作的是同一个窗口。 - this.appendOutputLine(tr('Reusing game connection from toolbox: {title} (PID {pid})', { + this.appendOutputLine(tr('Reusing the Console game connection: {title} (PID {pid})', { title: toolbox.game.title || String(toolbox.game.hwnd), pid: toolbox.game.pid, })); diff --git a/src/tempScreenshotPanel.ts b/src/tempScreenshotPanel.ts index 806ebfe8..8ba457a3 100644 --- a/src/tempScreenshotPanel.ts +++ b/src/tempScreenshotPanel.ts @@ -270,11 +270,11 @@ class TempScreenshotController { if (!target) return false; const dst = this.assetData.importImageFile(target.filePath, true); if (!dst) { - this.notice('error', tr('Failed to add to template assets: {error}', { error: target.name })); + this.notice('error', tr('Failed to add to Annotation Management: {error}', { error: target.name })); return false; } repaintAllAssetGalleries(); - this.notice('info', tr('Added to template assets: {name}', { name: path.basename(dst) })); + this.notice('info', tr('Added to Annotation Management: {name}', { name: path.basename(dst) })); return true; } diff --git a/src/templateAssetPanel.ts b/src/templateAssetPanel.ts index 04eb77db..d80a10a1 100644 --- a/src/templateAssetPanel.ts +++ b/src/templateAssetPanel.ts @@ -811,10 +811,10 @@ class AssetGalleryController { } const dst = this.data.importImageFile(target.filePath, true); if (!dst) { - void vscode.window.showErrorMessage(tr('Failed to add to template assets: {error}', { error: target.name })); + void vscode.window.showErrorMessage(tr('Failed to add to Annotation Management: {error}', { error: target.name })); return; } - void vscode.window.showInformationMessage(tr('Added to template assets: {name}', { name: path.basename(dst) })); + void vscode.window.showInformationMessage(tr('Added to Annotation Management: {name}', { name: path.basename(dst) })); await this.update(); } @@ -875,7 +875,7 @@ export class TemplateAssetPanel { } const panel = vscode.window.createWebviewPanel( 'okScriptToolkitTemplateAssets', - tr('Template Assets'), + tr('Annotation Management'), vscode.ViewColumn.Beside, { enableScripts: true, From 35a872f8abb9aa58b4dbd7989f0736e02830bf05 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:15:03 +0800 Subject: [PATCH 04/29] fix(commands): openAnnotationEditor and openResourcePublisher do what they say MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - openAnnotationEditor accepts an image Uri or shows a QuickPick of images in Annotation Management, then opens the annotation editor - openResourcePublisher (now titled "Publish Resources…") runs the Template/Rect/Point publish flow instead of only opening the panel --- l10n/bundle.l10n.es.json | 8 ++++++-- l10n/bundle.l10n.ja.json | 8 ++++++-- l10n/bundle.l10n.json | 8 ++++++-- l10n/bundle.l10n.ko.json | 8 ++++++-- l10n/bundle.l10n.zh-cn.json | 8 ++++++-- l10n/bundle.l10n.zh-tw.json | 8 ++++++-- package.nls.es.json | 6 +++--- package.nls.ja.json | 6 +++--- package.nls.json | 6 +++--- package.nls.ko.json | 6 +++--- package.nls.zh-cn.json | 6 +++--- package.nls.zh-tw.json | 6 +++--- src/entrypoint.ts | 3 --- src/extension.ts | 38 +++++++++++++++++++++++++++++++++++-- src/templateAssetPanel.ts | 13 ++++++++++++- 15 files changed, 102 insertions(+), 36 deletions(-) diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 5c7fdb93..ce6894a8 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -193,7 +193,6 @@ "Not set — follows the project convention": "Sin configurar — sigue la convención del proyecto", "LabelEnum class name (leave empty to follow the project convention)": "Nombre de la clase LabelEnum (vacío para seguir la convención del proyecto)", "e.g. assets/data/LabelEnum.py or src/label_enum.py": "p. ej. assets/data/LabelEnum.py o src/label_enum.py", - "Please click an image in the Template Assets panel to open the annotation editor.": "Haz clic en una imagen del panel «Recursos de plantilla» para abrir el editor de anotaciones.", "The target file currently defines class {old}.": "El archivo de destino define actualmente la clase {old}.", "The target file already exists but its class name could not be recognized.": "El archivo de destino ya existe, pero no se pudo reconocer su nombre de clase.", "This save will rename it to {new}.": "Este guardado la renombrará a {new}.", @@ -259,5 +258,10 @@ "Annotation Management": "Gestión de anotaciones", "Added to Annotation Management: {name}": "Añadido a Gestión de anotaciones: {name}", "Failed to add to Annotation Management: {error}": "No se pudo añadir a Gestión de anotaciones: {error}", - "Reusing the Console game connection: {title} (PID {pid})": "Reutilizando la conexión del juego de la consola: {title} (PID {pid})" + "Reusing the Console game connection: {title} (PID {pid})": "Reutilizando la conexión del juego de la consola: {title} (PID {pid})", + "This image is not in Annotation Management yet. Add it there first.": "Esta imagen aún no está en Gestión de anotaciones. Añádela primero.", + "Open Annotation Management": "Abrir gestión de anotaciones", + "No images to annotate yet. Capture or import one in Annotation Management first.": "Aún no hay imágenes para anotar. Captura o importa una en Gestión de anotaciones.", + "{count} annotations": "{count} anotaciones", + "Select an image to annotate": "Selecciona una imagen para anotar" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 24b6a2a1..4ca6ddd3 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -193,7 +193,6 @@ "Not set — follows the project convention": "未設定 — プロジェクト規約に従います", "LabelEnum class name (leave empty to follow the project convention)": "LabelEnum クラス名(空欄ならプロジェクト規約に従います)", "e.g. assets/data/LabelEnum.py or src/label_enum.py": "例: assets/data/LabelEnum.py または src/label_enum.py", - "Please click an image in the Template Assets panel to open the annotation editor.": "アノテーションエディターを開くには、「テンプレートアセット」パネルで画像をクリックしてください。", "The target file currently defines class {old}.": "対象ファイルの現在のクラス名は {old} です。", "The target file already exists but its class name could not be recognized.": "対象ファイルは既に存在しますが、クラス名を判別できませんでした。", "This save will rename it to {new}.": "この保存で {new} に改名されます。", @@ -259,5 +258,10 @@ "Annotation Management": "アノテーション管理", "Added to Annotation Management: {name}": "アノテーション管理に追加しました: {name}", "Failed to add to Annotation Management: {error}": "アノテーション管理への追加に失敗: {error}", - "Reusing the Console game connection: {title} (PID {pid})": "コンソールのゲーム接続を再利用: {title}(PID {pid})" + "Reusing the Console game connection: {title} (PID {pid})": "コンソールのゲーム接続を再利用: {title}(PID {pid})", + "This image is not in Annotation Management yet. Add it there first.": "この画像はまだアノテーション管理にありません。先に追加してください。", + "Open Annotation Management": "アノテーション管理を開く", + "No images to annotate yet. Capture or import one in Annotation Management first.": "アノテーションできる画像がまだありません。先にアノテーション管理でキャプチャまたはインポートしてください。", + "{count} annotations": "{count} 件のアノテーション", + "Select an image to annotate": "アノテーションする画像を選択" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 9087b6a1..16efa04a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -133,7 +133,6 @@ "Source": "Source", "At runtime, fix_match_regex translates this pattern using ocr.po before calling re.compile.": "At runtime, fix_match_regex translates this pattern using ocr.po before calling re.compile.", "{count} keys": "{count} keys", - "Please click an image in the Template Assets panel to open the annotation editor.": "Please click an image in the Template Assets panel to open the annotation editor.", "LabelEnum.py file path (relative to workspace root, leave empty to skip)": "LabelEnum.py file path (relative to workspace root, leave empty to skip)", "Enum file path must be relative and stay within the workspace root.": "Enum file path must be relative and stay within the workspace root.", "Enum file path": "Enum file path", @@ -259,5 +258,10 @@ "Annotation Management": "Annotation Management", "Added to Annotation Management: {name}": "Added to Annotation Management: {name}", "Failed to add to Annotation Management: {error}": "Failed to add to Annotation Management: {error}", - "Reusing the Console game connection: {title} (PID {pid})": "Reusing the Console game connection: {title} (PID {pid})" + "Reusing the Console game connection: {title} (PID {pid})": "Reusing the Console game connection: {title} (PID {pid})", + "This image is not in Annotation Management yet. Add it there first.": "This image is not in Annotation Management yet. Add it there first.", + "Open Annotation Management": "Open Annotation Management", + "No images to annotate yet. Capture or import one in Annotation Management first.": "No images to annotate yet. Capture or import one in Annotation Management first.", + "{count} annotations": "{count} annotations", + "Select an image to annotate": "Select an image to annotate" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index bd91c150..0f84b524 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -193,7 +193,6 @@ "Not set — follows the project convention": "설정 안 됨 — 프로젝트 규약을 따름", "LabelEnum class name (leave empty to follow the project convention)": "LabelEnum 클래스 이름(비워 두면 프로젝트 규약을 따름)", "e.g. assets/data/LabelEnum.py or src/label_enum.py": "예: assets/data/LabelEnum.py 또는 src/label_enum.py", - "Please click an image in the Template Assets panel to open the annotation editor.": "주석 편집기를 열려면 \"템플릿 에셋\" 패널에서 이미지를 클릭하세요.", "The target file currently defines class {old}.": "대상 파일의 현재 클래스 이름은 {old} 입니다.", "The target file already exists but its class name could not be recognized.": "대상 파일이 이미 있지만 클래스 이름을 인식하지 못했습니다.", "This save will rename it to {new}.": "이번 저장으로 {new} 로 이름이 바뀝니다.", @@ -259,5 +258,10 @@ "Annotation Management": "어노테이션 관리", "Added to Annotation Management: {name}": "어노테이션 관리에 추가됨: {name}", "Failed to add to Annotation Management: {error}": "어노테이션 관리 추가 실패: {error}", - "Reusing the Console game connection: {title} (PID {pid})": "콘솔의 게임 연결 재사용: {title}(PID {pid})" + "Reusing the Console game connection: {title} (PID {pid})": "콘솔의 게임 연결 재사용: {title}(PID {pid})", + "This image is not in Annotation Management yet. Add it there first.": "이 이미지는 아직 어노테이션 관리에 없습니다. 먼저 추가하세요.", + "Open Annotation Management": "어노테이션 관리 열기", + "No images to annotate yet. Capture or import one in Annotation Management first.": "어노테이션할 이미지가 아직 없습니다. 먼저 어노테이션 관리에서 캡처하거나 가져오세요.", + "{count} annotations": "어노테이션 {count}개", + "Select an image to annotate": "어노테이션할 이미지 선택" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index d753f36a..e1e256eb 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -193,7 +193,6 @@ "Not set — follows the project convention": "未设置 —— 跟随项目约定", "LabelEnum class name (leave empty to follow the project convention)": "LabelEnum 类名(留空则跟随项目约定)", "e.g. assets/data/LabelEnum.py or src/label_enum.py": "例如 assets/data/LabelEnum.py 或 src/label_enum.py", - "Please click an image in the Template Assets panel to open the annotation editor.": "请先在「模板资源」面板中单击一张图片,以打开标注编辑器。", "The target file currently defines class {old}.": "目标文件当前的类名是 {old}。", "The target file already exists but its class name could not be recognized.": "目标文件已存在,但没能识别出它的类名。", "This save will rename it to {new}.": "这次保存会把它改名为 {new}。", @@ -259,5 +258,10 @@ "Annotation Management": "标注管理", "Added to Annotation Management: {name}": "已添加到标注管理:{name}", "Failed to add to Annotation Management: {error}": "添加到标注管理失败:{error}", - "Reusing the Console game connection: {title} (PID {pid})": "复用控制台的游戏连接:{title}(PID {pid})" + "Reusing the Console game connection: {title} (PID {pid})": "复用控制台的游戏连接:{title}(PID {pid})", + "This image is not in Annotation Management yet. Add it there first.": "这张图片还不在标注管理中,请先添加。", + "Open Annotation Management": "打开标注管理", + "No images to annotate yet. Capture or import one in Annotation Management first.": "还没有可标注的图片。请先在标注管理中截图或导入。", + "{count} annotations": "{count} 个标注", + "Select an image to annotate": "选择要标注的图片" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index ecdbfc1f..b873d0c1 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -193,7 +193,6 @@ "Not set — follows the project convention": "未設定 —— 跟隨專案約定", "LabelEnum class name (leave empty to follow the project convention)": "LabelEnum 類別名稱(留空則跟隨專案約定)", "e.g. assets/data/LabelEnum.py or src/label_enum.py": "例如 assets/data/LabelEnum.py 或 src/label_enum.py", - "Please click an image in the Template Assets panel to open the annotation editor.": "請先在「範本資源」面板中點選一張圖片,以開啟標註編輯器。", "The target file currently defines class {old}.": "目標檔案目前的類名是 {old}。", "The target file already exists but its class name could not be recognized.": "目標檔案已存在,但無法辨識它的類名。", "This save will rename it to {new}.": "這次儲存會將它改名為 {new}。", @@ -259,5 +258,10 @@ "Annotation Management": "標註管理", "Added to Annotation Management: {name}": "已加入標註管理:{name}", "Failed to add to Annotation Management: {error}": "加入標註管理失敗:{error}", - "Reusing the Console game connection: {title} (PID {pid})": "複用控制台的遊戲連線:{title}(PID {pid})" + "Reusing the Console game connection: {title} (PID {pid})": "複用控制台的遊戲連線:{title}(PID {pid})", + "This image is not in Annotation Management yet. Add it there first.": "這張圖片還不在標註管理中,請先加入。", + "Open Annotation Management": "開啟標註管理", + "No images to annotate yet. Capture or import one in Annotation Management first.": "還沒有可標註的圖片。請先在標註管理中截圖或匯入。", + "{count} annotations": "{count} 個標註", + "Select an image to annotate": "選擇要標註的圖片" } diff --git a/package.nls.es.json b/package.nls.es.json index 62b9bc3e..793f950e 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -9,8 +9,8 @@ "command.showTaskLauncher": "Abrir consola", "command.openCharacterManager": "Abrir gestor de personajes y habilidades", "command.openTemplateAssets": "Abrir gestión de anotaciones", - "command.openResourcePublisher": "Abrir publicación de recursos", - "command.openAnnotationEditor": "Abrir editor de anotaciones", + "command.openResourcePublisher": "Publicar recursos…", + "command.openAnnotationEditor": "Abrir editor de anotaciones…", "command.showTempScreenshots": "Abrir capturas temporales", "command.showConventionSources": "Convención del proyecto vs. mi configuración", "walkthrough.title": "Primeros pasos con ok-script Toolkit", @@ -23,7 +23,7 @@ "walkthrough.visual.description": "[Abrir gestión de anotaciones](command:okScriptToolkit.openTemplateAssets) para capturar o importar imágenes y crear anotaciones Template, Rect y Point en un único editor.", "walkthrough.visual.mediaAlt": "Flujo de imagen a anotaciones Template, Rect y Point y publicación", "walkthrough.publish.title": "3. Publicar recursos", - "walkthrough.publish.description": "[Abrir publicación de recursos](command:okScriptToolkit.openResourcePublisher), selecciona Template / Rect / Point y publica. Los recursos de posición pueden exportarse como JSON o Python.", + "walkthrough.publish.description": "[Publicar recursos](command:okScriptToolkit.openResourcePublisher): selecciona Template / Rect / Point y publica. Los recursos de posición pueden exportarse como JSON o Python.", "walkthrough.run.title": "4. Ejecutar y depurar", "walkthrough.run.description": "[Abrir la consola](command:okScriptToolkit.showTaskLauncher), elige una tarea, ajusta parámetros, ejecútala y consulta el estado y los registros.", "walkthrough.links.title": "Seguir explorando", diff --git a/package.nls.ja.json b/package.nls.ja.json index 42c5a67a..3ed89a77 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -9,8 +9,8 @@ "command.showTaskLauncher": "コンソールを開く", "command.openCharacterManager": "キャラクター・スキル管理を開く", "command.openTemplateAssets": "アノテーション管理を開く", - "command.openResourcePublisher": "リソース公開を開く", - "command.openAnnotationEditor": "アノテーションエディターを開く", + "command.openResourcePublisher": "リソースを公開…", + "command.openAnnotationEditor": "アノテーションエディターを開く…", "command.showTempScreenshots": "一時スクリーンショットを開く", "command.showConventionSources": "プロジェクト規約と自分の設定", "walkthrough.title": "ok-script ツールキットを始める", @@ -23,7 +23,7 @@ "walkthrough.visual.description": "[アノテーション管理を開く](command:okScriptToolkit.openTemplateAssets)と、画像のキャプチャやインポート、Template・Rect・Point の注釈を 1 つのエディターで行えます。", "walkthrough.visual.mediaAlt": "画像から Template、Rect、Point の注釈を作り公開するワークフロー", "walkthrough.publish.title": "3. リソースを公開する", - "walkthrough.publish.description": "[リソース公開を開く](command:okScriptToolkit.openResourcePublisher)から Template / Rect / Point を選択して公開します。位置リソースは JSON または Python で出力できます。", + "walkthrough.publish.description": "[リソースを公開](command:okScriptToolkit.openResourcePublisher):Template / Rect / Point を選択して公開します。位置リソースは JSON または Python で出力できます。", "walkthrough.run.title": "4. 実行とデバッグ", "walkthrough.run.description": "[コンソールを開く](command:okScriptToolkit.showTaskLauncher)からタスクを選び、パラメータを調整して起動し、実行状態とログを確認します。", "walkthrough.links.title": "さらに使う", diff --git a/package.nls.json b/package.nls.json index 411b6e91..c795527d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -9,8 +9,8 @@ "command.showTaskLauncher": "Open Console", "command.openCharacterManager": "Open Character & Skill Manager", "command.openTemplateAssets": "Open Annotation Management", - "command.openResourcePublisher": "Open Resource Publishing", - "command.openAnnotationEditor": "Open Annotation Editor", + "command.openResourcePublisher": "Publish Resources…", + "command.openAnnotationEditor": "Open Annotation Editor…", "command.showTempScreenshots": "Open Temp Screenshots", "command.showConventionSources": "Project Convention vs My Settings", "walkthrough.title": "Get Started with ok-script Toolkit", @@ -23,7 +23,7 @@ "walkthrough.visual.description": "[Open Annotation Management](command:okScriptToolkit.openTemplateAssets) to capture or import images and create Template, Rect, and Point annotations in one editor.", "walkthrough.visual.mediaAlt": "Image to Template, Rect, and Point annotation and publishing workflow", "walkthrough.publish.title": "3. Publish resources", - "walkthrough.publish.description": "[Open Resource Publishing](command:okScriptToolkit.openResourcePublisher), select Template / Rect / Point, then publish. Position resources can be exported as JSON or Python.", + "walkthrough.publish.description": "[Publish resources](command:okScriptToolkit.openResourcePublisher): select Template / Rect / Point, then publish. Position resources can be exported as JSON or Python.", "walkthrough.run.title": "4. Run and debug", "walkthrough.run.description": "[Open the Console](command:okScriptToolkit.showTaskLauncher), choose a task, adjust parameters, start it, and follow runtime status and logs.", "walkthrough.links.title": "Keep exploring", diff --git a/package.nls.ko.json b/package.nls.ko.json index 1011d357..5f2539fb 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -9,8 +9,8 @@ "command.showTaskLauncher": "콘솔 열기", "command.openCharacterManager": "캐릭터·스킬 관리 열기", "command.openTemplateAssets": "어노테이션 관리 열기", - "command.openResourcePublisher": "리소스 게시 열기", - "command.openAnnotationEditor": "어노테이션 편집기 열기", + "command.openResourcePublisher": "리소스 게시…", + "command.openAnnotationEditor": "어노테이션 편집기 열기…", "command.showTempScreenshots": "임시 스크린샷 열기", "command.showConventionSources": "프로젝트 규약과 내 설정", "walkthrough.title": "ok-script 툴킷 시작하기", @@ -23,7 +23,7 @@ "walkthrough.visual.description": "[어노테이션 관리 열기](command:okScriptToolkit.openTemplateAssets)에서 이미지를 캡처하거나 가져오고 하나의 편집기에서 Template, Rect, Point 주석을 만드세요.", "walkthrough.visual.mediaAlt": "이미지에서 Template, Rect, Point 주석을 만들고 게시하는 워크플로", "walkthrough.publish.title": "3. 리소스 게시", - "walkthrough.publish.description": "[리소스 게시 열기](command:okScriptToolkit.openResourcePublisher)에서 Template / Rect / Point를 선택해 게시하세요. 위치 리소스는 JSON 또는 Python으로 내보낼 수 있습니다.", + "walkthrough.publish.description": "[리소스 게시](command:okScriptToolkit.openResourcePublisher): Template / Rect / Point를 선택해 게시하세요. 위치 리소스는 JSON 또는 Python으로 내보낼 수 있습니다.", "walkthrough.run.title": "4. 실행 및 디버깅", "walkthrough.run.description": "[콘솔 열기](command:okScriptToolkit.showTaskLauncher)에서 작업을 선택하고 매개변수를 조정해 실행한 뒤 상태와 로그를 확인하세요.", "walkthrough.links.title": "더 알아보기", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 0017ab2c..0f29ce25 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -9,8 +9,8 @@ "command.showTaskLauncher": "打开控制台", "command.openCharacterManager": "打开角色技能管理", "command.openTemplateAssets": "打开标注管理", - "command.openResourcePublisher": "打开资源发布", - "command.openAnnotationEditor": "打开标注编辑器", + "command.openResourcePublisher": "发布资源…", + "command.openAnnotationEditor": "打开标注编辑器…", "command.showTempScreenshots": "打开临时截图", "command.showConventionSources": "项目约定 vs 我的设置", "walkthrough.title": "开始使用 ok-script 工具箱", @@ -23,7 +23,7 @@ "walkthrough.visual.description": "[打开标注管理](command:okScriptToolkit.openTemplateAssets),截图或导入图片,并在一个编辑器中创建 Template、Rect、Point 标注。", "walkthrough.visual.mediaAlt": "图片经过 Template、Rect、Point 标注后进入统一发布流程", "walkthrough.publish.title": "3. 发布资源", - "walkthrough.publish.description": "[打开资源发布](command:okScriptToolkit.openResourcePublisher),选择 Template / Rect / Point 后发布;位置资源可导出为 JSON 或 Python。", + "walkthrough.publish.description": "[发布资源](command:okScriptToolkit.openResourcePublisher):选择 Template / Rect / Point 后发布;位置资源可导出为 JSON 或 Python。", "walkthrough.run.title": "4. 运行和调试", "walkthrough.run.description": "[打开控制台](command:okScriptToolkit.showTaskLauncher),选择任务、调整参数、启动,并查看运行状态与日志。", "walkthrough.links.title": "长期入口", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index ad5c18a3..e4fc607b 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -9,8 +9,8 @@ "command.showTaskLauncher": "開啟控制台", "command.openCharacterManager": "開啟角色技能管理", "command.openTemplateAssets": "開啟標註管理", - "command.openResourcePublisher": "開啟資源發布", - "command.openAnnotationEditor": "開啟標註編輯器", + "command.openResourcePublisher": "發布資源…", + "command.openAnnotationEditor": "開啟標註編輯器…", "command.showTempScreenshots": "開啟臨時截圖", "command.showConventionSources": "專案約定 vs 我的設定", "walkthrough.title": "開始使用 ok-script 工具箱", @@ -23,7 +23,7 @@ "walkthrough.visual.description": "[開啟標註管理](command:okScriptToolkit.openTemplateAssets),截圖或匯入圖片,並在同一個編輯器中建立 Template、Rect、Point 標註。", "walkthrough.visual.mediaAlt": "圖片經過 Template、Rect、Point 標註後進入統一發布流程", "walkthrough.publish.title": "3. 發布資源", - "walkthrough.publish.description": "[開啟資源發布](command:okScriptToolkit.openResourcePublisher),選擇 Template / Rect / Point 後發布;位置資源可匯出為 JSON 或 Python。", + "walkthrough.publish.description": "[發布資源](command:okScriptToolkit.openResourcePublisher):選擇 Template / Rect / Point 後發布;位置資源可匯出為 JSON 或 Python。", "walkthrough.run.title": "4. 執行和除錯", "walkthrough.run.description": "[開啟控制台](command:okScriptToolkit.showTaskLauncher),選擇任務、調整參數、啟動,並查看執行狀態與日誌。", "walkthrough.links.title": "長期入口", diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 82a1cb4d..2d6db285 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -95,9 +95,6 @@ export function activate(context: vscode.ExtensionContext): void { const walkthrough = `${context.extension.id}#${WALKTHROUGH_ID}`; return vscode.commands.executeCommand('workbench.action.openWalkthrough', walkthrough); }), - vscode.commands.registerCommand('okScriptToolkit.openResourcePublisher', () => { - return vscode.commands.executeCommand('okScriptToolkit.openTemplateAssets'); - }), vscode.workspace.onDidChangeWorkspaceFolders(() => { bindConfigWatchers(); void updateProjectReady(); diff --git a/src/extension.ts b/src/extension.ts index 4bca420a..07325ef9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -34,6 +34,7 @@ import { import { TempScreenshotStore } from './tempScreenshotStore'; import { TempScreenshotViewProvider } from './tempScreenshotPanel'; import { notifyAnnotationDataChanged } from './cocoAnnotationData'; +import { AnnotationPanel } from './annotationPanel'; /** 缩略图缓存 key 版本:内容 hash 化后旧命名(t_/a_)需要清理一次 */ const THUMB_KEY_VERSION = 'content-hash-v2'; @@ -377,8 +378,41 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('okScriptToolkit.showTempScreenshots', () => { void vscode.commands.executeCommand(`${TempScreenshotViewProvider.viewType}.focus`); }), - vscode.commands.registerCommand('okScriptToolkit.openAnnotationEditor', () => { - void vscode.window.showInformationMessage(tr('Please click an image in the Template Assets panel to open the annotation editor.')); + vscode.commands.registerCommand('okScriptToolkit.openAnnotationEditor', async (target?: vscode.Uri | string) => { + // 资源管理器右键 / 带参数调用时直接打开;否则让用户从标注管理的图片里选一张。 + templateAssetData.load(); + const images = templateAssetData.listImages(); + const fromArg = typeof target === 'string' ? target : target?.fsPath; + let imagePath = fromArg && images.find((image) => path.resolve(image) === path.resolve(fromArg)); + if (fromArg && !imagePath) { + void vscode.window.showWarningMessage(tr('This image is not in Annotation Management yet. Add it there first.')); + return; + } + if (!imagePath) { + if (!images.length) { + const add = tr('Open Annotation Management'); + const choice = await vscode.window.showInformationMessage(tr('No images to annotate yet. Capture or import one in Annotation Management first.'), add); + if (choice === add) TemplateAssetPanel.show(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore); + return; + } + const picked = await vscode.window.showQuickPick( + images.map((image) => ({ + label: path.basename(image), + description: tr('{count} annotations', { count: templateAssetData.getAnnotationsForImage(image, true).length }), + detail: path.relative(targetRoot || path.dirname(image), image), + image, + })), + { placeHolder: tr('Select an image to annotate'), matchOnDetail: true }, + ); + imagePath = picked?.image; + } + if (!imagePath) return; + AnnotationPanel.show(context.extensionUri, templateAssetData, thumbDir, imagePath, images, () => { + repaintAllAssetGalleries(); + }); + }), + vscode.commands.registerCommand('okScriptToolkit.openResourcePublisher', () => { + return TemplateAssetPanel.publish(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore); }), vscode.commands.registerCommand('okScriptToolkit.showConventionSources', () => { showConventionSources(); diff --git a/src/templateAssetPanel.ts b/src/templateAssetPanel.ts index d80a10a1..b1c54d6e 100644 --- a/src/templateAssetPanel.ts +++ b/src/templateAssetPanel.ts @@ -310,7 +310,7 @@ class AssetGalleryController { /* ---------- 统一发布 ---------- */ - private async handlePublish(): Promise { + async handlePublish(): Promise { const folder = vscode.workspace.workspaceFolders?.[0]; if (!folder) { void vscode.window.showWarningMessage(tr('No workspace folder open.')); @@ -904,6 +904,17 @@ export class TemplateAssetPanel { void TemplateAssetPanel.current?.controller.handleScreenshot(); } + /** 「发布资源…」命令:打开标注管理(让发布结果可见)并直接进入发布选择流程。 */ + static async publish( + data: TemplateAssetData, + thumbDir: string, + extensionUri: vscode.Uri, + tempStore?: TempScreenshotStore, + ): Promise { + TemplateAssetPanel.show(data, thumbDir, extensionUri, tempStore); + await TemplateAssetPanel.current?.controller.handlePublish(); + } + private constructor( private readonly panel: vscode.WebviewPanel, readonly controller: AssetGalleryController, From 2cba67dcea9a1cd1c51cb30bd0321253bc7c2fa7 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:17:10 +0800 Subject: [PATCH 05/29] feat(commands): console commands, view title menus, executor status bar, badges - console.* commands (run task, start/pause/resume/stop, stop current task, show logs, refresh, connect/disconnect game) sharing the webview logic - menus: view/title actions per view, commandPalette when clauses driven by okScriptToolkit.projectReady / executorRunning / executorPaused / taskRunning - status bar item mirroring executor state; console badge for running+queued tasks; temp screenshot badge for unsorted shots - okScriptToolkit.hasCharacterData context key hides the character & skill manager in the palette, console title menu and Game segment when no data - isSupportedProject moved to projectConfig for reuse --- l10n/bundle.l10n.es.json | 17 ++- l10n/bundle.l10n.ja.json | 17 ++- l10n/bundle.l10n.json | 17 ++- l10n/bundle.l10n.ko.json | 17 ++- l10n/bundle.l10n.zh-cn.json | 17 ++- l10n/bundle.l10n.zh-tw.json | 17 ++- media/console/app.js | 4 + media/console/index.html | 2 +- package.json | 210 ++++++++++++++++++++++++++++++++++-- package.nls.es.json | 12 ++- package.nls.ja.json | 12 ++- package.nls.json | 12 ++- package.nls.ko.json | 12 ++- package.nls.zh-cn.json | 12 ++- package.nls.zh-tw.json | 12 ++- src/characterPanel.ts | 8 ++ src/consolePanel.ts | 119 +++++++++++++++++++- src/entrypoint.ts | 24 +---- src/extension.ts | 69 +++++++++++- src/projectConfig.ts | 20 ++++ src/tempScreenshotPanel.ts | 13 ++- 21 files changed, 593 insertions(+), 50 deletions(-) diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index ce6894a8..45b88bc3 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -263,5 +263,20 @@ "Open Annotation Management": "Abrir gestión de anotaciones", "No images to annotate yet. Capture or import one in Annotation Management first.": "Aún no hay imágenes para anotar. Captura o importa una en Gestión de anotaciones.", "{count} annotations": "{count} anotaciones", - "Select an image to annotate": "Selecciona una imagen para anotar" + "Select an image to annotate": "Selecciona una imagen para anotar", + "ok-script Executor": "Ejecutor de ok-script", + "ok-script: idle": "ok-script: inactivo", + "ok-script: starting…": "ok-script: iniciando…", + "ok-script: paused": "ok-script: en pausa", + "ok-script: ready": "ok-script: listo", + "Executor is not running. Click to open the Console.": "El ejecutor no está en marcha. Haz clic para abrir la consola.", + "Executor is starting. Click to open the Console.": "El ejecutor se está iniciando. Haz clic para abrir la consola.", + "Executor is paused. Click to open the Console.": "El ejecutor está en pausa. Haz clic para abrir la consola.", + "Running: {task} · queued: {count}. Click to open the Console.": "En ejecución: {task} · en cola: {count}. Haz clic para abrir la consola.", + "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.": "Ejecutor listo · {count} tarea(s) de activación habilitada(s). Haz clic para abrir la consola.", + "{count} task(s) running or queued": "{count} tarea(s) en ejecución o en cola", + "{count} temporary screenshot(s) waiting to be sorted": "{count} captura(s) temporal(es) por ordenar", + "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "Aún no se han cargado tareas únicas. Abre la consola y espera a que se cargue la lista.", + "Select a task to queue": "Selecciona una tarea para poner en cola", + "Select a task to run (the executor will start first)": "Selecciona una tarea para ejecutar (primero se iniciará el ejecutor)" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 4ca6ddd3..1c90bcc6 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -263,5 +263,20 @@ "Open Annotation Management": "アノテーション管理を開く", "No images to annotate yet. Capture or import one in Annotation Management first.": "アノテーションできる画像がまだありません。先にアノテーション管理でキャプチャまたはインポートしてください。", "{count} annotations": "{count} 件のアノテーション", - "Select an image to annotate": "アノテーションする画像を選択" + "Select an image to annotate": "アノテーションする画像を選択", + "ok-script Executor": "ok-script エグゼキューター", + "ok-script: idle": "ok-script: 待機中", + "ok-script: starting…": "ok-script: 起動中…", + "ok-script: paused": "ok-script: 一時停止中", + "ok-script: ready": "ok-script: 準備完了", + "Executor is not running. Click to open the Console.": "エグゼキューターは起動していません。クリックしてコンソールを開きます。", + "Executor is starting. Click to open the Console.": "エグゼキューターを起動しています。クリックしてコンソールを開きます。", + "Executor is paused. Click to open the Console.": "エグゼキューターは一時停止中です。クリックしてコンソールを開きます。", + "Running: {task} · queued: {count}. Click to open the Console.": "実行中: {task} · 待機: {count}。クリックしてコンソールを開きます。", + "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.": "エグゼキューター準備完了 · トリガータスク {count} 件有効。クリックしてコンソールを開きます。", + "{count} task(s) running or queued": "{count} 件のタスクが実行中または待機中", + "{count} temporary screenshot(s) waiting to be sorted": "未整理の一時スクリーンショット {count} 枚", + "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "一回限りのタスクがまだ読み込まれていません。コンソールを開いてタスク一覧の読み込みを待ってください。", + "Select a task to queue": "キューに追加するタスクを選択", + "Select a task to run (the executor will start first)": "実行するタスクを選択(先にエグゼキューターを起動します)" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 16efa04a..296bb8b6 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -263,5 +263,20 @@ "Open Annotation Management": "Open Annotation Management", "No images to annotate yet. Capture or import one in Annotation Management first.": "No images to annotate yet. Capture or import one in Annotation Management first.", "{count} annotations": "{count} annotations", - "Select an image to annotate": "Select an image to annotate" + "Select an image to annotate": "Select an image to annotate", + "ok-script Executor": "ok-script Executor", + "ok-script: idle": "ok-script: idle", + "ok-script: starting…": "ok-script: starting…", + "ok-script: paused": "ok-script: paused", + "ok-script: ready": "ok-script: ready", + "Executor is not running. Click to open the Console.": "Executor is not running. Click to open the Console.", + "Executor is starting. Click to open the Console.": "Executor is starting. Click to open the Console.", + "Executor is paused. Click to open the Console.": "Executor is paused. Click to open the Console.", + "Running: {task} · queued: {count}. Click to open the Console.": "Running: {task} · queued: {count}. Click to open the Console.", + "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.": "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.", + "{count} task(s) running or queued": "{count} task(s) running or queued", + "{count} temporary screenshot(s) waiting to be sorted": "{count} temporary screenshot(s) waiting to be sorted", + "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.", + "Select a task to queue": "Select a task to queue", + "Select a task to run (the executor will start first)": "Select a task to run (the executor will start first)" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 0f84b524..4a626ff4 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -263,5 +263,20 @@ "Open Annotation Management": "어노테이션 관리 열기", "No images to annotate yet. Capture or import one in Annotation Management first.": "어노테이션할 이미지가 아직 없습니다. 먼저 어노테이션 관리에서 캡처하거나 가져오세요.", "{count} annotations": "어노테이션 {count}개", - "Select an image to annotate": "어노테이션할 이미지 선택" + "Select an image to annotate": "어노테이션할 이미지 선택", + "ok-script Executor": "ok-script 실행기", + "ok-script: idle": "ok-script: 유휴", + "ok-script: starting…": "ok-script: 시작 중…", + "ok-script: paused": "ok-script: 일시 중지됨", + "ok-script: ready": "ok-script: 준비됨", + "Executor is not running. Click to open the Console.": "실행기가 실행 중이 아닙니다. 클릭하면 콘솔이 열립니다.", + "Executor is starting. Click to open the Console.": "실행기를 시작하는 중입니다. 클릭하면 콘솔이 열립니다.", + "Executor is paused. Click to open the Console.": "실행기가 일시 중지되었습니다. 클릭하면 콘솔이 열립니다.", + "Running: {task} · queued: {count}. Click to open the Console.": "실행 중: {task} · 대기: {count}. 클릭하면 콘솔이 열립니다.", + "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.": "실행기 준비됨 · 트리거 작업 {count}개 활성화. 클릭하면 콘솔이 열립니다.", + "{count} task(s) running or queued": "작업 {count}개 실행 또는 대기 중", + "{count} temporary screenshot(s) waiting to be sorted": "정리 대기 중인 임시 스크린샷 {count}개", + "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "일회성 작업이 아직 로드되지 않았습니다. 콘솔을 열고 작업 목록이 로드될 때까지 기다리세요.", + "Select a task to queue": "대기열에 추가할 작업 선택", + "Select a task to run (the executor will start first)": "실행할 작업 선택(먼저 실행기를 시작합니다)" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index e1e256eb..c8c3345f 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -263,5 +263,20 @@ "Open Annotation Management": "打开标注管理", "No images to annotate yet. Capture or import one in Annotation Management first.": "还没有可标注的图片。请先在标注管理中截图或导入。", "{count} annotations": "{count} 个标注", - "Select an image to annotate": "选择要标注的图片" + "Select an image to annotate": "选择要标注的图片", + "ok-script Executor": "ok-script 执行器", + "ok-script: idle": "ok-script:空闲", + "ok-script: starting…": "ok-script:启动中…", + "ok-script: paused": "ok-script:已暂停", + "ok-script: ready": "ok-script:就绪", + "Executor is not running. Click to open the Console.": "执行器未启动。点击打开控制台。", + "Executor is starting. Click to open the Console.": "执行器启动中。点击打开控制台。", + "Executor is paused. Click to open the Console.": "执行器已暂停。点击打开控制台。", + "Running: {task} · queued: {count}. Click to open the Console.": "运行中:{task} · 排队:{count}。点击打开控制台。", + "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.": "执行器就绪 · 已启用 {count} 个触发任务。点击打开控制台。", + "{count} task(s) running or queued": "{count} 个任务运行或排队中", + "{count} temporary screenshot(s) waiting to be sorted": "{count} 张临时截图待整理", + "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "尚未加载一次性任务。请打开控制台,等任务列表加载完成。", + "Select a task to queue": "选择要加入队列的任务", + "Select a task to run (the executor will start first)": "选择要运行的任务(将先启动执行器)" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index b873d0c1..fdff191a 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -263,5 +263,20 @@ "Open Annotation Management": "開啟標註管理", "No images to annotate yet. Capture or import one in Annotation Management first.": "還沒有可標註的圖片。請先在標註管理中截圖或匯入。", "{count} annotations": "{count} 個標註", - "Select an image to annotate": "選擇要標註的圖片" + "Select an image to annotate": "選擇要標註的圖片", + "ok-script Executor": "ok-script 執行器", + "ok-script: idle": "ok-script:閒置", + "ok-script: starting…": "ok-script:啟動中…", + "ok-script: paused": "ok-script:已暫停", + "ok-script: ready": "ok-script:就緒", + "Executor is not running. Click to open the Console.": "執行器未啟動。點擊開啟控制台。", + "Executor is starting. Click to open the Console.": "執行器啟動中。點擊開啟控制台。", + "Executor is paused. Click to open the Console.": "執行器已暫停。點擊開啟控制台。", + "Running: {task} · queued: {count}. Click to open the Console.": "執行中:{task} · 排隊:{count}。點擊開啟控制台。", + "Executor is ready · {count} trigger task(s) enabled. Click to open the Console.": "執行器就緒 · 已啟用 {count} 個觸發任務。點擊開啟控制台。", + "{count} task(s) running or queued": "{count} 個任務執行或排隊中", + "{count} temporary screenshot(s) waiting to be sorted": "{count} 張臨時截圖待整理", + "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "尚未載入一次性任務。請開啟控制台,等任務列表載入完成。", + "Select a task to queue": "選擇要加入佇列的任務", + "Select a task to run (the executor will start first)": "選擇要執行的任務(將先啟動執行器)" } diff --git a/media/console/app.js b/media/console/app.js index 9175c4af..5c09d84c 100644 --- a/media/console/app.js +++ b/media/console/app.js @@ -83,6 +83,10 @@ case 'connectDone': Console.onConnectDone(); break; + // 宿主能力:未检测到角色数据时隐藏「角色技能管理」入口 + case 'capabilities': + document.getElementById('openCharacterManager').hidden = message.characterData !== true; + break; case 'status': setStatus(message.level, message.text); break; diff --git a/media/console/index.html b/media/console/index.html index 8798c5e0..b9b195dd 100644 --- a/media/console/index.html +++ b/media/console/index.html @@ -131,7 +131,7 @@ - + diff --git a/package.json b/package.json index b40b781d..f18c9ee6 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ { "command": "okScriptToolkit.openTemplatesEditor", "title": "%command.openTemplatesEditor%", - "category": "%command.category%" + "category": "%command.category%", + "icon": "$(link-external)" }, { "command": "okScriptToolkit.showTaskLauncher", @@ -66,22 +67,26 @@ { "command": "okScriptToolkit.openCharacterManager", "title": "%command.openCharacterManager%", - "category": "%command.category%" + "category": "%command.category%", + "icon": "$(person)" }, { "command": "okScriptToolkit.openTemplateAssets", "title": "%command.openTemplateAssets%", - "category": "%command.category%" + "category": "%command.category%", + "icon": "$(link-external)" }, { "command": "okScriptToolkit.openResourcePublisher", "title": "%command.openResourcePublisher%", - "category": "%command.category%" + "category": "%command.category%", + "icon": "$(cloud-upload)" }, { "command": "okScriptToolkit.openAnnotationEditor", "title": "%command.openAnnotationEditor%", - "category": "%command.category%" + "category": "%command.category%", + "icon": "$(edit)" }, { "command": "okScriptToolkit.showTempScreenshots", @@ -91,12 +96,73 @@ { "command": "okScriptToolkit.screenshotToTemplate", "title": "%command.screenshotToTemplate%", - "category": "%command.category%" + "category": "%command.category%", + "icon": "$(device-camera)" }, { "command": "okScriptToolkit.showConventionSources", "title": "%command.showConventionSources%", "category": "%command.category%" + }, + { + "command": "okScriptToolkit.console.refresh", + "title": "%command.console.refresh%", + "category": "%command.category%", + "icon": "$(refresh)" + }, + { + "command": "okScriptToolkit.console.runTask", + "title": "%command.console.runTask%", + "category": "%command.category%", + "icon": "$(play)" + }, + { + "command": "okScriptToolkit.console.startExecutor", + "title": "%command.console.startExecutor%", + "category": "%command.category%", + "icon": "$(debug-start)" + }, + { + "command": "okScriptToolkit.console.pause", + "title": "%command.console.pause%", + "category": "%command.category%", + "icon": "$(debug-pause)" + }, + { + "command": "okScriptToolkit.console.resume", + "title": "%command.console.resume%", + "category": "%command.category%", + "icon": "$(debug-continue)" + }, + { + "command": "okScriptToolkit.console.stopCurrentTask", + "title": "%command.console.stopCurrentTask%", + "category": "%command.category%", + "icon": "$(debug-step-out)" + }, + { + "command": "okScriptToolkit.console.stopExecutor", + "title": "%command.console.stopExecutor%", + "category": "%command.category%", + "icon": "$(debug-stop)" + }, + { + "command": "okScriptToolkit.console.showLogs", + "title": "%command.console.showLogs%", + "category": "%command.category%", + "icon": "$(output)" + }, + { + "command": "okScriptToolkit.console.connectGame", + "title": "%command.console.connectGame%", + "category": "%command.category%", + "icon": "$(plug)" + }, + { + "command": "okScriptToolkit.console.disconnectGame", + "title": "%command.console.disconnectGame%", + "category": "%command.category%", + "icon": "$(debug-disconnect)" } ], "walkthroughs": [ @@ -458,7 +524,137 @@ "fileMatch": "ok-script-toolkit.json", "url": "./schemas/ok-script-toolkit.schema.json" } - ] + ], + "menus": { + "commandPalette": [ + { + "command": "okScriptToolkit.console.refresh", + "when": "okScriptToolkit.projectReady" + }, + { + "command": "okScriptToolkit.console.runTask", + "when": "okScriptToolkit.projectReady" + }, + { + "command": "okScriptToolkit.console.startExecutor", + "when": "okScriptToolkit.projectReady && !okScriptToolkit.executorRunning" + }, + { + "command": "okScriptToolkit.console.pause", + "when": "okScriptToolkit.executorRunning && !okScriptToolkit.executorPaused" + }, + { + "command": "okScriptToolkit.console.resume", + "when": "okScriptToolkit.executorPaused" + }, + { + "command": "okScriptToolkit.console.stopCurrentTask", + "when": "okScriptToolkit.taskRunning" + }, + { + "command": "okScriptToolkit.console.stopExecutor", + "when": "okScriptToolkit.executorRunning" + }, + { + "command": "okScriptToolkit.console.showLogs", + "when": "okScriptToolkit.projectReady" + }, + { + "command": "okScriptToolkit.console.connectGame", + "when": "okScriptToolkit.projectReady" + }, + { + "command": "okScriptToolkit.console.disconnectGame", + "when": "okScriptToolkit.projectReady" + }, + { + "command": "okScriptToolkit.openCharacterManager", + "when": "okScriptToolkit.hasCharacterData" + } + ], + "view/title": [ + { + "command": "okScriptToolkit.console.runTask", + "when": "view == okScriptToolkit.console", + "group": "navigation@1" + }, + { + "command": "okScriptToolkit.console.startExecutor", + "when": "view == okScriptToolkit.console && !okScriptToolkit.executorRunning", + "group": "navigation@2" + }, + { + "command": "okScriptToolkit.console.pause", + "when": "view == okScriptToolkit.console && okScriptToolkit.executorRunning && !okScriptToolkit.executorPaused", + "group": "navigation@2" + }, + { + "command": "okScriptToolkit.console.resume", + "when": "view == okScriptToolkit.console && okScriptToolkit.executorPaused", + "group": "navigation@2" + }, + { + "command": "okScriptToolkit.console.stopExecutor", + "when": "view == okScriptToolkit.console && okScriptToolkit.executorRunning", + "group": "navigation@3" + }, + { + "command": "okScriptToolkit.console.refresh", + "when": "view == okScriptToolkit.console", + "group": "navigation@4" + }, + { + "command": "okScriptToolkit.console.stopCurrentTask", + "when": "view == okScriptToolkit.console && okScriptToolkit.taskRunning", + "group": "1_run@1" + }, + { + "command": "okScriptToolkit.console.showLogs", + "when": "view == okScriptToolkit.console", + "group": "2_view@1" + }, + { + "command": "okScriptToolkit.openCharacterManager", + "when": "view == okScriptToolkit.console && okScriptToolkit.hasCharacterData", + "group": "2_view@2" + }, + { + "command": "okScriptToolkit.console.connectGame", + "when": "view == okScriptToolkit.console", + "group": "3_game@1" + }, + { + "command": "okScriptToolkit.console.disconnectGame", + "when": "view == okScriptToolkit.console", + "group": "3_game@2" + }, + { + "command": "okScriptToolkit.openTemplatesEditor", + "when": "view == okScriptToolkit.templateGallery", + "group": "navigation@1" + }, + { + "command": "okScriptToolkit.screenshotToTemplate", + "when": "view == okScriptToolkit.templateAssets", + "group": "navigation@1" + }, + { + "command": "okScriptToolkit.openAnnotationEditor", + "when": "view == okScriptToolkit.templateAssets", + "group": "navigation@2" + }, + { + "command": "okScriptToolkit.openResourcePublisher", + "when": "view == okScriptToolkit.templateAssets", + "group": "navigation@3" + }, + { + "command": "okScriptToolkit.openTemplateAssets", + "when": "view == okScriptToolkit.templateAssets", + "group": "navigation@4" + } + ] + } }, "scripts": { "compile": "tsc -p ./", diff --git a/package.nls.es.json b/package.nls.es.json index 793f950e..94e89522 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -67,5 +67,15 @@ "configuration.resources": "ok-script: Recursos y anotaciones", "configuration.screenshots": "ok-script: Capturas", "configuration.console": "ok-script: Consola y tareas", - "configuration.characters": "ok-script: Datos de personajes" + "configuration.characters": "ok-script: Datos de personajes", + "command.console.refresh": "Consola: actualizar tareas", + "command.console.runTask": "Consola: ejecutar tarea…", + "command.console.startExecutor": "Consola: iniciar ejecutor", + "command.console.pause": "Consola: pausar ejecutor", + "command.console.resume": "Consola: reanudar ejecutor", + "command.console.stopCurrentTask": "Consola: detener la tarea actual", + "command.console.stopExecutor": "Consola: detener ejecutor", + "command.console.showLogs": "Consola: mostrar registros", + "command.console.connectGame": "Consola: conectar juego", + "command.console.disconnectGame": "Consola: desconectar juego" } diff --git a/package.nls.ja.json b/package.nls.ja.json index 3ed89a77..1c3a054c 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -67,5 +67,15 @@ "configuration.resources": "ok-script: リソースとアノテーション", "configuration.screenshots": "ok-script: スクリーンショット", "configuration.console": "ok-script: コンソールとタスク", - "configuration.characters": "ok-script: キャラクターデータ" + "configuration.characters": "ok-script: キャラクターデータ", + "command.console.refresh": "コンソール: タスクを再読み込み", + "command.console.runTask": "コンソール: タスクを実行…", + "command.console.startExecutor": "コンソール: エグゼキューターを起動", + "command.console.pause": "コンソール: エグゼキューターを一時停止", + "command.console.resume": "コンソール: エグゼキューターを再開", + "command.console.stopCurrentTask": "コンソール: 現在のタスクを停止", + "command.console.stopExecutor": "コンソール: エグゼキューターを停止", + "command.console.showLogs": "コンソール: ログを表示", + "command.console.connectGame": "コンソール: ゲームに接続", + "command.console.disconnectGame": "コンソール: ゲームから切断" } diff --git a/package.nls.json b/package.nls.json index c795527d..3db709c7 100644 --- a/package.nls.json +++ b/package.nls.json @@ -67,5 +67,15 @@ "configuration.resources": "ok-script: Resources & Annotation", "configuration.screenshots": "ok-script: Screenshots", "configuration.console": "ok-script: Console & Tasks", - "configuration.characters": "ok-script: Character Data" + "configuration.characters": "ok-script: Character Data", + "command.console.refresh": "Console: Refresh Tasks", + "command.console.runTask": "Console: Run Task…", + "command.console.startExecutor": "Console: Start Executor", + "command.console.pause": "Console: Pause Executor", + "command.console.resume": "Console: Resume Executor", + "command.console.stopCurrentTask": "Console: Stop Current Task", + "command.console.stopExecutor": "Console: Stop Executor", + "command.console.showLogs": "Console: Show Logs", + "command.console.connectGame": "Console: Connect Game", + "command.console.disconnectGame": "Console: Disconnect Game" } diff --git a/package.nls.ko.json b/package.nls.ko.json index 5f2539fb..10bdfc6a 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -67,5 +67,15 @@ "configuration.resources": "ok-script: 리소스 및 어노테이션", "configuration.screenshots": "ok-script: 스크린샷", "configuration.console": "ok-script: 콘솔 및 작업", - "configuration.characters": "ok-script: 캐릭터 데이터" + "configuration.characters": "ok-script: 캐릭터 데이터", + "command.console.refresh": "콘솔: 작업 새로 고침", + "command.console.runTask": "콘솔: 작업 실행…", + "command.console.startExecutor": "콘솔: 실행기 시작", + "command.console.pause": "콘솔: 실행기 일시 중지", + "command.console.resume": "콘솔: 실행기 재개", + "command.console.stopCurrentTask": "콘솔: 현재 작업 중지", + "command.console.stopExecutor": "콘솔: 실행기 종료", + "command.console.showLogs": "콘솔: 로그 보기", + "command.console.connectGame": "콘솔: 게임 연결", + "command.console.disconnectGame": "콘솔: 게임 연결 해제" } diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 0f29ce25..cf3fd7dc 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -67,5 +67,15 @@ "configuration.resources": "ok-script:资源与标注", "configuration.screenshots": "ok-script:截图", "configuration.console": "ok-script:控制台与任务", - "configuration.characters": "ok-script:角色数据" + "configuration.characters": "ok-script:角色数据", + "command.console.refresh": "控制台:刷新任务", + "command.console.runTask": "控制台:运行任务…", + "command.console.startExecutor": "控制台:启动执行器", + "command.console.pause": "控制台:暂停执行器", + "command.console.resume": "控制台:继续执行器", + "command.console.stopCurrentTask": "控制台:停止当前任务", + "command.console.stopExecutor": "控制台:关闭执行器", + "command.console.showLogs": "控制台:查看日志", + "command.console.connectGame": "控制台:连接游戏", + "command.console.disconnectGame": "控制台:断开游戏" } diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index e4fc607b..d6f112f6 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -67,5 +67,15 @@ "configuration.resources": "ok-script:資源與標註", "configuration.screenshots": "ok-script:截圖", "configuration.console": "ok-script:控制台與任務", - "configuration.characters": "ok-script:角色資料" + "configuration.characters": "ok-script:角色資料", + "command.console.refresh": "控制台:重新整理任務", + "command.console.runTask": "控制台:執行任務…", + "command.console.startExecutor": "控制台:啟動執行器", + "command.console.pause": "控制台:暫停執行器", + "command.console.resume": "控制台:繼續執行器", + "command.console.stopCurrentTask": "控制台:停止目前任務", + "command.console.stopExecutor": "控制台:關閉執行器", + "command.console.showLogs": "控制台:查看日誌", + "command.console.connectGame": "控制台:連線遊戲", + "command.console.disconnectGame": "控制台:中斷遊戲連線" } diff --git a/src/characterPanel.ts b/src/characterPanel.ts index 233f333c..4cdbcda9 100644 --- a/src/characterPanel.ts +++ b/src/characterPanel.ts @@ -109,6 +109,14 @@ function configuredPaths(projectDir: string): CharacterDataPaths { }); } +/** 角色数据是否存在(主表或技能目录任一存在):决定是否露出角色技能管理入口 */ +export function detectCharacterData(): boolean { + const projectDir = resolveProjectDir(); + if (!projectDir || !fs.existsSync(projectDir)) return false; + const paths = configuredPaths(projectDir); + return fs.existsSync(paths.masterFile) || fs.existsSync(paths.skillsDir); +} + function normalizedAvatarKey(value: string | undefined): string { return (value || '').trim().toLowerCase().replace(/[\s_-]+/g, ''); } diff --git a/src/consolePanel.ts b/src/consolePanel.ts index 7d4c475f..a6faaeed 100644 --- a/src/consolePanel.ts +++ b/src/consolePanel.ts @@ -385,9 +385,27 @@ export function formatConsoleOutputLine(line: string, now = new Date()): string * 触发任务;一次性任务以入队方式交给同一个执行器跑一次。 * 因此本类不再有「当前任务 / 单进程」语义,改为维护执行器会话与启用集合。 */ +/** 宿主侧执行器状态摘要:供状态栏、视图徽标和 context key 使用 */ +export interface ExecutorStateSummary { + status: 'idle' | 'connecting' | 'running'; + paused: boolean; + /** 当前任务显示名(空闲为空) */ + current: string; + /** 一次性任务等待数 */ + queued: number; + /** 已启用的触发任务数 */ + triggers: number; +} + export class ConsoleViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'okScriptToolkit.console'; + /** 是否检测到角色数据(无则隐藏「角色技能管理」入口) */ + private characterDataAvailable = false; + private readonly executorStateEmitter = new vscode.EventEmitter(); + /** 执行器状态翻转(启动/就绪/暂停/任务切换/退出)时触发 */ + readonly onDidChangeExecutorState = this.executorStateEmitter.event; + private readonly output: vscode.OutputChannel; private configModule = 'src.config'; private view: vscode.WebviewView | null = null; @@ -479,6 +497,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { // webview 重建后同步执行器状态,避免切换侧边栏再回来时状态丢失 this.pushExecutorState(view); this.pushGameState(); + this.pushCapabilities(); break; case 'refresh': await this.refreshTasks(view); @@ -1241,19 +1260,19 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { } /** 显式启动执行器(状态条按钮):环境不满足时会在 UI 提示 */ - private async startExecutor(view: vscode.WebviewView): Promise { + private async startExecutor(view?: vscode.WebviewView): Promise { if (!(await this.ensureExecutor(view))) return; this.pushExecutorState(view); } /** 一次性任务入队:由常驻执行器执行一次后自动出队 */ - private async enqueueOnetime(view: vscode.WebviewView, task: TaskInfo): Promise { + private async enqueueOnetime(view: vscode.WebviewView | undefined, task: TaskInfo): Promise { if (!(await this.ensureExecutor(view))) return; this.writeCommand(`onetime_enqueue ${this.taskKey(task)}`); } /** 确保执行器已启动;返回 false 表示环境不满足(已在 UI 提示) */ - private async ensureExecutor(view: vscode.WebviewView): Promise { + private async ensureExecutor(view?: vscode.WebviewView): Promise { const { projectDir, pythonPath } = this.getConfig(); if (this.executor) { if (projectDir === this.executorProjectDir && projectDir === this.currentProjectDir) return true; @@ -1510,8 +1529,14 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { /** 把执行器状态推给 webview */ private pushExecutorState(view?: vscode.WebviewView): void { + const summary = this.executorState(); + this.executorStateEmitter.fire(summary); const target = view ?? this.view; if (!target) return; + const active = (summary.current ? 1 : 0) + summary.queued; + target.badge = active > 0 + ? { value: active, tooltip: tr('{count} task(s) running or queued', { count: active }) } + : undefined; const projectMismatch = this.executor !== null && !this.isCurrentProjectExecutor(); void target.webview.postMessage({ type: 'executor', @@ -1538,6 +1563,93 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { }); } + // ── 命令面板 / 视图标题栏 / 状态栏入口(与 webview 按钮走同一套逻辑)── + + executorState(): ExecutorStateSummary { + const own = this.executor !== null && this.isCurrentProjectExecutor(); + const currentKey = own ? (this.snapshot.current || '') : ''; + return { + status: this.executor ? (this.connecting ? 'connecting' : 'running') : 'idle', + paused: own && this.snapshot.paused === true, + current: currentKey ? this.taskLabel(currentKey) : '', + queued: own ? (this.snapshot.onetimeQueue || []).length : 0, + triggers: this.enabledTriggers.size, + }; + } + + private taskLabel(key: string): string { + return this.knownTasks.find((task) => this.taskKey(task) === key)?.displayName || key.split('::').pop() || key; + } + + async commandStartExecutor(): Promise { + await this.startExecutor(this.view ?? undefined); + } + + commandPause(): void { + if (this.requireOwnExecutor()) this.writeCommand('pause'); + } + + commandResume(): void { + if (this.requireOwnExecutor()) this.writeCommand('resume'); + } + + commandStopCurrent(): void { + if (this.requireOwnExecutor()) this.writeCommand('task_disable'); + } + + commandStopExecutor(): void { + this.stopExecutor(); + } + + setCharacterDataAvailable(available: boolean): void { + if (this.characterDataAvailable === available) return; + this.characterDataAvailable = available; + this.pushCapabilities(); + } + + private pushCapabilities(): void { + void this.view?.webview.postMessage({ type: 'capabilities', characterData: this.characterDataAvailable }); + } + + showLogs(): void { + this.output.show(true); + } + + /** 刷新任务列表;视图尚未打开时先聚焦(视图 ready 时会自行刷新) */ + async commandRefresh(): Promise { + if (this.view) { + await this.refreshTasks(this.view); + return; + } + await vscode.commands.executeCommand(`${ConsoleViewProvider.viewType}.focus`); + } + + /** 「运行任务…」:从已加载的一次性任务里选一个入队(与卡片「启动」等价,必要时会启动执行器) */ + async commandRunTask(): Promise { + const tasks = this.knownTasks.filter((task) => task.kind !== 'trigger'); + if (!tasks.length) { + await vscode.commands.executeCommand(`${ConsoleViewProvider.viewType}.focus`); + void vscode.window.showInformationMessage(tr('No one-time tasks are loaded yet. Open the Console and wait for the task list to load.')); + return; + } + const picked = await vscode.window.showQuickPick( + tasks.map((task) => ({ label: task.displayName, description: task.className, detail: task.module, task })), + { + placeHolder: this.executor + ? tr('Select a task to queue') + : tr('Select a task to run (the executor will start first)'), + matchOnDescription: true, + }, + ); + if (picked) await this.enqueueOnetime(this.view ?? undefined, picked.task); + } + + private requireOwnExecutor(): boolean { + if (this.isCurrentProjectExecutor()) return true; + void vscode.window.showWarningMessage(tr('The executor is not running.')); + return false; + } + private setStatus(level: 'ok' | 'warn' | 'error' | 'info', text: string): void { void this.view?.webview.postMessage({ type: 'status', level, text }); } @@ -1648,6 +1760,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { this.toolboxSubscription?.dispose(); this.toolboxSubscription = undefined; this.gameConnect.detachUi(); + this.executorStateEmitter.dispose(); this.output.dispose(); } diff --git a/src/entrypoint.ts b/src/entrypoint.ts index 2d6db285..c7c2fd0a 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -1,36 +1,14 @@ -import * as fs from 'fs'; -import * as path from 'path'; import * as vscode from 'vscode'; import { captureTemplateScreenshotInBackground } from './backgroundScreenshot'; import { activate as activateCore, deactivate as deactivateCore } from './extension'; import { GlobalScreenshotHotkey } from './globalScreenshotHotkey'; -import { resolveProjectDir } from './projectConfig'; +import { isSupportedProject, resolveProjectDir } from './projectConfig'; import { getProjectConfig } from './screenshotCapture'; import { repaintAllAssetGalleries } from './templateAssetPanel'; const WALKTHROUGH_ID = 'okScriptToolkit.getStarted'; const PROJECT_READY_CONTEXT = 'okScriptToolkit.projectReady'; -function isSupportedProject(projectDir: string): boolean { - if (!projectDir) return false; - try { - if (!fs.statSync(projectDir).isDirectory()) return false; - } catch { - return false; - } - - return [ - path.join(projectDir, 'config.py'), - path.join(projectDir, 'src', 'config.py'), - ].some((file) => { - try { - return fs.statSync(file).isFile(); - } catch { - return false; - } - }); -} - export function activate(context: vscode.ExtensionContext): void { activateCore(context); diff --git a/src/extension.ts b/src/extension.ts index 07325ef9..ce4953a2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import { setCaptureLogger, disposeCaptureWorker } from './screenshotCapture'; import { LangData, poDirectorySetting } from './langData'; import { tr } from './localization'; import { cocoFeatureRelPaths, refreshCocoFeaturePath } from './cocoFeaturePath'; -import { effectsFileSetting, i18nLangDirectorySetting, resolveProjectDir, templatesDirectory } from './projectConfig'; +import { effectsFileSetting, i18nLangDirectorySetting, isSupportedProject, resolveProjectDir, templatesDirectory } from './projectConfig'; import { showConventionSources } from './conventionSources'; import { FeatureData } from './featureData'; import { EffectData } from './effectData'; @@ -22,8 +22,8 @@ import { TemplateGalleryViewProvider, repaintAllGalleries, } from './templatePanel'; -import { ConsoleViewProvider } from './consolePanel'; -import { CharacterManagerPanel } from './characterPanel'; +import { ConsoleViewProvider, ExecutorStateSummary } from './consolePanel'; +import { CharacterManagerPanel, detectCharacterData } from './characterPanel'; import { GameConnectService } from './toolboxConnect'; import { TemplateAssetData } from './templateAssetData'; import { @@ -301,6 +301,59 @@ export function activate(context: vscode.ExtensionContext): void { } context.subscriptions.push(gameConnect, okConsole); + // ── 执行器状态 → 状态栏 + context key(视图标题栏 / 命令面板按状态显隐)── + const executorStatusItem = vscode.window.createStatusBarItem('okScriptToolkit.executor', vscode.StatusBarAlignment.Left, 50); + executorStatusItem.name = tr('ok-script Executor'); + executorStatusItem.command = 'okScriptToolkit.showTaskLauncher'; + const renderExecutorStatus = (state: ExecutorStateSummary) => { + void vscode.commands.executeCommand('setContext', 'okScriptToolkit.executorRunning', state.status !== 'idle'); + void vscode.commands.executeCommand('setContext', 'okScriptToolkit.executorPaused', state.paused); + void vscode.commands.executeCommand('setContext', 'okScriptToolkit.taskRunning', Boolean(state.current)); + if (state.status === 'idle') { + executorStatusItem.text = `$(debug-stop) ${tr('ok-script: idle')}`; + executorStatusItem.tooltip = tr('Executor is not running. Click to open the Console.'); + } else if (state.status === 'connecting') { + executorStatusItem.text = `$(loading~spin) ${tr('ok-script: starting…')}`; + executorStatusItem.tooltip = tr('Executor is starting. Click to open the Console.'); + } else if (state.paused) { + executorStatusItem.text = `$(debug-pause) ${tr('ok-script: paused')}`; + executorStatusItem.tooltip = tr('Executor is paused. Click to open the Console.'); + } else if (state.current) { + const queued = state.queued ? ` +${state.queued}` : ''; + executorStatusItem.text = `$(sync~spin) ${state.current}${queued}`; + executorStatusItem.tooltip = tr('Running: {task} · queued: {count}. Click to open the Console.', { task: state.current, count: state.queued }); + } else { + executorStatusItem.text = `$(pass) ${tr('ok-script: ready')}`; + executorStatusItem.tooltip = tr('Executor is ready · {count} trigger task(s) enabled. Click to open the Console.', { count: state.triggers }); + } + // 空闲且当前工作区不是 ok-script 项目时不占状态栏 + if (state.status === 'idle' && !isSupportedProject(resolveProjectDir())) executorStatusItem.hide(); + else executorStatusItem.show(); + }; + renderExecutorStatus(okConsole.executorState()); + context.subscriptions.push(executorStatusItem, okConsole.onDidChangeExecutorState(renderExecutorStatus)); + + // ── 角色数据检测 → 隐藏/露出「角色技能管理」入口 ── + const updateCharacterDataContext = () => { + const available = detectCharacterData(); + void vscode.commands.executeCommand('setContext', 'okScriptToolkit.hasCharacterData', available); + okConsole.setCharacterDataAvailable(available); + }; + updateCharacterDataContext(); + const characterDataWatcher = vscode.workspace.createFileSystemWatcher('**/assets/data/{characters.json,character_skills}'); + context.subscriptions.push( + characterDataWatcher, + characterDataWatcher.onDidCreate(updateCharacterDataContext), + characterDataWatcher.onDidDelete(updateCharacterDataContext), + vscode.workspace.onDidChangeWorkspaceFolders(updateCharacterDataContext), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('okScriptToolkit')) { + updateCharacterDataContext(); + renderExecutorStatus(okConsole.executorState()); + } + }), + ); + context.subscriptions.push( vscode.languages.registerInlayHintsProvider( { language: 'python', scheme: 'file' }, @@ -369,6 +422,16 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('okScriptToolkit.openCharacterManager', () => { CharacterManagerPanel.show(characterManagerDependencies); }), + vscode.commands.registerCommand('okScriptToolkit.console.refresh', () => okConsole.commandRefresh()), + vscode.commands.registerCommand('okScriptToolkit.console.runTask', () => okConsole.commandRunTask()), + vscode.commands.registerCommand('okScriptToolkit.console.startExecutor', () => okConsole.commandStartExecutor()), + vscode.commands.registerCommand('okScriptToolkit.console.pause', () => okConsole.commandPause()), + vscode.commands.registerCommand('okScriptToolkit.console.resume', () => okConsole.commandResume()), + vscode.commands.registerCommand('okScriptToolkit.console.stopCurrentTask', () => okConsole.commandStopCurrent()), + vscode.commands.registerCommand('okScriptToolkit.console.stopExecutor', () => okConsole.commandStopExecutor()), + vscode.commands.registerCommand('okScriptToolkit.console.showLogs', () => okConsole.showLogs()), + vscode.commands.registerCommand('okScriptToolkit.console.connectGame', () => gameConnect.connect()), + vscode.commands.registerCommand('okScriptToolkit.console.disconnectGame', () => gameConnect.disconnect()), vscode.commands.registerCommand('okScriptToolkit.openTemplateAssets', () => { TemplateAssetPanel.show(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore); }), diff --git a/src/projectConfig.ts b/src/projectConfig.ts index 5cd746de..45d24fc2 100644 --- a/src/projectConfig.ts +++ b/src/projectConfig.ts @@ -390,3 +390,23 @@ export function labelEnumClassName(filePath: string, projectDir?: string, scope? export function templatesCocoAnnotationsSetting(projectDir?: string): string | undefined { return normalizeRelPath(templatesOf(loadProjectConfig(projectDir)).cocoAnnotations); } + +/** 目录内存在 config.py 或 src/config.py 即视为可运行的 ok-script 项目 */ +export function isSupportedProject(projectDir: string): boolean { + if (!projectDir) return false; + try { + if (!fs.statSync(projectDir).isDirectory()) return false; + } catch { + return false; + } + return [ + path.join(projectDir, 'config.py'), + path.join(projectDir, 'src', 'config.py'), + ].some((file) => { + try { + return fs.statSync(file).isFile(); + } catch { + return false; + } + }); +} diff --git a/src/tempScreenshotPanel.ts b/src/tempScreenshotPanel.ts index 8ba457a3..f16b84a4 100644 --- a/src/tempScreenshotPanel.ts +++ b/src/tempScreenshotPanel.ts @@ -326,11 +326,22 @@ export class TempScreenshotViewProvider implements vscode.WebviewViewProvider { () => view.visible, ); controller.attachHtml(); + const updateBadge = () => { + const count = this.store.list().length; + view.badge = count > 0 + ? { value: count, tooltip: tr('{count} temporary screenshot(s) waiting to be sorted', { count }) } + : undefined; + }; + updateBadge(); + const storeSubscription = this.store.onChange(updateBadge); if (view.visible) void prewarmScreenshotCapture(); view.onDidChangeVisibility(() => { if (view.visible) { void prewarmScreenshotCapture(); void controller.refresh(); } }); - view.onDidDispose(() => controller.dispose()); + view.onDidDispose(() => { + storeSubscription.dispose(); + controller.dispose(); + }); } } From 047cd697dedb74c4fa503c5b650cd7864d1451c6 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:19:47 +0800 Subject: [PATCH 06/29] feat(ui): replace emoji/Unicode glyph icons with codicons - vendor @vscode/codicons 0.0.36 (css+ttf, licenses) into media/shared/codicons - shared/icons.js (OkIcons.icon/label/iconOnly) loaded by every panel; CSP gains font-src; applySharedAssets fills the two new placeholders - console, resource preview, annotation management, temp shots, annotation editor and character manager use codicons; icon-only buttons get aria-label - distinct icons: sync default (sync) vs restore (discard) vs swap (arrow-swap) - design-system doc + audit step 7 enforce codicons and forbid glyph icons - jsdom tests inline shared/icons.js like thumbnailActions.js --- docs/design-system.en.md | 5 + docs/design-system.md | 10 + media/annotationPanel/app.js | 4 + media/annotationPanel/index.html | 12 +- media/characterManager/app.js | 28 +- media/characterManager/index.html | 8 +- media/console/configPanel.js | 6 +- media/console/console.js | 18 +- media/console/core.js | 6 +- media/console/index.html | 4 +- media/console/taskCard.js | 31 +- media/shared/codicons/LICENSE | 395 ++++++++++++++ media/shared/codicons/LICENSE-CODE | 21 + media/shared/codicons/README.txt | 3 + media/shared/codicons/codicon.css | 629 +++++++++++++++++++++++ media/shared/codicons/codicon.ttf | Bin 0 -> 80188 bytes media/shared/controls.css | 12 + media/shared/icons.js | 29 ++ media/shared/thumbnailActions.js | 4 +- media/shared/tokens.css | 6 + media/tempScreenshots/app.js | 4 +- media/tempScreenshots/index.html | 4 +- media/templateAssetPanel/app.js | 6 +- media/templateAssetPanel/index.html | 4 +- media/templatePanel/app.js | 6 +- media/templatePanel/index.html | 4 +- scripts/test_annotation_clipboard.js | 1 + scripts/test_annotation_coords.js | 1 + scripts/test_annotation_history.js | 1 + scripts/test_asset_swap_picker.js | 1 + scripts/test_console_accountseg.js | 1 + scripts/test_console_configseg.js | 1 + scripts/test_design_system.js | 18 + scripts/test_task_launcher_executor.js | 1 + scripts/test_task_launcher_subconfigs.js | 1 + scripts/test_temp_screenshots.js | 1 + scripts/test_thumbnail_actions.js | 2 + src/webviewHtml.ts | 6 +- 38 files changed, 1228 insertions(+), 66 deletions(-) create mode 100644 media/shared/codicons/LICENSE create mode 100644 media/shared/codicons/LICENSE-CODE create mode 100644 media/shared/codicons/README.txt create mode 100644 media/shared/codicons/codicon.css create mode 100644 media/shared/codicons/codicon.ttf create mode 100644 media/shared/icons.js diff --git a/docs/design-system.en.md b/docs/design-system.en.md index f9039f44..47f31f08 100644 --- a/docs/design-system.en.md +++ b/docs/design-system.en.md @@ -95,8 +95,13 @@ media/shared/tokens.css Colors · Typography · Spacing · Radius · Border media/shared/controls.css Button(primary/secondary/mini/ghost/icon) · Input · Select · Checkbox/Switch · Tag · Card · Toolbar · SectionTitle · Empty/Broken/Hint · Divider · Clickability rules ([role=button], actual-button/clickable-row categories) + · .has-codicon / .is-icon-only (icon button alignment and size) +media/shared/codicons/ @vscode/codicons 0.0.36 (codicon.css + codicon.ttf, vendored unchanged) +media/shared/icons.js OkIcons.icon / label / iconOnly — the single way panels create icons ``` +**Icon rule (2026-10-09):** UI icons always use codicons (the same set as native VS Code UI). Emoji and Unicode glyphs (`▶ ⚙ ⇄ ⟲ + 👁 × ↻` and similar) are no longer used: they drift with the font and screen readers read out the symbol names. Icon-only buttons must have both `title` and `aria-label` (`OkIcons.iconOnly` handles this). One meaning, one icon: sync defaults `sync`, restore factory defaults `discard`, swap annotations `arrow-swap`, delete `trash`, view original `go-to-file` / `eye`. Panel HTML must load `__SHARED_CODICONS_URI__` and `__SHARED_ICONS_URI__`, and the CSP must include `font-src __CSP_SOURCE__`. + Connect a new panel in **three steps; omitting any step silently breaks styling**: 1. In `index.html`, load `__SHARED_TOKENS_URI__` → `__SHARED_CONTROLS_URI__` → panel styles in that order. This is cascade precedence; panel styles may override the shared layer. diff --git a/docs/design-system.md b/docs/design-system.md index 1ac01c67..97e4e36a 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -106,8 +106,18 @@ media/shared/tokens.css Colors · Typography · Spacing · Radius · Border media/shared/controls.css Button(主/次/迷你/幽灵/图标) · Input · Select · Checkbox/Switch · Tag · Card · Toolbar · SectionTitle · Empty/Broken/Hint · Divider · 可点击性硬规范([role=button]、真按钮/行级区分层清单) + · .has-codicon / .is-icon-only(图标按钮的对齐与尺寸) +media/shared/codicons/ @vscode/codicons 0.0.36(codicon.css + codicon.ttf,原样内置) +media/shared/icons.js OkIcons.icon / label / iconOnly —— 面板里生成图标的唯一入口 ``` +**图标规范(2026-10-09)**:界面图标一律用 codicon(与 VS Code 原生 UI 同源), +不再用 emoji 或 Unicode 符号(`▶ ⚙ ⇄ ⟲ + 👁 × ↻` 等字形随字体漂移、读屏会念出符号名)。 +仅图标的按钮必须同时有 `title` 与 `aria-label`(`OkIcons.iconOnly` 自动处理)。 +同一语义只用一个图标:同步默认值 `sync`、恢复出厂 `discard`、交换标注 `arrow-swap`、删除 `trash`、 +查看原图 `go-to-file` / `eye`。面板 HTML 需引入 `__SHARED_CODICONS_URI__` 与 `__SHARED_ICONS_URI__`, +CSP 需含 `font-src __CSP_SOURCE__`。 + 新增面板接入方式(**三步,缺一步样式就静默失效**): 1. `index.html` 按顺序引入 `__SHARED_TOKENS_URI__` → `__SHARED_CONTROLS_URI__` → 面板自身样式 diff --git a/media/annotationPanel/app.js b/media/annotationPanel/app.js index 52ee399d..efcbb8c1 100644 --- a/media/annotationPanel/app.js +++ b/media/annotationPanel/app.js @@ -214,7 +214,9 @@ document.getElementById('undoBtn').disabled = !canUndo(); document.getElementById('redoBtn').disabled = !canRedo(); document.getElementById('undoBtn').title = t('undo') + ' (' + keybindings.undo + ')'; + document.getElementById('undoBtn').setAttribute('aria-label', t('undo')); document.getElementById('redoBtn').title = t('redo') + ' (' + keybindings.redo + ')'; + document.getElementById('redoBtn').setAttribute('aria-label', t('redo')); } function parseKeybinding(kb) { @@ -1104,7 +1106,9 @@ document.getElementById('hideAllBtn').textContent = t('hideAllAnnotations'); document.getElementById('onlyCurrentBtn').textContent = t('showOnlyCurrent'); document.getElementById('prevBtn').title = t('prevImage'); + document.getElementById('prevBtn').setAttribute('aria-label', t('prevImage')); document.getElementById('nextBtn').title = t('nextImage'); + document.getElementById('nextBtn').setAttribute('aria-label', t('nextImage')); document.getElementById('emptyMsg').textContent = t('noImageLoaded'); updateModeUi(); } diff --git a/media/annotationPanel/index.html b/media/annotationPanel/index.html index 6a04c72a..8588f0b2 100644 --- a/media/annotationPanel/index.html +++ b/media/annotationPanel/index.html @@ -2,7 +2,8 @@ - + + @@ -27,12 +28,12 @@ - - + +
- - + +
@@ -151,6 +152,7 @@

Bounding Box

}, true); })(); + diff --git a/media/characterManager/app.js b/media/characterManager/app.js index 1a9008b2..7553ef52 100644 --- a/media/characterManager/app.js +++ b/media/characterManager/app.js @@ -20,6 +20,7 @@ function localizeStaticUi() { document.querySelector('.title').textContent = t('charactersTitle'); $('globalSearch').placeholder = t('searchCharacters'); $('refresh').title = t('refresh'); + $('refresh').setAttribute('aria-label', t('refresh')); const tabs = { characters: 'charactersTab', effects: 'effectsTab', locales: 'localesTab', issues: 'issuesTab' }; for (const node of document.querySelectorAll('.tab')) { const badge = node.querySelector('.badge'); @@ -38,8 +39,8 @@ function localizeStaticUi() { $('effectUsage').options[1].textContent = t('usedOnly'); $('effectUsage').options[2].textContent = t('unusedOnly'); $('effectUsage').options[3].textContent = t('unknownOnly'); - $('addEffectCategory').textContent = `+ ${t('addEffectCategory')}`; - $('addEffect').textContent = `+ ${t('addEffect')}`; + OkIcons.label($('addEffectCategory'), 'add', t('addEffectCategory')); + OkIcons.label($('addEffect'), 'add', t('addEffect')); document.querySelector('.locale-toolbar > span:first-child').textContent = t('localizationMatrix'); document.querySelector('.locale-toolbar > span:last-child').textContent = t('localizationHint'); $('issueSearch').placeholder = t('searchIssues'); @@ -49,8 +50,9 @@ function localizeStaticUi() { $('issueSeverity').options[3].textContent = t('info'); $('loading').firstElementChild.textContent = t('readingCharacterData'); $('modalClose').title = t('close'); - $('modalCancel').textContent = `× ${t('cancel')}`; - $('modalSave').textContent = `✓ ${t('save')}`; + $('modalClose').setAttribute('aria-label', t('close')); + OkIcons.label($('modalCancel'), 'close', t('cancel')); + OkIcons.label($('modalSave'), 'check', t('save')); } function clear(node) { node.replaceChildren(); } @@ -60,8 +62,10 @@ function element(tag, className, content) { if (content !== undefined) node.textContent = text(content); return node; } -function button(label, className, handler, title) { +/** icon 为 codicon 名(可选) */ +function button(label, className, handler, title, icon) { const node = element('button', className, label); + if (icon) OkIcons.label(node, icon, label); if (title) node.title = title; node.addEventListener('click', handler); return node; @@ -220,9 +224,9 @@ function showModal(title, build, onSubmit, saveText) { $('modalBody').appendChild(form); const actionLabel = saveText || t('save'); const actionKind = actionLabel === t('add') ? 'action-add' : actionLabel === t('modify') ? 'action-edit' : 'action-save'; - const actionIcon = actionLabel === t('add') ? '+ ' : actionLabel === t('modify') ? '✎ ' : '✓ '; + const actionIcon = actionLabel === t('add') ? 'add' : actionLabel === t('modify') ? 'edit' : 'check'; $('modalSave').className = `action-button ${actionKind}`; - $('modalSave').textContent = actionIcon + actionLabel; + OkIcons.label($('modalSave'), actionIcon, actionLabel); modalSubmit = () => onSubmit(controls); $('modalBackdrop').classList.add('show'); form.querySelector('input,select,textarea')?.focus(); @@ -433,7 +437,7 @@ function renderCharacterDetail(character) { $('characterDetail').appendChild(header); if (!character.skills.length) { $('characterDetail').appendChild(element('div', 'empty', t('noSkills'))); return; } const sectionTitle = element('div', 'section-title'); - sectionTitle.append(element('span', '', t('skillsAndEnhancements')), button(`+ ${t('addSkill')}`, 'action-button action-add compact', () => showSkillEditor(character, null))); + sectionTitle.append(element('span', '', t('skillsAndEnhancements')), button(t('addSkill'), 'action-button action-add compact', () => showSkillEditor(character, null), undefined, 'add')); $('characterDetail').appendChild(sectionTitle); for (const skill of character.skills) { const card = element('article', 'skill-card'); @@ -446,8 +450,8 @@ function renderCharacterDetail(character) { skillMeta.appendChild(makeChip(skillTypeLabel(skill.skillType))); if (skill.element) skillMeta.appendChild(makeChip(skill.element)); if (skill.hasEnhancement) skillMeta.appendChild(makeChip(t('enhancedState'))); - skillMeta.appendChild(button(`✎ ${t('modify')}`, 'action-button action-edit compact', () => showSkillEditor(character, skill), t('modifySkill'))); - if (skill.source === 'custom') skillMeta.appendChild(button(`× ${t('delete')}`, 'action-button action-delete compact', () => confirmDelete(skill.name, () => mutate({ action: 'deleteSkill', characterId: character.characterId, skillId: skill.skillId })))); + skillMeta.appendChild(button(t('modify'), 'action-button action-edit compact', () => showSkillEditor(character, skill), t('modifySkill'), 'edit')); + if (skill.source === 'custom') skillMeta.appendChild(button(t('delete'), 'action-button action-delete compact', () => confirmDelete(skill.name, () => mutate({ action: 'deleteSkill', characterId: character.characterId, skillId: skill.skillId })), undefined, 'trash')); else skillMeta.appendChild(makeChip(t('syncedSkillLocked'))); skillMeta.appendChild(button(`↗ ${t('open')}`, 'action-button action-open compact', () => postOpen('character', { characterId: character.characterId, skillId: skill.skillId }), t('openSource'))); head.appendChild(skillMeta); card.appendChild(head); @@ -463,7 +467,7 @@ function renderCharacterDetail(character) { const block = element('section', 'enhancement'); const enhancementHead = element('div', 'enhancement-head'); const name = element('div', 'enhancement-name', enhancement.name); if (enhancement.visiblePulse) name.appendChild(element('span', 'pulse', t('visiblePulse'))); const enhancementActions = element('div', 'enhancement-actions'); - enhancementActions.append(button(`✎ ${t('modify')}`, 'action-button action-edit compact', () => showEnhancementEditor(character, skill, enhancement, index), t('modifyEnhancement')), button(`× ${t('delete')}`, 'action-button action-delete compact', () => confirmDelete(enhancement.name, () => mutate({ action: 'deleteEnhancement', characterId: character.characterId, skillId: skill.skillId, enhancementIndex: index })))); + enhancementActions.append(button(t('modify'), 'action-button action-edit compact', () => showEnhancementEditor(character, skill, enhancement, index), t('modifyEnhancement'), 'edit'), button(t('delete'), 'action-button action-delete compact', () => confirmDelete(enhancement.name, () => mutate({ action: 'deleteEnhancement', characterId: character.characterId, skillId: skill.skillId, enhancementIndex: index }))), undefined, 'trash')); enhancementHead.append(name, enhancementActions); block.appendChild(enhancementHead); if (enhancement.triggerText) block.appendChild(element('div', 'enhancement-trigger', enhancement.triggerText)); if (enhancement.enhancementEffect) block.appendChild(element('div', 'item-sub', enhancement.enhancementEffect)); @@ -507,7 +511,7 @@ function renderCharacterDetail(character) { } body.appendChild(block); } - body.appendChild(button(`+ ${t('addEnhancement')}`, 'action-button action-add compact add-enhancement-action', () => showEnhancementEditor(character, skill, null, -1))); + body.appendChild(button(t('addEnhancement'), 'action-button action-add compact add-enhancement-action', () => showEnhancementEditor(character, skill, null, -1), undefined, 'add')); card.appendChild(body); $('characterDetail').appendChild(card); } } diff --git a/media/characterManager/index.html b/media/characterManager/index.html index c4eb05f4..f26f364a 100644 --- a/media/characterManager/index.html +++ b/media/characterManager/index.html @@ -3,8 +3,9 @@ - + + @@ -17,7 +18,7 @@
- +
+ diff --git a/media/templateAssetPanel/app.js b/media/templateAssetPanel/app.js index e6b1ecbd..4a5f2876 100644 --- a/media/templateAssetPanel/app.js +++ b/media/templateAssetPanel/app.js @@ -60,9 +60,9 @@ const actDiv = document.createElement('div'); actDiv.className = 'actions thumbnail-actions'; actDiv.append( - ThumbnailActions.button('👁', t('openSourceImage'), () => vscode.postMessage({ type: 'openSource', imagePath: meta.imagePath })), - ThumbnailActions.button('⇄', t('assetSwapTooltip'), () => openSwapPicker(meta)), - ThumbnailActions.button('×', t('assetDeleteTooltip'), () => vscode.postMessage({ type: 'deleteImage', imagePath: meta.imagePath })), + ThumbnailActions.button('go-to-file', t('openSourceImage'), () => vscode.postMessage({ type: 'openSource', imagePath: meta.imagePath })), + ThumbnailActions.button('arrow-swap', t('assetSwapTooltip'), () => openSwapPicker(meta)), + ThumbnailActions.button('trash', t('assetDeleteTooltip'), () => vscode.postMessage({ type: 'deleteImage', imagePath: meta.imagePath })), ); ['open', 'swap', 'delete'].forEach((action, index) => { actDiv.children[index].dataset.action = action; }); box.appendChild(actDiv); diff --git a/media/templateAssetPanel/index.html b/media/templateAssetPanel/index.html index 77bcad02..e3460fee 100644 --- a/media/templateAssetPanel/index.html +++ b/media/templateAssetPanel/index.html @@ -2,7 +2,8 @@ - + + @@ -35,6 +36,7 @@

+ diff --git a/media/templatePanel/app.js b/media/templatePanel/app.js index f282d9d5..d4e3f9e3 100644 --- a/media/templatePanel/app.js +++ b/media/templatePanel/app.js @@ -60,9 +60,9 @@ const actions = document.createElement('div'); actions.className = 'actions thumbnail-actions'; actions.append( - ThumbnailActions.button('+', t('insertExpression'), () => vscode.postMessage({ type: 'insert', expression: meta.expression })), - ThumbnailActions.button('⧉', t('copyExpression'), () => vscode.postMessage({ type: 'copy', expression: meta.expression })), - ThumbnailActions.button('👁', t('viewOriginal'), () => vscode.postMessage({ + ThumbnailActions.button('add', t('insertExpression'), () => vscode.postMessage({ type: 'insert', expression: meta.expression })), + ThumbnailActions.button('copy', t('copyExpression'), () => vscode.postMessage({ type: 'copy', expression: meta.expression })), + ThumbnailActions.button('eye', t('viewOriginal'), () => vscode.postMessage({ type: 'open', imagePath: meta.imagePath, name: meta.name, bbox: JSON.stringify(meta.bbox), })), ); diff --git a/media/templatePanel/index.html b/media/templatePanel/index.html index d21195dc..42d8debb 100644 --- a/media/templatePanel/index.html +++ b/media/templatePanel/index.html @@ -2,7 +2,8 @@ - + + @@ -21,6 +22,7 @@
+ diff --git a/scripts/test_annotation_clipboard.js b/scripts/test_annotation_clipboard.js index 4fb92d22..a5e78801 100644 --- a/scripts/test_annotation_clipboard.js +++ b/scripts/test_annotation_clipboard.js @@ -30,6 +30,7 @@ const dictionary = { }; let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); html = html .replaceAll('__CSP_NONCE__', 'test') .replaceAll('__CSP_SOURCE__', "'self'") diff --git a/scripts/test_annotation_coords.js b/scripts/test_annotation_coords.js index 44e1cccd..5523f8ed 100644 --- a/scripts/test_annotation_coords.js +++ b/scripts/test_annotation_coords.js @@ -22,6 +22,7 @@ const dictionary = { }; let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); html = html .replaceAll('__CSP_NONCE__', 'test') .replaceAll('__CSP_SOURCE__', "'self'") diff --git a/scripts/test_annotation_history.js b/scripts/test_annotation_history.js index f84834a5..51be1bcc 100644 --- a/scripts/test_annotation_history.js +++ b/scripts/test_annotation_history.js @@ -14,6 +14,7 @@ const root = path.resolve(__dirname, '..'); const componentRoot = path.join(root, 'media', 'annotationPanel'); const dictionary = new Proxy({}, { get: (_, key) => String(key) }); let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); html = html .replaceAll('__CSP_NONCE__', 'test') .replaceAll('__CSP_SOURCE__', "'self'") diff --git a/scripts/test_asset_swap_picker.js b/scripts/test_asset_swap_picker.js index fb3008e5..38a570a9 100755 --- a/scripts/test_asset_swap_picker.js +++ b/scripts/test_asset_swap_picker.js @@ -19,6 +19,7 @@ const dictionary = { cancel: 'Cancel', loadFailed: 'Failed', assetNoMatch: 'No match', noTemplatesWithHint: 'No templates', assetDropHint: 'Drop', }; let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); html = html.replaceAll('__CSP_NONCE__', 'test').replaceAll('__CSP_SOURCE__', "'self'") .replaceAll('__I18N_JSON__', JSON.stringify(dictionary)).replaceAll('__SHARED_TOKENS_URI__', '').replaceAll('__SHARED_CONTROLS_URI__', '') .replace('', ``) diff --git a/scripts/test_console_accountseg.js b/scripts/test_console_accountseg.js index e896bd3d..13f1876d 100644 --- a/scripts/test_console_accountseg.js +++ b/scripts/test_console_accountseg.js @@ -13,6 +13,7 @@ const { JSDOM, VirtualConsole } = jsdom; const root = path.resolve(__dirname, '..'); const componentRoot = path.join(root, 'media', 'console'); let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); const dictionary = { consoleTitle: 'Console', consoleTabTasks: 'Tasks', consoleTabGame: 'Game', consoleTabConfig: 'Config', diff --git a/scripts/test_console_configseg.js b/scripts/test_console_configseg.js index 46ca6c5a..b8b73c60 100644 --- a/scripts/test_console_configseg.js +++ b/scripts/test_console_configseg.js @@ -13,6 +13,7 @@ const { JSDOM, VirtualConsole } = jsdom; const root = path.resolve(__dirname, '..'); const componentRoot = path.join(root, 'media', 'console'); let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); const dictionary = { consoleTitle: 'Console', consoleTabTasks: 'Tasks', consoleTabGame: 'Game', consoleTabConfig: 'Config', diff --git a/scripts/test_design_system.js b/scripts/test_design_system.js index 4a156041..e893d4a4 100644 --- a/scripts/test_design_system.js +++ b/scripts/test_design_system.js @@ -123,4 +123,22 @@ for (const file of fs.readdirSync(sharedDir)) { } assert(offenders.length === 0, `以下文件直引了 --vscode-*:${offenders.join(', ')}`); +console.log('7. 图标统一走 codicons(引入 + CSP font-src + 不再用 emoji/Unicode 图标字形)'); +const ICON_GLYPHS = /[▶⚙⇄⟲+⧉👁✕↻↩↪◀⏸⏹✓✔⚠✎▲▼▸▾❌]/u; +for (const panel of panels) { + const dir = path.join(mediaDir, panel); + const html = read(path.join(dir, 'index.html')); + assert(html.includes('__SHARED_CODICONS_URI__'), `${panel}/index.html 未引入 codicons`); + assert(html.includes('__SHARED_ICONS_URI__'), `${panel}/index.html 未引入 shared/icons.js`); + assert(/font-src __CSP_SOURCE__/.test(html), `${panel}/index.html 的 CSP 缺少 font-src(codicon 字体会被拦截)`); + for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.js') || f.endsWith('.html'))) { + const code = read(path.join(dir, file)) + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/^\s*\/\/.*$/gm, '') + .replace(//g, ''); + const hit = code.match(ICON_GLYPHS); + assert(!hit, `media/${panel}/${file} 仍用字形 ${hit && hit[0]} 当图标(改用 OkIcons / codicon)`); + } +} + console.log('\n全部通过'); diff --git a/scripts/test_task_launcher_executor.js b/scripts/test_task_launcher_executor.js index 8f535cb1..47c2744f 100644 --- a/scripts/test_task_launcher_executor.js +++ b/scripts/test_task_launcher_executor.js @@ -12,6 +12,7 @@ const { JSDOM, VirtualConsole } = jsdom; const root = path.resolve(__dirname, '..'); const componentRoot = path.join(root, 'media', 'console'); let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); const dictionary = { taskTitle: 'Tasks', refresh: 'Refresh', noTasks: 'No tasks', launch: 'Launch', stop: 'Stop', diff --git a/scripts/test_task_launcher_subconfigs.js b/scripts/test_task_launcher_subconfigs.js index e4d5b2b2..8c1e2061 100644 --- a/scripts/test_task_launcher_subconfigs.js +++ b/scripts/test_task_launcher_subconfigs.js @@ -12,6 +12,7 @@ const { JSDOM, VirtualConsole } = jsdom; const root = path.resolve(__dirname, '..'); const componentRoot = path.join(root, 'media', 'console'); let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); const baseDictionary = JSON.parse(fs.readFileSync(path.join(root, 'l10n', 'webview.en.json'), 'utf8')); if (!baseDictionary.parameters) throw new Error('EN dictionary not found'); const dictionary = { diff --git a/scripts/test_temp_screenshots.js b/scripts/test_temp_screenshots.js index 5463466d..bb56e63a 100644 --- a/scripts/test_temp_screenshots.js +++ b/scripts/test_temp_screenshots.js @@ -37,6 +37,7 @@ const dictionary = { }; let html = fs.readFileSync(path.join(componentRoot, 'index.html'), 'utf8'); +html = html.replace('', () => ``); html = html .replaceAll('__CSP_NONCE__', 'test') .replaceAll('__CSP_SOURCE__', "'self'") diff --git a/scripts/test_thumbnail_actions.js b/scripts/test_thumbnail_actions.js index f4c2ccab..af82e7a8 100644 --- a/scripts/test_thumbnail_actions.js +++ b/scripts/test_thumbnail_actions.js @@ -10,6 +10,7 @@ try { jsdom = require('jsdom'); } catch { const { JSDOM } = jsdom; const root = path.resolve(__dirname, '..'); const shared = fs.readFileSync(path.join(root, 'media/shared/thumbnailActions.js'), 'utf8'); +const icons = fs.readFileSync(path.join(root, 'media/shared/icons.js'), 'utf8'); function gallery(panel, message) { const html = fs.readFileSync(path.join(root, 'media', panel, 'index.html'), 'utf8') @@ -22,6 +23,7 @@ function gallery(panel, message) { w.setTimeout = fn => { timers.set(++nextTimer, fn); return nextTimer; }; w.clearTimeout = id => timers.delete(id); w.acquireVsCodeApi = () => ({ postMessage: data => sent.push(JSON.parse(JSON.stringify(data))), getState: () => ({}), setState() {} }); + w.eval(icons); w.eval(shared); w.eval(fs.readFileSync(path.join(root, 'media', panel, 'app.js'), 'utf8')); w.dispatchEvent(new w.MessageEvent('message', { data: message })); diff --git a/src/webviewHtml.ts b/src/webviewHtml.ts index 69c0b5a7..9a52e490 100644 --- a/src/webviewHtml.ts +++ b/src/webviewHtml.ts @@ -40,7 +40,7 @@ export function sharedResourceRoot(extensionUri: vscode.Uri): vscode.Uri { } /** - * 把共享设计层的 URI 注入面板 HTML 的两个占位符。 + * 把共享设计层的 URI 注入面板 HTML 的占位符(tokens / controls / codicons / 缩略图动作)。 * * 顺序语义由面板 HTML 保证:`__SHARED_TOKENS_URI__` / `__SHARED_CONTROLS_URI__` * 两个 `` 必须排在面板自身样式之前,面板才能覆盖共享层。 @@ -57,9 +57,11 @@ export function applySharedAssets( joinPath(extensionUri, 'media', 'shared', name), ).toString(true); return html + .split('__SHARED_CODICONS_URI__').join(uri('codicons/codicon.css')) .split('__SHARED_TOKENS_URI__').join(uri('tokens.css')) .split('__SHARED_CONTROLS_URI__').join(uri('controls.css')) - .split('__SHARED_THUMBNAIL_ACTIONS_URI__').join(uri('thumbnailActions.js')); + .split('__SHARED_THUMBNAIL_ACTIONS_URI__').join(uri('thumbnailActions.js')) + .split('__SHARED_ICONS_URI__').join(uri('icons.js')); } /** From d64ad7824a0fd92a059df22d3091118a440fedd4 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:21:21 +0800 Subject: [PATCH 07/29] feat(console): responsive side nav and denser task list - side nav gets codicons + tooltips; collapses to a 44px icon rail at <=640px and to a top segmented bar at <=480px (labels only <=340px) - arrow keys move between segments (tablist pattern) - status bar rows and card actions wrap instead of overflowing - denser task cards (smaller header padding/title, 2-line description) - snapshot buttons are neutral secondary buttons, not primary --- media/console/console.css | 81 ++++++++++++++++++++++++++++++++++----- media/console/console.js | 23 ++++++++--- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/media/console/console.css b/media/console/console.css index cdce18c1..e64fbc06 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -199,7 +199,7 @@ .group-head.is-collapsed .group-head__chev { transform: rotate(-90deg); } .group-head__count { color: color-mix(in srgb, var(--muted) 72%, transparent); font-weight: 500; } -.group-body { display: grid; gap: var(--space-sm); } +.group-body { display: grid; gap: 6px; } .group-body.is-collapsed { display: none; } /* ── 任务卡片(新布局:单行主区 + 参数徽标 + 状态徽标) ─────────── */ @@ -222,9 +222,9 @@ .task-card__header { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: var(--space-md); + gap: var(--space-sm); align-items: start; - padding: var(--space-md) var(--space-lg) var(--space-sm); + padding: var(--space-sm) var(--space-md) 6px; } .task-card__identity { min-width: 0; } @@ -234,7 +234,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: var(--font-lg); + font-size: var(--font-md); font-weight: 650; } @@ -294,11 +294,16 @@ } .task-card__description { - margin-top: 7px; + margin-top: var(--space-xs); color: var(--muted); font-size: var(--font-xs); - line-height: 1.55; + line-height: 1.45; white-space: pre-wrap; + /* 列表里最多两行,完整描述在悬停摘要/抽屉里 */ + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; } .task-card__actions { @@ -924,6 +929,16 @@ } .subgroup-body { display: flex; flex-direction: column; gap: 8px; } .task-card__snap { min-width: 30px; font-size: var(--font-sm); } +/* 快照操作是次要动作:中性底色 + 描边,不与「启动」主按钮抢视觉 */ +.task-card__actions .task-card__snap { + flex: 0 0 auto; + min-height: var(--control-h-sm); + padding: 2px 6px; + border: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + color: var(--text-primary); + background: var(--bg-control); +} +.task-card__actions .task-card__snap:hover:not(:disabled) { background: var(--bg-control-hover); border-color: var(--border); } .tag--store { font-size: var(--font-2xs); border: 1px solid var(--border-strong, var(--border)); @@ -1024,16 +1039,62 @@ box-shadow: inset 2px 0 0 var(--border-strong); } .lay-side__ico { - width: 14px; height: 14px; + display: inline-flex; flex: 0 0 auto; - border-radius: var(--radius-sm); - background: var(--border-strong); - opacity: .7; + opacity: .8; } +.lay-side__ico .codicon { font-size: var(--icon-md); } .lay-side__item.is-active .lay-side__ico { opacity: 1; } +.lay-side__item:focus-visible { outline: 1px solid var(--border-strong); outline-offset: -1px; } .lay-side__foot { margin-top: auto; padding: 10px 12px; font-size: var(--font-xs); color: var(--muted); } .lay-main { flex: 1; min-width: 0; } +/* 中等宽度:侧栏收成图标导航条(文字进 tooltip),把横向空间还给任务列表 */ +@media (max-width: 640px) { + .gbar__row { flex-wrap: wrap; row-gap: 2px; } + .gbar__val { flex: 1 1 0; } + .gbar__actions { flex-wrap: wrap; margin-left: auto; } + .task-card__actions { flex-wrap: wrap; } + .shell { gap: var(--space-sm); } + .lay-side { width: 44px; padding: 6px 4px 6px 0; } + .lay-side__item { justify-content: center; padding: 8px 0; } + .lay-side__label, + .lay-side__foot { display: none; } +} + +/* 窄侧边栏(≈300px):导航改为顶部分段条(图标 + 文字),主内容占满宽度 */ +@media (max-width: 480px) { + .shell { flex-direction: column; gap: var(--space-sm); } + .lay-side { + position: static; + flex-direction: row; + width: auto; + max-height: none; + align-self: stretch; + margin-inline: -8px; + padding: var(--space-xs) 8px; + border-right: 0; + border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + } + .lay-side__item { + flex: 1 1 0; + min-width: 0; + gap: var(--space-xs); + padding: 5px var(--space-xs); + font-size: var(--font-sm); + } + .lay-side__item.is-active { box-shadow: inset 0 -2px 0 var(--border-strong); } + .lay-side__label { + display: inline; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} +@media (max-width: 340px) { + .lay-side__ico { display: none; } +} + /* ═══ 配置健康度条(任务分段顶部,真实快照数据) ══════════════ */ .rc-health { display: flex; diff --git a/media/console/console.js b/media/console/console.js index 9e931275..0e26761a 100644 --- a/media/console/console.js +++ b/media/console/console.js @@ -758,20 +758,33 @@ // 侧栏导航(主从双栏):文案 + 点击/键盘切换 const sideTabs = [ - ['segTasks', 'consoleTabTasks'], - ['segGame', 'consoleTabGame'], - ['segConfig', 'consoleTabConfig'], - ['segAccounts', 'consoleTabAccounts'], + ['segTasks', 'consoleTabTasks', 'checklist'], + ['segGame', 'consoleTabGame', 'game'], + ['segConfig', 'consoleTabConfig', 'settings'], + ['segAccounts', 'consoleTabAccounts', 'account'], ]; - for (const [id, key] of sideTabs) { + const tabItems = sideTabs.map(([id]) => $(id)); + for (const [id, key, icon] of sideTabs) { const item = $(id); + item.querySelector('.lay-side__ico').replaceChildren(OkIcons.icon(icon)); item.querySelector('.lay-side__label').textContent = t(key); + // 收成图标条时文字不可见:title 兜底提示,aria-label 保证读屏 + item.title = t(key); + item.setAttribute('aria-label', t(key)); item.addEventListener('click', () => switchSeg(item.dataset.seg)); item.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); switchSeg(item.dataset.seg); + return; } + // tablist 标准键位:方向键在分段间移动并切换 + const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key]; + if (!step) return; + event.preventDefault(); + const next = tabItems[(tabItems.indexOf(item) + step + tabItems.length) % tabItems.length]; + next.focus(); + switchSeg(next.dataset.seg); }); } $('sideFoot').textContent = t('consoleTitle'); From ab7fa5682eff9af7d658ea5da3d297610d02cdad Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:23:26 +0800 Subject: [PATCH 08/29] feat(ui): honest empty states and inline errors with next-step actions - Annotation Management empty state explains what goes here and offers Screenshot / Import (no longer reuses Resource Preview's COCO hint) - invalid annotation source shows a persistent banner with Open source file - Resource Preview empty state lists the COCO paths actually checked and links to Annotation Management; Box/Point modes get their own hint - Console status banner renders icon + action buttons (Set Project Path, Set Python Path, Show Logs, Retry, Reload Tasks, Stop Executor); executor start/exit errors go inline, falling back to a notification with the same buttons only when the Console is hidden - shared .panel-empty__*/.panel-actions/.panel-banner styles; unused noTemplatesWithHint/noTemplateAssets* strings removed --- l10n/bundle.l10n.es.json | 10 +++- l10n/bundle.l10n.ja.json | 10 +++- l10n/bundle.l10n.json | 10 +++- l10n/bundle.l10n.ko.json | 10 +++- l10n/bundle.l10n.zh-cn.json | 10 +++- l10n/bundle.l10n.zh-tw.json | 10 +++- l10n/webview.en.json | 13 +++-- l10n/webview.es.json | 13 +++-- l10n/webview.ja.json | 13 +++-- l10n/webview.ko.json | 13 +++-- l10n/webview.zh-cn.json | 13 +++-- l10n/webview.zh-tw.json | 13 +++-- media/console/app.js | 2 +- media/console/core.js | 28 ++++++++- media/shared/controls.css | 29 +++++++++ media/templateAssetPanel/app.js | 47 ++++++++++++++- media/templateAssetPanel/index.html | 1 + media/templatePanel/app.js | 31 +++++++++- src/consolePanel.ts | 91 ++++++++++++++++++++++++----- src/templateAssetPanel.ts | 11 ++++ src/templatePanel.ts | 8 ++- 21 files changed, 336 insertions(+), 50 deletions(-) diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 45b88bc3..3e8f99b1 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -278,5 +278,13 @@ "{count} temporary screenshot(s) waiting to be sorted": "{count} captura(s) temporal(es) por ordenar", "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "Aún no se han cargado tareas únicas. Abre la consola y espera a que se cargue la lista.", "Select a task to queue": "Selecciona una tarea para poner en cola", - "Select a task to run (the executor will start first)": "Selecciona una tarea para ejecutar (primero se iniciará el ejecutor)" + "Select a task to run (the executor will start first)": "Selecciona una tarea para ejecutar (primero se iniciará el ejecutor)", + "Show Logs": "Ver registros", + "Set Project Path": "Configurar ruta del proyecto", + "Set Python Path": "Configurar ruta de Python", + "Retry": "Reintentar", + "Reload Tasks": "Recargar tareas", + "Stop Executor": "Detener ejecutor", + "Executor closed": "Ejecutor cerrado", + "Executor exited with code {code}": "El ejecutor terminó con el código {code}" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 1c90bcc6..e1d295be 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -278,5 +278,13 @@ "{count} temporary screenshot(s) waiting to be sorted": "未整理の一時スクリーンショット {count} 枚", "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "一回限りのタスクがまだ読み込まれていません。コンソールを開いてタスク一覧の読み込みを待ってください。", "Select a task to queue": "キューに追加するタスクを選択", - "Select a task to run (the executor will start first)": "実行するタスクを選択(先にエグゼキューターを起動します)" + "Select a task to run (the executor will start first)": "実行するタスクを選択(先にエグゼキューターを起動します)", + "Show Logs": "ログを表示", + "Set Project Path": "プロジェクトパスを設定", + "Set Python Path": "Python パスを設定", + "Retry": "再試行", + "Reload Tasks": "タスクを再読み込み", + "Stop Executor": "エグゼキューターを停止", + "Executor closed": "エグゼキューターを終了しました", + "Executor exited with code {code}": "エグゼキューターが終了しました(終了コード {code})" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 296bb8b6..1f58cdac 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -278,5 +278,13 @@ "{count} temporary screenshot(s) waiting to be sorted": "{count} temporary screenshot(s) waiting to be sorted", "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.", "Select a task to queue": "Select a task to queue", - "Select a task to run (the executor will start first)": "Select a task to run (the executor will start first)" + "Select a task to run (the executor will start first)": "Select a task to run (the executor will start first)", + "Show Logs": "Show Logs", + "Set Project Path": "Set Project Path", + "Set Python Path": "Set Python Path", + "Retry": "Retry", + "Reload Tasks": "Reload Tasks", + "Stop Executor": "Stop Executor", + "Executor closed": "Executor closed", + "Executor exited with code {code}": "Executor exited with code {code}" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 4a626ff4..3ed85a73 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -278,5 +278,13 @@ "{count} temporary screenshot(s) waiting to be sorted": "정리 대기 중인 임시 스크린샷 {count}개", "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "일회성 작업이 아직 로드되지 않았습니다. 콘솔을 열고 작업 목록이 로드될 때까지 기다리세요.", "Select a task to queue": "대기열에 추가할 작업 선택", - "Select a task to run (the executor will start first)": "실행할 작업 선택(먼저 실행기를 시작합니다)" + "Select a task to run (the executor will start first)": "실행할 작업 선택(먼저 실행기를 시작합니다)", + "Show Logs": "로그 보기", + "Set Project Path": "프로젝트 경로 설정", + "Set Python Path": "Python 경로 설정", + "Retry": "다시 시도", + "Reload Tasks": "작업 다시 로드", + "Stop Executor": "실행기 종료", + "Executor closed": "실행기가 종료되었습니다", + "Executor exited with code {code}": "실행기가 종료되었습니다(종료 코드 {code})" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index c8c3345f..78a93354 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -278,5 +278,13 @@ "{count} temporary screenshot(s) waiting to be sorted": "{count} 张临时截图待整理", "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "尚未加载一次性任务。请打开控制台,等任务列表加载完成。", "Select a task to queue": "选择要加入队列的任务", - "Select a task to run (the executor will start first)": "选择要运行的任务(将先启动执行器)" + "Select a task to run (the executor will start first)": "选择要运行的任务(将先启动执行器)", + "Show Logs": "查看日志", + "Set Project Path": "设置项目路径", + "Set Python Path": "设置 Python 路径", + "Retry": "重试", + "Reload Tasks": "重新加载任务", + "Stop Executor": "关闭执行器", + "Executor closed": "执行器已关闭", + "Executor exited with code {code}": "执行器异常退出(退出码 {code})" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index fdff191a..52ad6e8c 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -278,5 +278,13 @@ "{count} temporary screenshot(s) waiting to be sorted": "{count} 張臨時截圖待整理", "No one-time tasks are loaded yet. Open the Console and wait for the task list to load.": "尚未載入一次性任務。請開啟控制台,等任務列表載入完成。", "Select a task to queue": "選擇要加入佇列的任務", - "Select a task to run (the executor will start first)": "選擇要執行的任務(將先啟動執行器)" + "Select a task to run (the executor will start first)": "選擇要執行的任務(將先啟動執行器)", + "Show Logs": "查看日誌", + "Set Project Path": "設定專案路徑", + "Set Python Path": "設定 Python 路徑", + "Retry": "重試", + "Reload Tasks": "重新載入任務", + "Stop Executor": "關閉執行器", + "Executor closed": "執行器已關閉", + "Executor exited with code {code}": "執行器異常結束(結束碼 {code})" } diff --git a/l10n/webview.en.json b/l10n/webview.en.json index 6e248dd9..b1f61370 100644 --- a/l10n/webview.en.json +++ b/l10n/webview.en.json @@ -248,7 +248,6 @@ "templatesHint": "Click=insert · Double-click=copy · Use the bottom-right buttons for direct actions", "templatesCount": "{shown}/{total} templates", "noTemplates": "No templates found.", - "noTemplatesWithHint": "No templates found.\nMake sure the workspace contains assets/coco_annotations.json\n(or ok_tasks/assets/coco_annotations.json).", "noTemplateMatch": "No templates match “{query}”.", "viewOriginal": "View source image with highlighted bounds", "thumbnailStats": "{loaded} thumbnails loaded", @@ -258,8 +257,6 @@ "templateSource": "Source: {path}", "templateAssetsTitle": "Annotation Management", "templateAssetsHint": "Click an image or its edit button to open annotations", - "noTemplateAssets": "No template assets yet.\nClick Import to add images.", - "noTemplateAssetsHint": "No template assets.", "screenshotSaved": "Screenshot saved: {name}", "screenshotFailed": "Screenshot failed: {error}", "noClipboardImage": "No image in clipboard.", @@ -385,5 +382,13 @@ "conflictFields": "Fields", "resourcesTitle": "Resource Preview", "resourcesSearch": "Search resource names or expressions…", - "resourcesEmpty": "No resources in this mode." + "resourcesEmpty": "No resources in this mode.", + "assetsEmptyTitle": "No images to annotate yet", + "assetsEmptyBody": "Capture or import an image, or drag one in from Temp Shots.", + "openAnnotationSource": "Open source file", + "previewEmptyTemplateTitle": "No published templates yet", + "previewEmptyTemplateBody": "Resource Preview shows published resources. Annotate images in Annotation Management, then publish them.", + "previewEmptyPositionBody": "Draw boxes or points in Annotation Management (Box / Point mode).", + "previewCheckedPaths": "Checked:\n{paths}", + "openAnnotationManagement": "Open Annotation Management" } diff --git a/l10n/webview.es.json b/l10n/webview.es.json index 793f02d0..dd1f6732 100644 --- a/l10n/webview.es.json +++ b/l10n/webview.es.json @@ -248,7 +248,6 @@ "templatesHint": "Clic=insertar · Doble clic=copiar · Use los botones inferiores derechos para acciones directas", "templatesCount": "{shown}/{total} plantillas", "noTemplates": "No se encontraron plantillas.", - "noTemplatesWithHint": "No se encontraron plantillas.\nCompruebe que el espacio de trabajo contiene assets/coco_annotations.json\n(o ok_tasks/assets/coco_annotations.json).", "noTemplateMatch": "No hay plantillas que coincidan con «{query}».", "viewOriginal": "Ver la imagen original con la posición resaltada", "thumbnailStats": "{loaded} miniaturas cargadas", @@ -258,8 +257,6 @@ "templateSource": "Origen: {path}", "templateAssetsTitle": "Gestión de anotaciones", "templateAssetsHint": "Pulse la imagen o el botón de editar para abrir anotaciones", - "noTemplateAssets": "No template assets yet.\nClick Import to add images.", - "noTemplateAssetsHint": "No template assets.", "screenshotSaved": "Screenshot saved: {name}", "screenshotFailed": "Screenshot failed: {error}", "noClipboardImage": "No image in clipboard.", @@ -385,5 +382,13 @@ "conflictFields": "Campos", "resourcesTitle": "Vista previa de recursos", "resourcesSearch": "Buscar nombres o expresiones de recursos…", - "resourcesEmpty": "No hay recursos en este modo." + "resourcesEmpty": "No hay recursos en este modo.", + "assetsEmptyTitle": "Aún no hay imágenes para anotar", + "assetsEmptyBody": "Captura o importa una imagen, o arrástrala desde Capturas temporales.", + "openAnnotationSource": "Abrir archivo de origen", + "previewEmptyTemplateTitle": "Aún no hay plantillas publicadas", + "previewEmptyTemplateBody": "La vista previa muestra recursos publicados. Anota imágenes en Gestión de anotaciones y luego publícalas.", + "previewEmptyPositionBody": "Dibuja cajas o puntos en Gestión de anotaciones (modo Box / Point).", + "previewCheckedPaths": "Rutas comprobadas:\n{paths}", + "openAnnotationManagement": "Abrir gestión de anotaciones" } diff --git a/l10n/webview.ja.json b/l10n/webview.ja.json index f4f2bedb..7d01cfb1 100644 --- a/l10n/webview.ja.json +++ b/l10n/webview.ja.json @@ -248,7 +248,6 @@ "templatesHint": "クリック=挿入 · ダブルクリック=コピー · 右下のボタンで直接操作", "templatesCount": "{shown}/{total} テンプレート", "noTemplates": "テンプレートが見つかりません。", - "noTemplatesWithHint": "テンプレートが見つかりません。\nワークスペースに assets/coco_annotations.json\n(または ok_tasks/assets/coco_annotations.json)があることを確認してください。", "noTemplateMatch": "「{query}」に一致するテンプレートがありません。", "viewOriginal": "位置を強調表示した元画像を表示", "thumbnailStats": "サムネイル {loaded} 件を読み込み", @@ -258,8 +257,6 @@ "templateSource": "ソース: {path}", "templateAssetsTitle": "アノテーション管理", "templateAssetsHint": "画像または編集ボタンをクリックしてアノテーションを開く", - "noTemplateAssets": "No template assets yet.\nClick Import to add images.", - "noTemplateAssetsHint": "No template assets.", "screenshotSaved": "Screenshot saved: {name}", "screenshotFailed": "Screenshot failed: {error}", "noClipboardImage": "No image in clipboard.", @@ -385,5 +382,13 @@ "conflictFields": "フィールド", "resourcesTitle": "リソースプレビュー", "resourcesSearch": "リソース名や式を検索…", - "resourcesEmpty": "このモードにはリソースがありません。" + "resourcesEmpty": "このモードにはリソースがありません。", + "assetsEmptyTitle": "アノテーションする画像がまだありません", + "assetsEmptyBody": "スクリーンショットを撮るか画像をインポートしてください。一時スクリーンショットからドラッグすることもできます。", + "openAnnotationSource": "ソースファイルを開く", + "previewEmptyTemplateTitle": "公開済みのテンプレートがまだありません", + "previewEmptyTemplateBody": "リソースプレビューには公開済みのリソースだけが表示されます。アノテーション管理で注釈を付けてから公開してください。", + "previewEmptyPositionBody": "アノテーション管理の Box / Point モードで注釈を付けてください。", + "previewCheckedPaths": "確認したパス:\n{paths}", + "openAnnotationManagement": "アノテーション管理を開く" } diff --git a/l10n/webview.ko.json b/l10n/webview.ko.json index f54b1336..16f826b0 100644 --- a/l10n/webview.ko.json +++ b/l10n/webview.ko.json @@ -248,7 +248,6 @@ "templatesHint": "클릭=삽입 · 더블클릭=복사 · 오른쪽 아래 버튼으로 바로 실행", "templatesCount": "{shown}/{total} 템플릿", "noTemplates": "템플릿을 찾을 수 없습니다.", - "noTemplatesWithHint": "템플릿을 찾을 수 없습니다.\n작업 영역에 assets/coco_annotations.json\n(또는 ok_tasks/assets/coco_annotations.json)이 있는지 확인하세요.", "noTemplateMatch": "“{query}”와 일치하는 템플릿이 없습니다.", "viewOriginal": "위치를 강조 표시한 원본 이미지 보기", "thumbnailStats": "미리 보기 {loaded}개 불러옴", @@ -258,8 +257,6 @@ "templateSource": "소스: {path}", "templateAssetsTitle": "어노테이션 관리", "templateAssetsHint": "이미지 또는 편집 버튼을 클릭하여 주석 편집기 열기", - "noTemplateAssets": "No template assets yet.\nClick Import to add images.", - "noTemplateAssetsHint": "No template assets.", "screenshotSaved": "Screenshot saved: {name}", "screenshotFailed": "Screenshot failed: {error}", "noClipboardImage": "No image in clipboard.", @@ -385,5 +382,13 @@ "conflictFields": "필드", "resourcesTitle": "리소스 미리보기", "resourcesSearch": "리소스 이름 또는 표현식 검색…", - "resourcesEmpty": "이 모드에 리소스가 없습니다." + "resourcesEmpty": "이 모드에 리소스가 없습니다.", + "assetsEmptyTitle": "어노테이션할 이미지가 아직 없습니다", + "assetsEmptyBody": "스크린샷을 찍거나 이미지를 가져오세요. 임시 스크린샷에서 끌어다 놓을 수도 있습니다.", + "openAnnotationSource": "소스 파일 열기", + "previewEmptyTemplateTitle": "게시된 템플릿이 아직 없습니다", + "previewEmptyTemplateBody": "리소스 미리보기에는 게시된 리소스만 표시됩니다. 어노테이션 관리에서 주석을 단 뒤 게시하세요.", + "previewEmptyPositionBody": "어노테이션 관리의 Box / Point 모드로 주석을 다세요.", + "previewCheckedPaths": "확인한 경로:\n{paths}", + "openAnnotationManagement": "어노테이션 관리 열기" } diff --git a/l10n/webview.zh-cn.json b/l10n/webview.zh-cn.json index 9d60b461..415c7574 100644 --- a/l10n/webview.zh-cn.json +++ b/l10n/webview.zh-cn.json @@ -248,7 +248,6 @@ "templatesHint": "单击=插入 · 双击=复制 · 右下角按钮可直接操作", "templatesCount": "{shown}/{total} 个模板", "noTemplates": "未找到任何模板。", - "noTemplatesWithHint": "未找到任何模板。\n请确认工作区存在 assets/coco_annotations.json\n(或 ok_tasks/assets/coco_annotations.json)。", "noTemplateMatch": "没有匹配“{query}”的模板。", "viewOriginal": "查看原图(红框标注位置)", "thumbnailStats": "缩略图 {loaded} 成功", @@ -258,8 +257,6 @@ "templateSource": "来源: {path}", "templateAssetsTitle": "标注管理", "templateAssetsHint": "单击或点击编辑按钮打开标注编辑器", - "noTemplateAssets": "暂无模板素材。\n点击 Import 导入图片。", - "noTemplateAssetsHint": "暂无模板素材。", "screenshotSaved": "截图已保存: {name}", "screenshotFailed": "截图保存失败: {error}", "noClipboardImage": "剪贴板中没有图片。", @@ -385,5 +382,13 @@ "conflictFields": "字段", "resourcesTitle": "资源预览", "resourcesSearch": "搜索资源名或表达式…", - "resourcesEmpty": "当前模式没有资源。" + "resourcesEmpty": "当前模式没有资源。", + "assetsEmptyTitle": "还没有可标注的图片", + "assetsEmptyBody": "截图或导入图片,也可以把「临时截图」里的图拖到这里。", + "openAnnotationSource": "打开源文件", + "previewEmptyTemplateTitle": "还没有已发布的模板", + "previewEmptyTemplateBody": "资源预览只显示已发布的资源。请先在标注管理中标注,再点「发布」。", + "previewEmptyPositionBody": "请在标注管理中用 Box / Point 模式标注。", + "previewCheckedPaths": "已检查:\n{paths}", + "openAnnotationManagement": "打开标注管理" } diff --git a/l10n/webview.zh-tw.json b/l10n/webview.zh-tw.json index 1754ec37..e6e12ea5 100644 --- a/l10n/webview.zh-tw.json +++ b/l10n/webview.zh-tw.json @@ -248,7 +248,6 @@ "templatesHint": "單擊=插入 · 雙擊=複製 · 右下角按鈕可直接操作", "templatesCount": "{shown}/{total} 個模板", "noTemplates": "找不到任何模板。", - "noTemplatesWithHint": "找不到任何模板。\n請確認工作區存在 assets/coco_annotations.json\n(或 ok_tasks/assets/coco_annotations.json)。", "noTemplateMatch": "沒有符合「{query}」的模板。", "viewOriginal": "檢視原圖(標示位置)", "thumbnailStats": "已載入 {loaded} 個縮圖", @@ -258,8 +257,6 @@ "templateSource": "來源:{path}", "templateAssetsTitle": "標註管理", "templateAssetsHint": "單擊或點擊編輯按鈕開啟標註編輯器", - "noTemplateAssets": "暂无模板素材。\n点击 Import 导入图片。", - "noTemplateAssetsHint": "暂无模板素材。", "screenshotSaved": "截图已保存: {name}", "screenshotFailed": "截图保存失败: {error}", "noClipboardImage": "剪贴板中没有图片。", @@ -385,5 +382,13 @@ "conflictFields": "欄位", "resourcesTitle": "資源預覽", "resourcesSearch": "搜尋資源名稱或運算式…", - "resourcesEmpty": "目前模式沒有資源。" + "resourcesEmpty": "目前模式沒有資源。", + "assetsEmptyTitle": "還沒有可標註的圖片", + "assetsEmptyBody": "截圖或匯入圖片,也可以把「臨時截圖」裡的圖拖到這裡。", + "openAnnotationSource": "開啟原始檔", + "previewEmptyTemplateTitle": "還沒有已發布的模板", + "previewEmptyTemplateBody": "資源預覽只顯示已發布的資源。請先在標註管理中標註,再點「發布」。", + "previewEmptyPositionBody": "請在標註管理中用 Box / Point 模式標註。", + "previewCheckedPaths": "已檢查:\n{paths}", + "openAnnotationManagement": "開啟標註管理" } diff --git a/media/console/app.js b/media/console/app.js index 5c09d84c..5f8023ba 100644 --- a/media/console/app.js +++ b/media/console/app.js @@ -88,7 +88,7 @@ document.getElementById('openCharacterManager').hidden = message.characterData !== true; break; case 'status': - setStatus(message.level, message.text); + setStatus(message.level, message.text, message.actions || []); break; default: break; diff --git a/media/console/core.js b/media/console/core.js index 3ba91e5f..82778fcf 100644 --- a/media/console/core.js +++ b/media/console/core.js @@ -49,15 +49,37 @@ const taskKind = task => task.kind || state.schemas[taskKey(task)]?.kind || 'onetime'; const triggerEnabled = key => state.executor.enabledTriggers.indexOf(key) >= 0; - function setStatus(level, text) { + const STATUS_ICONS = { error: 'error', warn: 'warning', ok: 'check', info: 'info' }; + + /** 状态横幅:文字 + 可选「下一步」按钮(actions 由宿主给出 {action,label}) */ + function setStatus(level, text, actions = []) { if (!text) { elements.status.hidden = true; - elements.status.textContent = ''; + elements.status.replaceChildren(); return; } elements.status.hidden = false; elements.status.className = `status-banner ${level || ''}`; - elements.status.textContent = text; + elements.status.setAttribute('role', level === 'error' ? 'alert' : 'status'); + const message = document.createElement('div'); + message.className = 'status-banner__text'; + if (STATUS_ICONS[level]) message.append(OkIcons.icon(STATUS_ICONS[level]), ' '); + message.append(text); + const nodes = [message]; + if (Array.isArray(actions) && actions.length) { + const row = document.createElement('div'); + row.className = 'panel-actions panel-actions--start'; + actions.forEach((item, index) => { + const button = document.createElement('button'); + button.type = 'button'; + button.className = index === 0 ? 'mini-btn mini-btn--primary' : 'mini-btn'; + button.textContent = item.label; + button.addEventListener('click', () => post({ type: 'statusAction', action: item.action })); + row.appendChild(button); + }); + nodes.push(row); + } + elements.status.replaceChildren(...nodes); } function initializeStaticUi() { diff --git a/media/shared/controls.css b/media/shared/controls.css index 56d1ff80..21529c30 100644 --- a/media/shared/controls.css +++ b/media/shared/controls.css @@ -181,6 +181,35 @@ input[type='radio'] { accent-color: var(--border-strong); } font-size: var(--font-sm); white-space: pre-wrap; } +.panel-empty__title { + margin-bottom: var(--space-xs); + color: var(--text-primary); + font-size: var(--font-md); + font-weight: var(--weight-medium); +} +.panel-empty__path { + font-family: var(--font-mono); + font-size: var(--font-xs); + word-break: break-all; +} +/* 空态 / 错误横幅里的下一步动作按钮 */ +.panel-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--space-sm); + margin-top: var(--space-md); +} +.panel-actions--start { justify-content: flex-start; margin-top: var(--space-sm); } +.panel-banner { + margin: var(--space-sm); + border: var(--border-width) solid color-mix(in srgb, var(--err) 45%, var(--border)); + border-radius: var(--radius-md); + padding: var(--space-sm) var(--space-md); + background: var(--danger-bg); + font-size: var(--font-sm); + white-space: pre-wrap; +} .panel-hint { color: var(--text-muted); font-size: var(--font-xs); diff --git a/media/templateAssetPanel/app.js b/media/templateAssetPanel/app.js index 4a5f2876..d7943562 100644 --- a/media/templateAssetPanel/app.js +++ b/media/templateAssetPanel/app.js @@ -102,7 +102,7 @@ emptyEl.style.display = 'none'; if (metas.length === 0) { emptyEl.style.display = ''; - emptyEl.textContent = t('noTemplatesWithHint'); + renderEmptyState(); } else if (shown === 0) { emptyEl.style.display = ''; emptyEl.textContent = t('assetNoMatch') + ': "' + search.value.trim() + '"'; @@ -111,6 +111,47 @@ search.addEventListener('input', () => applyFilter()); + /** 空态:说明这里放什么 + 直接给出下一步(截图 / 导入),不再借用资源预览的 COCO 提示 */ + function renderEmptyState() { + const title = document.createElement('div'); + title.className = 'panel-empty__title'; + title.textContent = t('assetsEmptyTitle'); + const body = document.createElement('div'); + body.textContent = t('assetsEmptyBody'); + const actions = document.createElement('div'); + actions.className = 'panel-actions'; + const shot = document.createElement('button'); + shot.type = 'button'; + shot.className = 'mini-btn mini-btn--primary'; + OkIcons.label(shot, 'device-camera', t('screenshot')); + shot.addEventListener('click', () => vscode.postMessage({ type: 'screenshot', hardForeground: hardForegroundChk.checked })); + const imp = document.createElement('button'); + imp.type = 'button'; + imp.className = 'mini-btn'; + OkIcons.label(imp, 'folder-opened', t('assetImport')); + imp.addEventListener('click', () => vscode.postMessage({ type: 'importFile' })); + actions.append(shot, imp); + emptyEl.replaceChildren(title, body, actions); + } + + /** 标注源文件损坏:常驻横幅 + 打开源文件(之前只有一次性通知,关掉就找不到了) */ + const sourceErrorEl = document.getElementById('sourceError'); + function renderSourceError(text) { + sourceErrorEl.hidden = !text; + if (!text) { sourceErrorEl.replaceChildren(); return; } + const message = document.createElement('div'); + message.append(OkIcons.icon('error'), ' ' + text); + const actions = document.createElement('div'); + actions.className = 'panel-actions panel-actions--start'; + const open = document.createElement('button'); + open.type = 'button'; + open.className = 'mini-btn'; + OkIcons.label(open, 'go-to-file', t('openAnnotationSource')); + open.addEventListener('click', () => vscode.postMessage({ type: 'openAnnotationSource' })); + actions.append(open); + sourceErrorEl.replaceChildren(message, actions); + } + /** * 一张缩略图的**唯一落地口**:写缓存 + 落到网格卡片 + 落到打开中的交换选择器。 * @@ -297,6 +338,10 @@ for (const it of (msg.items || [])) applyThumb(it.name, it.url); break; } + case 'sourceError': { + renderSourceError(msg.text || ''); + break; + } } }); diff --git a/media/templateAssetPanel/index.html b/media/templateAssetPanel/index.html index e3460fee..a576cd73 100644 --- a/media/templateAssetPanel/index.html +++ b/media/templateAssetPanel/index.html @@ -18,6 +18,7 @@ +
diff --git a/media/templatePanel/app.js b/media/templatePanel/app.js index d4e3f9e3..7413c24f 100644 --- a/media/templatePanel/app.js +++ b/media/templatePanel/app.js @@ -9,6 +9,8 @@ const cards = new Map(); let metas = []; let currentMode = 'template'; + /** 宿主告知的数据来源(模板模式 = 已发布的 COCO 路径候选) */ + let sources = []; let loadedCount = 0; let failedCount = 0; @@ -103,13 +105,39 @@ emptyEl.textContent = ''; if (metas.length === 0) { emptyEl.style.display = ''; - emptyEl.textContent = currentMode === 'template' ? t('noTemplatesWithHint') : t('resourcesEmpty'); + renderEmptyState(); } else if (shown === 0) { emptyEl.style.display = ''; emptyEl.textContent = t('noTemplateMatch', { query: search.value.trim() }); } } + /** 空态:资源预览只读「已发布」的资源,所以给出查过的路径 + 去标注管理的入口 */ + function renderEmptyState() { + const title = document.createElement('div'); + title.className = 'panel-empty__title'; + title.textContent = currentMode === 'template' ? t('previewEmptyTemplateTitle') : t('resourcesEmpty'); + const body = document.createElement('div'); + body.textContent = currentMode === 'template' ? t('previewEmptyTemplateBody') : t('previewEmptyPositionBody'); + const nodes = [title, body]; + if (sources.length) { + const checked = document.createElement('div'); + checked.className = 'panel-empty__path'; + checked.textContent = t('previewCheckedPaths', { paths: sources.join('\n') }); + nodes.push(checked); + } + const actions = document.createElement('div'); + actions.className = 'panel-actions'; + const open = document.createElement('button'); + open.type = 'button'; + open.className = 'mini-btn mini-btn--primary'; + OkIcons.label(open, 'edit', t('openAnnotationManagement')); + open.addEventListener('click', () => vscode.postMessage({ type: 'openAnnotationManagement' })); + actions.append(open); + nodes.push(actions); + emptyEl.replaceChildren(...nodes); + } + function resetResources(resources) { grid.innerHTML = ''; cards.clear(); @@ -167,6 +195,7 @@ switch (msg.type) { case 'resources': currentMode = msg.mode || currentMode; + sources = Array.isArray(msg.sources) ? msg.sources : []; updateModeButtons(); resetResources(msg.resources || []); break; diff --git a/src/consolePanel.ts b/src/consolePanel.ts index a6faaeed..a8dba6a8 100644 --- a/src/consolePanel.ts +++ b/src/consolePanel.ts @@ -385,6 +385,20 @@ export function formatConsoleOutputLine(line: string, now = new Date()): string * 触发任务;一次性任务以入队方式交给同一个执行器跑一次。 * 因此本类不再有「当前任务 / 单进程」语义,改为维护执行器会话与启用集合。 */ +/** 状态横幅里的「下一步」按钮:webview 只回传动作 id,宿主决定怎么做 */ +type StatusAction = 'showLogs' | 'openProjectSetting' | 'openPythonSetting' | 'startExecutor' | 'refresh' | 'stopExecutor'; + +function statusActionLabel(action: StatusAction): string { + switch (action) { + case 'showLogs': return tr('Show Logs'); + case 'openProjectSetting': return tr('Set Project Path'); + case 'openPythonSetting': return tr('Set Python Path'); + case 'startExecutor': return tr('Retry'); + case 'refresh': return tr('Reload Tasks'); + case 'stopExecutor': return tr('Stop Executor'); + } +} + /** 宿主侧执行器状态摘要:供状态栏、视图徽标和 context key 使用 */ export interface ExecutorStateSummary { status: 'idle' | 'connecting' | 'running'; @@ -615,6 +629,9 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { case 'openCharacterManager': this.openCharacterManager?.(); break; + case 'statusAction': + if (typeof msg.action === 'string') await this.runStatusAction(msg.action as StatusAction); + break; case 'openPath': // 多账户存储等数据文件的「打开位置」:交给操作系统文件管理器定位 if (typeof msg.path === 'string' && msg.path) { @@ -1076,6 +1093,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { type: 'status', level: 'warn', text: tr('No ok-script project was found. Configure okScriptToolkit.okScriptProjectPath or open a folder containing src/config.py.'), + actions: this.statusActions(['openProjectSetting']), }); return; } @@ -1085,6 +1103,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { type: 'status', level: 'warn', text: tr('Project directory does not exist: {path}', { path: projectDir }), + actions: this.statusActions(['openProjectSetting']), }); return; } @@ -1113,6 +1132,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { type: 'status', level: this.knownTasks.length ? 'warn' : 'error', text: tr('Failed to load task list: {error}', { error: result.error || tr('Unknown error') }), + actions: this.statusActions(['refresh', 'openPythonSetting', 'showLogs']), }); void this.probeSchemasInBackground(view, projectDir, pythonPath, locale, generation); return; @@ -1168,6 +1188,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { text: tr('Failed to collect task parameter schema (launch is still available): {error}', { error: probe.error || tr('Unknown error'), }), + actions: this.statusActions(['refresh', 'showLogs']), }); return; } @@ -1276,19 +1297,19 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { const { projectDir, pythonPath } = this.getConfig(); if (this.executor) { if (projectDir === this.executorProjectDir && projectDir === this.currentProjectDir) return true; - void vscode.window.showErrorMessage(tr('Stop the running executor before starting another project.')); + this.reportError(tr('Stop the running executor before starting another project.'), ['stopExecutor']); return false; } if (!projectDir) { - void vscode.window.showErrorMessage(tr('The ok-script project path is not configured.')); + this.reportError(tr('The ok-script project path is not configured.'), ['openProjectSetting']); return false; } if (!fs.existsSync(projectDir)) { - void vscode.window.showErrorMessage(tr('Project directory does not exist: {path}', { path: projectDir })); + this.reportError(tr('Project directory does not exist: {path}', { path: projectDir }), ['openProjectSetting']); return false; } if (!fs.existsSync(pythonPath) && pythonPath !== 'python') { - void vscode.window.showErrorMessage(tr('Python interpreter does not exist: {path}', { path: pythonPath })); + this.reportError(tr('Python interpreter does not exist: {path}', { path: pythonPath }), ['openPythonSetting']); return false; } @@ -1367,9 +1388,8 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { if (this.executor !== child) return; this.appendOutputLine(''); this.appendOutputLine(tr('❌ Failed to start Python process: {error}', { error: err.message })); - void vscode.window.showErrorMessage(tr('Failed to launch task: {error}', { error: err.message })); this.resetExecutorState(); - this.setStatus('error', tr('Failed to launch task: {error}', { error: err.message })); + this.reportError(tr('Failed to launch task: {error}', { error: err.message }), ['openPythonSetting', 'showLogs']); }); child.on('close', (code) => { if (this.executor !== child) return; @@ -1379,11 +1399,12 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { this.appendOutputLine(code === 0 ? tr('✅ Executor closed') : tr('❌ Executor exit code: {code}', { code: code ?? 'null' })); - this.setStatus(code === 0 ? 'ok' : 'error', code === 0 - ? tr('✅ Executor closed') - : tr('❌ Executor exit code: {code}', { code: code ?? 'null' })); - if (!wasStopping && code !== 0) { - void vscode.window.showErrorMessage(tr('❌ Executor exit code: {code}', { code: code ?? 'null' })); + if (code === 0) { + this.setStatus('ok', tr('Executor closed')); + } else if (!wasStopping) { + this.reportError(tr('Executor exited with code {code}', { code: code ?? 'null' }), ['showLogs', 'startExecutor']); + } else { + this.setStatus('error', tr('Executor exited with code {code}', { code: code ?? 'null' }), ['showLogs']); } }); this.pushExecutorState(view); @@ -1650,8 +1671,52 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { return false; } - private setStatus(level: 'ok' | 'warn' | 'error' | 'info', text: string): void { - void this.view?.webview.postMessage({ type: 'status', level, text }); + private setStatus(level: 'ok' | 'warn' | 'error' | 'info', text: string, actions: StatusAction[] = []): void { + void this.view?.webview.postMessage({ type: 'status', level, text, actions: this.statusActions(actions) }); + } + + private statusActions(actions: StatusAction[]): { action: StatusAction; label: string }[] { + return actions.map((action) => ({ action, label: statusActionLabel(action) })); + } + + /** + * 错误就地显示在控制台横幅(带下一步按钮);控制台不可见时才退回通知, + * 通知同样带这些按钮 —— 避免「一闪而过的通知是唯一线索」。 + */ + private reportError(text: string, actions: StatusAction[] = []): void { + if (this.view?.visible) { + this.setStatus('error', text, actions); + return; + } + const labels = actions.map(statusActionLabel); + void vscode.window.showErrorMessage(text, ...labels).then((picked) => { + const index = picked ? labels.indexOf(picked) : -1; + if (index >= 0) void this.runStatusAction(actions[index]); + }); + this.setStatus('error', text, actions); + } + + private async runStatusAction(action: StatusAction): Promise { + switch (action) { + case 'showLogs': + this.output.show(true); + break; + case 'openProjectSetting': + await vscode.commands.executeCommand('workbench.action.openSettings', 'okScriptToolkit.okScriptProjectPath'); + break; + case 'openPythonSetting': + await vscode.commands.executeCommand('workbench.action.openSettings', 'okScriptToolkit.okScriptPython'); + break; + case 'startExecutor': + await this.startExecutor(this.view ?? undefined); + break; + case 'refresh': + await this.commandRefresh(); + break; + case 'stopExecutor': + this.stopExecutor(); + break; + } } /** diff --git a/src/templateAssetPanel.ts b/src/templateAssetPanel.ts index b1c54d6e..77a0c34f 100644 --- a/src/templateAssetPanel.ts +++ b/src/templateAssetPanel.ts @@ -156,6 +156,12 @@ class AssetGalleryController { void vscode.window.showErrorMessage(tr('The annotation source is invalid. Fix the source file before saving or exporting.')); } this.lastReadErrors = authoringErrors.length > 0; + void this.webview.postMessage({ + type: 'sourceError', + text: authoringErrors.length + ? tr('The annotation source is invalid. Fix the source file before saving or exporting.') + : '', + }); const metas = imageFiles.map((imgPath) => { const size = this.imagePixelSize(imgPath); const cats = this.data.getCategoriesForImage(imgPath); @@ -231,6 +237,11 @@ class AssetGalleryController { } break; } + case 'openAnnotationSource': { + const file = this.data.annotationFile; + if (file && fs.existsSync(file)) await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(file)); + break; + } case 'openSource': { if (msg.imagePath && this.data.listImages().includes(msg.imagePath)) { await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(msg.imagePath)); diff --git a/src/templatePanel.ts b/src/templatePanel.ts index 7bfef15b..1f30b6c5 100644 --- a/src/templatePanel.ts +++ b/src/templatePanel.ts @@ -10,6 +10,7 @@ import { applySharedAssets, getNonce } from './webviewHtml'; import { readAuthoringFile } from './boxResourceStore'; import { readPoints } from './pointResourceStore'; import { templatesDirectory } from './projectConfig'; +import { cocoFeatureRelPaths } from './cocoFeaturePath'; export type ResourcePreviewMode = 'template' | 'rect' | 'point'; @@ -142,7 +143,9 @@ class GalleryController { const gen = ++this.generation; const mode = this.mode; const metas = this.metas(mode); - await this.webview.postMessage({ type: 'resources', mode, resources: metas }); + // 模板模式的空态要告诉用户「到底查了哪些文件」(发布产物 COCO 的候选路径) + const sources = mode === 'template' ? cocoFeatureRelPaths(this.features.root) : []; + await this.webview.postMessage({ type: 'resources', mode, resources: metas, sources }); if (gen !== this.generation || this.disposed || mode !== this.mode) return; const batchSize = 6; @@ -185,6 +188,9 @@ class GalleryController { await this.update(); } break; + case 'openAnnotationManagement': + await vscode.commands.executeCommand('okScriptToolkit.openTemplateAssets'); + break; case 'copy': if (typeof msg.expression === 'string' && msg.expression) { await vscode.env.clipboard.writeText(msg.expression); From 7dbb25c0cfbf77eb01dd57961ea9b8558b289fa4 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:26:13 +0800 Subject: [PATCH 09/29] feat(annotation): undo for the annotation swap The swap was already behind a modal confirm; after it is written the notification now offers Undo, which restores both images' original boxes only if neither image changed since the swap. --- l10n/bundle.l10n.es.json | 6 +++++- l10n/bundle.l10n.ja.json | 6 +++++- l10n/bundle.l10n.json | 6 +++++- l10n/bundle.l10n.ko.json | 6 +++++- l10n/bundle.l10n.zh-cn.json | 6 +++++- l10n/bundle.l10n.zh-tw.json | 6 +++++- src/templateAssetPanel.ts | 27 ++++++++++++++++++++++++++- 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 3e8f99b1..0401f0e0 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -286,5 +286,9 @@ "Reload Tasks": "Recargar tareas", "Stop Executor": "Detener ejecutor", "Executor closed": "Ejecutor cerrado", - "Executor exited with code {code}": "El ejecutor terminó con el código {code}" + "Executor exited with code {code}": "El ejecutor terminó con el código {code}", + "Undo": "Deshacer", + "Annotations changed after the swap, so it cannot be undone automatically.": "Las anotaciones cambiaron después del intercambio; no se puede deshacer automáticamente.", + "Failed to undo the swap.": "No se pudo deshacer el intercambio.", + "Swap undone: '{first}' and '{second}' have their original annotations again.": "Intercambio deshecho: '{first}' y '{second}' recuperan sus anotaciones originales." } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index e1d295be..112c4cb7 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -286,5 +286,9 @@ "Reload Tasks": "タスクを再読み込み", "Stop Executor": "エグゼキューターを停止", "Executor closed": "エグゼキューターを終了しました", - "Executor exited with code {code}": "エグゼキューターが終了しました(終了コード {code})" + "Executor exited with code {code}": "エグゼキューターが終了しました(終了コード {code})", + "Undo": "元に戻す", + "Annotations changed after the swap, so it cannot be undone automatically.": "交換後にアノテーションが変更されたため、自動で元に戻せません。", + "Failed to undo the swap.": "交換を元に戻せませんでした。", + "Swap undone: '{first}' and '{second}' have their original annotations again.": "交換を元に戻しました: '{first}' と '{second}' は元のアノテーションに戻りました。" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 1f58cdac..2b51bbef 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -286,5 +286,9 @@ "Reload Tasks": "Reload Tasks", "Stop Executor": "Stop Executor", "Executor closed": "Executor closed", - "Executor exited with code {code}": "Executor exited with code {code}" + "Executor exited with code {code}": "Executor exited with code {code}", + "Undo": "Undo", + "Annotations changed after the swap, so it cannot be undone automatically.": "Annotations changed after the swap, so it cannot be undone automatically.", + "Failed to undo the swap.": "Failed to undo the swap.", + "Swap undone: '{first}' and '{second}' have their original annotations again.": "Swap undone: '{first}' and '{second}' have their original annotations again." } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 3ed85a73..e2e90232 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -286,5 +286,9 @@ "Reload Tasks": "작업 다시 로드", "Stop Executor": "실행기 종료", "Executor closed": "실행기가 종료되었습니다", - "Executor exited with code {code}": "실행기가 종료되었습니다(종료 코드 {code})" + "Executor exited with code {code}": "실행기가 종료되었습니다(종료 코드 {code})", + "Undo": "실행 취소", + "Annotations changed after the swap, so it cannot be undone automatically.": "교환 후 어노테이션이 변경되어 자동으로 되돌릴 수 없습니다.", + "Failed to undo the swap.": "교환을 되돌리지 못했습니다.", + "Swap undone: '{first}' and '{second}' have their original annotations again.": "교환 취소됨: '{first}'와 '{second}'가 원래 어노테이션으로 돌아갔습니다." } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 78a93354..5b2d9f93 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -286,5 +286,9 @@ "Reload Tasks": "重新加载任务", "Stop Executor": "关闭执行器", "Executor closed": "执行器已关闭", - "Executor exited with code {code}": "执行器异常退出(退出码 {code})" + "Executor exited with code {code}": "执行器异常退出(退出码 {code})", + "Undo": "撤销", + "Annotations changed after the swap, so it cannot be undone automatically.": "交换之后标注又有改动,无法自动撤销。", + "Failed to undo the swap.": "撤销交换失败。", + "Swap undone: '{first}' and '{second}' have their original annotations again.": "已撤销交换:「{first}」和「{second}」恢复了原来的标注。" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 52ad6e8c..c98f343b 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -286,5 +286,9 @@ "Reload Tasks": "重新載入任務", "Stop Executor": "關閉執行器", "Executor closed": "執行器已關閉", - "Executor exited with code {code}": "執行器異常結束(結束碼 {code})" + "Executor exited with code {code}": "執行器異常結束(結束碼 {code})", + "Undo": "復原", + "Annotations changed after the swap, so it cannot be undone automatically.": "交換之後標註又有變動,無法自動復原。", + "Failed to undo the swap.": "復原交換失敗。", + "Swap undone: '{first}' and '{second}' have their original annotations again.": "已復原交換:「{first}」和「{second}」恢復了原本的標註。" } diff --git a/src/templateAssetPanel.ts b/src/templateAssetPanel.ts index 77a0c34f..6e63d5c8 100644 --- a/src/templateAssetPanel.ts +++ b/src/templateAssetPanel.ts @@ -775,9 +775,34 @@ class AssetGalleryController { } await this.update(); AnnotationPanel.current?.controller.reloadIfShowing([sourcePath, targetPath]); - void vscode.window.showInformationMessage( + // 交换是整体写盘:给一次撤销机会(只在两张图的框仍是交换后的样子时才执行,避免覆盖之后的编辑) + const swappedSource = scaleBoxes(targetBoxes, targetSize, sourceSize); + const swappedTarget = scaleBoxes(sourceBoxes, sourceSize, targetSize); + const undo = tr('Undo'); + const picked = await vscode.window.showInformationMessage( tr("Swapped annotations between '{first}' and '{second}'.", { first, second }), + undo, ); + if (picked !== undo) return; + this.data.load(); + if (JSON.stringify(this.boxesOf(sourcePath)) !== JSON.stringify(swappedSource) + || JSON.stringify(this.boxesOf(targetPath)) !== JSON.stringify(swappedTarget)) { + void vscode.window.showWarningMessage(tr('Annotations changed after the swap, so it cannot be undone automatically.')); + return; + } + let restored = false; + try { + restored = this.data.swapAnnotationsForImages(sourcePath, targetPath, sourceBoxes, targetBoxes, sourceSize, targetSize); + } catch { + restored = false; + } + if (!restored) { + void vscode.window.showErrorMessage(tr('Failed to undo the swap.')); + return; + } + await this.update(); + AnnotationPanel.current?.controller.reloadIfShowing([sourcePath, targetPath]); + void vscode.window.showInformationMessage(tr("Swap undone: '{first}' and '{second}' have their original annotations again.", { first, second })); } private boxesOf(imagePath: string): SwapBox[] { From bab2b84f56f60a3a1f648ddc0a03367c12df8d7e Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:26:13 +0800 Subject: [PATCH 10/29] feat(ui): one gesture model for thumbnail cards, keyboard access, delete apart - single click selects (no more 500ms delayed insert vs double-click copy); double-click / Enter = primary action (Resource Preview: insert reference, Annotation Management: open annotation editor, Temp Shots: preview) - keyboard: roving tabindex, role=listbox/option, arrows/Home/End move, E = annotate, Ctrl/Cmd+C = copy reference, Delete = delete (host confirms) - delete moved out of the view/swap row into its own top-right slot with danger styling on hover/focus; selected/focus card states - hints and card tooltips describe the gestures (6 languages) - tests updated for the new gestures; keyboard navigation covered --- l10n/webview.en.json | 6 +- l10n/webview.es.json | 6 +- l10n/webview.ja.json | 6 +- l10n/webview.ko.json | 6 +- l10n/webview.zh-cn.json | 6 +- l10n/webview.zh-tw.json | 6 +- media/shared/controls.css | 11 +++ media/shared/thumbnailActions.js | 132 ++++++++++++++++++++++++++---- media/tempScreenshots/app.js | 25 ++++-- media/templateAssetPanel/app.js | 29 +++++-- media/templatePanel/app.js | 13 +-- scripts/test_asset_swap_picker.js | 3 + scripts/test_thumbnail_actions.js | 77 +++++++++++------ 13 files changed, 254 insertions(+), 72 deletions(-) diff --git a/l10n/webview.en.json b/l10n/webview.en.json index b1f61370..3447417a 100644 --- a/l10n/webview.en.json +++ b/l10n/webview.en.json @@ -245,7 +245,7 @@ "overriddenTasksLabel": "Overridden tasks", "templatesSearch": "Search template names…", "templatesTitle": "Resource Preview", - "templatesHint": "Click=insert · Double-click=copy · Use the bottom-right buttons for direct actions", + "templatesHint": "Double-click / Enter = insert · Ctrl+C = copy · Buttons on each card for direct actions", "templatesCount": "{shown}/{total} templates", "noTemplates": "No templates found.", "noTemplateMatch": "No templates match “{query}”.", @@ -390,5 +390,7 @@ "previewEmptyTemplateBody": "Resource Preview shows published resources. Annotate images in Annotation Management, then publish them.", "previewEmptyPositionBody": "Draw boxes or points in Annotation Management (Box / Point mode).", "previewCheckedPaths": "Checked:\n{paths}", - "openAnnotationManagement": "Open Annotation Management" + "openAnnotationManagement": "Open Annotation Management", + "assetCardHint": "Double-click / Enter / E: open annotation editor · Delete: delete", + "tempGridLabel": "Temporary screenshots" } diff --git a/l10n/webview.es.json b/l10n/webview.es.json index dd1f6732..dde1ab0b 100644 --- a/l10n/webview.es.json +++ b/l10n/webview.es.json @@ -245,7 +245,7 @@ "overriddenTasksLabel": "Tareas con anulaciones", "templatesSearch": "Buscar nombres de plantillas…", "templatesTitle": "Vista previa de recursos", - "templatesHint": "Clic=insertar · Doble clic=copiar · Use los botones inferiores derechos para acciones directas", + "templatesHint": "Doble clic / Enter = insertar · Ctrl+C = copiar · Usa los botones de cada tarjeta para acciones directas", "templatesCount": "{shown}/{total} plantillas", "noTemplates": "No se encontraron plantillas.", "noTemplateMatch": "No hay plantillas que coincidan con «{query}».", @@ -390,5 +390,7 @@ "previewEmptyTemplateBody": "La vista previa muestra recursos publicados. Anota imágenes en Gestión de anotaciones y luego publícalas.", "previewEmptyPositionBody": "Dibuja cajas o puntos en Gestión de anotaciones (modo Box / Point).", "previewCheckedPaths": "Rutas comprobadas:\n{paths}", - "openAnnotationManagement": "Abrir gestión de anotaciones" + "openAnnotationManagement": "Abrir gestión de anotaciones", + "assetCardHint": "Doble clic / Enter / E: abrir editor de anotaciones · Supr: eliminar", + "tempGridLabel": "Capturas temporales" } diff --git a/l10n/webview.ja.json b/l10n/webview.ja.json index 7d01cfb1..027bc0f3 100644 --- a/l10n/webview.ja.json +++ b/l10n/webview.ja.json @@ -245,7 +245,7 @@ "overriddenTasksLabel": "上書きありのタスク", "templatesSearch": "テンプレート名を検索…", "templatesTitle": "リソースプレビュー", - "templatesHint": "クリック=挿入 · ダブルクリック=コピー · 右下のボタンで直接操作", + "templatesHint": "ダブルクリック / Enter = 挿入 · Ctrl+C = コピー · カード右下のボタンで直接操作", "templatesCount": "{shown}/{total} テンプレート", "noTemplates": "テンプレートが見つかりません。", "noTemplateMatch": "「{query}」に一致するテンプレートがありません。", @@ -390,5 +390,7 @@ "previewEmptyTemplateBody": "リソースプレビューには公開済みのリソースだけが表示されます。アノテーション管理で注釈を付けてから公開してください。", "previewEmptyPositionBody": "アノテーション管理の Box / Point モードで注釈を付けてください。", "previewCheckedPaths": "確認したパス:\n{paths}", - "openAnnotationManagement": "アノテーション管理を開く" + "openAnnotationManagement": "アノテーション管理を開く", + "assetCardHint": "ダブルクリック / Enter / E: アノテーションエディターを開く · Delete: 削除", + "tempGridLabel": "一時スクリーンショット" } diff --git a/l10n/webview.ko.json b/l10n/webview.ko.json index 16f826b0..eeaef89e 100644 --- a/l10n/webview.ko.json +++ b/l10n/webview.ko.json @@ -245,7 +245,7 @@ "overriddenTasksLabel": "재정의가 있는 작업", "templatesSearch": "템플릿 이름 검색…", "templatesTitle": "리소스 미리보기", - "templatesHint": "클릭=삽입 · 더블클릭=복사 · 오른쪽 아래 버튼으로 바로 실행", + "templatesHint": "더블클릭 / Enter = 삽입 · Ctrl+C = 복사 · 카드 오른쪽 아래 버튼으로 바로 실행", "templatesCount": "{shown}/{total} 템플릿", "noTemplates": "템플릿을 찾을 수 없습니다.", "noTemplateMatch": "“{query}”와 일치하는 템플릿이 없습니다.", @@ -390,5 +390,7 @@ "previewEmptyTemplateBody": "리소스 미리보기에는 게시된 리소스만 표시됩니다. 어노테이션 관리에서 주석을 단 뒤 게시하세요.", "previewEmptyPositionBody": "어노테이션 관리의 Box / Point 모드로 주석을 다세요.", "previewCheckedPaths": "확인한 경로:\n{paths}", - "openAnnotationManagement": "어노테이션 관리 열기" + "openAnnotationManagement": "어노테이션 관리 열기", + "assetCardHint": "더블클릭 / Enter / E: 어노테이션 편집기 열기 · Delete: 삭제", + "tempGridLabel": "임시 스크린샷" } diff --git a/l10n/webview.zh-cn.json b/l10n/webview.zh-cn.json index 415c7574..a2198ed9 100644 --- a/l10n/webview.zh-cn.json +++ b/l10n/webview.zh-cn.json @@ -245,7 +245,7 @@ "overriddenTasksLabel": "有覆盖的任务", "templatesSearch": "搜索模板名…", "templatesTitle": "资源预览", - "templatesHint": "单击=插入 · 双击=复制 · 右下角按钮可直接操作", + "templatesHint": "双击 / Enter = 插入 · Ctrl+C = 复制 · 卡片右下角按钮可直接操作", "templatesCount": "{shown}/{total} 个模板", "noTemplates": "未找到任何模板。", "noTemplateMatch": "没有匹配“{query}”的模板。", @@ -390,5 +390,7 @@ "previewEmptyTemplateBody": "资源预览只显示已发布的资源。请先在标注管理中标注,再点「发布」。", "previewEmptyPositionBody": "请在标注管理中用 Box / Point 模式标注。", "previewCheckedPaths": "已检查:\n{paths}", - "openAnnotationManagement": "打开标注管理" + "openAnnotationManagement": "打开标注管理", + "assetCardHint": "双击 / Enter / E:打开标注编辑器 · Delete:删除", + "tempGridLabel": "临时截图" } diff --git a/l10n/webview.zh-tw.json b/l10n/webview.zh-tw.json index e6e12ea5..b821c523 100644 --- a/l10n/webview.zh-tw.json +++ b/l10n/webview.zh-tw.json @@ -245,7 +245,7 @@ "overriddenTasksLabel": "有覆寫的任務", "templatesSearch": "搜尋模板名稱…", "templatesTitle": "資源預覽", - "templatesHint": "單擊=插入 · 雙擊=複製 · 右下角按鈕可直接操作", + "templatesHint": "雙擊 / Enter = 插入 · Ctrl+C = 複製 · 卡片右下角按鈕可直接操作", "templatesCount": "{shown}/{total} 個模板", "noTemplates": "找不到任何模板。", "noTemplateMatch": "沒有符合「{query}」的模板。", @@ -390,5 +390,7 @@ "previewEmptyTemplateBody": "資源預覽只顯示已發布的資源。請先在標註管理中標註,再點「發布」。", "previewEmptyPositionBody": "請在標註管理中用 Box / Point 模式標註。", "previewCheckedPaths": "已檢查:\n{paths}", - "openAnnotationManagement": "開啟標註管理" + "openAnnotationManagement": "開啟標註管理", + "assetCardHint": "雙擊 / Enter / E:開啟標註編輯器 · Delete:刪除", + "tempGridLabel": "臨時截圖" } diff --git a/media/shared/controls.css b/media/shared/controls.css index 21529c30..df5eb831 100644 --- a/media/shared/controls.css +++ b/media/shared/controls.css @@ -269,6 +269,17 @@ input[type='radio'] { accent-color: var(--border-strong); } background: var(--bg-control-hover); border-color: var(--border); } +/* 危险动作(删除)与查看/交换分开放:卡片右上角,悬停才转危险色 */ +.thumbnail-actions--danger { top: 3px; bottom: auto; } +.thumbnail-actions button.is-danger:hover, +.thumbnail-actions button.is-danger:focus-visible { + color: var(--err); + border-color: var(--err); +} + +/* 缩略图卡片选中 / 键盘焦点(单击 = 选中,Enter / 双击 = 主操作) */ +.card.is-selected { border-color: var(--border-strong); box-shadow: inset 0 0 0 1px var(--border-strong); } +.card:focus-visible { outline: 1px solid var(--border-strong); outline-offset: 2px; } /* 行级可点击区(组头 / 折叠标题行 / tab):极浅色调,不参与按钮权重 */ .seg__btn:not(.is-active), diff --git a/media/shared/thumbnailActions.js b/media/shared/thumbnailActions.js index 22b22b15..667f6621 100644 --- a/media/shared/thumbnailActions.js +++ b/media/shared/thumbnailActions.js @@ -1,4 +1,47 @@ (function () { + /** + * 缩略图卡片的统一交互(资源预览 / 标注管理 / 临时截图三处共用): + * + * 单击 / Space = 选中(无副作用;不再有 500ms 延迟提交) + * 双击 / Enter = 主操作(资源预览 = 插入引用;标注管理 = 打开标注编辑器) + * E = 编辑(标注) + * Ctrl/Cmd + C = 复制引用 + * Delete = 删除(宿主会弹确认) + * 方向键 / Home / End = 在网格里移动选中 + * + * 卡片是 role=option、tabindex 轮转(只有选中项 tabindex=0),网格是 role=listbox。 + */ + function visibleCards(grid) { + return [...grid.querySelectorAll('.card')].filter((card) => card.style.display !== 'none'); + } + + function columnsOf(cards) { + if (cards.length < 2) return 1; + const top = cards[0].offsetTop; + let columns = 0; + for (const card of cards) { + if (card.offsetTop !== top) break; + columns++; + } + return Math.max(1, columns); + } + + function select(card, focus) { + const grid = card.parentElement; + if (grid) { + for (const other of grid.querySelectorAll('.card')) { + if (other === card) continue; + other.classList.remove('is-selected'); + other.setAttribute('aria-selected', 'false'); + other.tabIndex = -1; + } + } + card.classList.add('is-selected'); + card.setAttribute('aria-selected', 'true'); + card.tabIndex = 0; + if (focus) card.focus(); + } + window.ThumbnailActions = { button(icon, label, onClick) { const button = document.createElement('button'); @@ -10,31 +53,86 @@ button.setAttribute('aria-label', label); button.addEventListener('click', (event) => { event.stopPropagation(); - const card = button.closest('.card'); - window.clearTimeout(card?._clickTimer); - if (card) card._clickTimer = undefined; onClick(); }); button.addEventListener('dblclick', (event) => event.stopPropagation()); return button; }, - bindClicks(card, onSingle, onDouble) { - // Use the same 500ms gesture window for committing and recognizing clicks. - // A late native dblclick cannot copy after an insertion has already committed. + + /** 网格容器:listbox 语义 + 无障碍名称 */ + bindGrid(grid, label) { + grid.setAttribute('role', 'listbox'); + if (label) grid.setAttribute('aria-label', label); + }, + + /** + * handlers: { label, onPrimary, onEdit?, onCopy?, onDelete?, onSelect? } + */ + bindCard(card, handlers) { + card.setAttribute('role', 'option'); + card.setAttribute('aria-selected', 'false'); + if (handlers.label) card.setAttribute('aria-label', handlers.label); + // tab 轮转:先全部 -1,渲染/过滤后由 refreshTabStop 选出唯一入口 + card.tabIndex = -1; + card.addEventListener('click', (event) => { - if (event.button !== 0 || event.detail > 2) return; - const pending = card._clickTimer !== undefined; - window.clearTimeout(card._clickTimer); - card._clickTimer = undefined; - if (pending) { - onDouble(); - return; + if (event.button !== 0) return; + select(card, true); + handlers.onSelect?.(); + }); + card.addEventListener('dblclick', () => { + select(card, false); + handlers.onPrimary?.(); + }); + card.addEventListener('keydown', (event) => { + if (event.target !== card) return; + const key = event.key; + if (key === 'Enter') { + event.preventDefault(); + select(card, false); + handlers.onPrimary?.(); + } else if (key === ' ') { + event.preventDefault(); + select(card, false); + handlers.onSelect?.(); + } else if ((key === 'e' || key === 'E') && !event.ctrlKey && !event.metaKey && !event.altKey && handlers.onEdit) { + event.preventDefault(); + handlers.onEdit(); + } else if ((key === 'c' || key === 'C') && (event.ctrlKey || event.metaKey) && handlers.onCopy) { + event.preventDefault(); + handlers.onCopy(); + } else if ((key === 'Delete' || (key === 'Backspace' && event.metaKey)) && handlers.onDelete) { + event.preventDefault(); + handlers.onDelete(); + } else if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key)) { + const parent = card.parentElement; + if (!parent) return; + const cards = visibleCards(parent); + const index = cards.indexOf(card); + if (index < 0) return; + const columns = columnsOf(cards); + const delta = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -columns, ArrowDown: columns }[key]; + let next = index; + if (key === 'Home') next = 0; + else if (key === 'End') next = cards.length - 1; + else next = Math.min(cards.length - 1, Math.max(0, index + delta)); + event.preventDefault(); + if (next !== index || key === 'Home' || key === 'End') { + select(cards[next], true); + cards[next].scrollIntoView?.({ block: 'nearest' }); + } } - card._clickTimer = window.setTimeout(() => { - card._clickTimer = undefined; - if (card.isConnected) onSingle(); - }, 500); }); }, + + /** 渲染或过滤后调用:保证网格里恰好一张可见卡片能被 Tab 进入(优先选中项) */ + refreshTabStop(grid) { + const cards = visibleCards(grid); + const selected = cards.find((card) => card.classList.contains('is-selected')); + const entry = selected || cards[0]; + for (const card of grid.querySelectorAll('.card')) card.tabIndex = card === entry ? 0 : -1; + }, + + select, }; })(); diff --git a/media/tempScreenshots/app.js b/media/tempScreenshots/app.js index ecc74e35..511d5ca8 100644 --- a/media/tempScreenshots/app.js +++ b/media/tempScreenshots/app.js @@ -9,6 +9,7 @@ const selBox = document.getElementById('selBox'); const stageEmpty = document.getElementById('stageEmpty'); const grid = document.getElementById('grid'); + ThumbnailActions.bindGrid(grid, t('tempGridLabel')); const emptyHint = document.getElementById('emptyHint'); const countEl = document.getElementById('count'); const coordInfo = document.getElementById('coordInfo'); @@ -291,20 +292,29 @@ const actions = document.createElement('div'); actions.className = 'actions thumbnail-actions'; const sendBtn = ThumbnailActions.button('arrow-right', t('tempSendToAssets'), () => vscode.postMessage({ type: 'sendToAssets', id: meta.id })); - sendBtn.className = 'send'; - const delBtn = ThumbnailActions.button('trash', t('tempDelete'), () => vscode.postMessage({ type: 'deleteTemp', id: meta.id })); - delBtn.className = 'del'; - actions.append(sendBtn, delBtn); + sendBtn.classList.add('send'); + actions.append(sendBtn); preview.appendChild(actions); + // 删除单独放右上角,与「添加到标注管理」分开 + const dangerActions = document.createElement('div'); + dangerActions.className = 'actions thumbnail-actions thumbnail-actions--danger'; + const delBtn = ThumbnailActions.button('trash', t('tempDelete'), () => vscode.postMessage({ type: 'deleteTemp', id: meta.id })); + delBtn.classList.add('del', 'is-danger'); + dangerActions.append(delBtn); + preview.appendChild(dangerActions); const name = document.createElement('div'); name.className = 'name'; name.textContent = meta.name; card.appendChild(name); - card.addEventListener('click', () => { - stopCarousel(); - showFrame(index); + // 单击 / Space = 选中并在上方预览;Enter / 双击 = 同样预览(临时截图没有「插入」类主操作);Delete = 删除 + const preview_ = () => { stopCarousel(); showFrame(index); }; + ThumbnailActions.bindCard(card, { + label: meta.name, + onSelect: preview_, + onPrimary: preview_, + onDelete: () => vscode.postMessage({ type: 'deleteTemp', id: meta.id }), }); card.addEventListener('dragstart', (e) => onCardDragStart(e, meta, card)); card.addEventListener('dragend', () => { @@ -318,6 +328,7 @@ updateCount(); updateEmpty(); + ThumbnailActions.refreshTabStop(grid); } function updateCount() { diff --git a/media/templateAssetPanel/app.js b/media/templateAssetPanel/app.js index d7943562..d51a4109 100644 --- a/media/templateAssetPanel/app.js +++ b/media/templateAssetPanel/app.js @@ -11,6 +11,7 @@ let metas = []; document.title = t('templateAssetsTitle'); + ThumbnailActions.bindGrid(grid, t('templateAssetsTitle')); search.placeholder = t('templatesSearch'); document.getElementById('importBtn').textContent = t('assetImport'); document.getElementById('screenshotBtn').textContent = t('screenshot'); @@ -36,7 +37,8 @@ function updateCardMeta(card, meta) { card.title = meta.name + '\n' + meta.width + 'x' + meta.height + - (meta.categories.length ? '\n' + meta.categories.join(', ') : ''); + (meta.categories.length ? '\n' + meta.categories.join(', ') : '') + + '\n' + t('assetCardHint'); const name = card.querySelector('.name'); const cats = card.querySelector('.cats'); const size = card.querySelector('.size'); @@ -62,10 +64,17 @@ actDiv.append( ThumbnailActions.button('go-to-file', t('openSourceImage'), () => vscode.postMessage({ type: 'openSource', imagePath: meta.imagePath })), ThumbnailActions.button('arrow-swap', t('assetSwapTooltip'), () => openSwapPicker(meta)), - ThumbnailActions.button('trash', t('assetDeleteTooltip'), () => vscode.postMessage({ type: 'deleteImage', imagePath: meta.imagePath })), ); - ['open', 'swap', 'delete'].forEach((action, index) => { actDiv.children[index].dataset.action = action; }); + ['open', 'swap'].forEach((action, index) => { actDiv.children[index].dataset.action = action; }); box.appendChild(actDiv); + // 删除与查看/交换分开:放在右上角,危险色只在悬停/聚焦时出现(宿主仍会弹确认) + const dangerDiv = document.createElement('div'); + dangerDiv.className = 'actions thumbnail-actions thumbnail-actions--danger'; + const deleteBtn = ThumbnailActions.button('trash', t('assetDeleteTooltip'), () => requestDelete(meta)); + deleteBtn.classList.add('is-danger'); + deleteBtn.dataset.action = 'delete'; + dangerDiv.appendChild(deleteBtn); + box.appendChild(dangerDiv); const m = document.createElement('div'); m.className = 'meta'; @@ -82,12 +91,21 @@ card.appendChild(box); card.appendChild(m); updateCardMeta(card, meta); - card.addEventListener('click', () => { - vscode.postMessage({ type: 'openAnnotation', imagePath: meta.imagePath }); + // 单击 = 选中;双击 / Enter / E = 打开标注编辑器;Delete = 删除(宿主确认) + const openAnnotation = () => vscode.postMessage({ type: 'openAnnotation', imagePath: meta.imagePath }); + ThumbnailActions.bindCard(card, { + label: meta.name, + onPrimary: openAnnotation, + onEdit: openAnnotation, + onDelete: () => requestDelete(meta), }); return card; } + function requestDelete(meta) { + vscode.postMessage({ type: 'deleteImage', imagePath: meta.imagePath }); + } + function applyFilter() { const q = search.value.trim().toLowerCase(); let shown = 0; @@ -99,6 +117,7 @@ if (ok) shown++; } updateCount(); + ThumbnailActions.refreshTabStop(grid); emptyEl.style.display = 'none'; if (metas.length === 0) { emptyEl.style.display = ''; diff --git a/media/templatePanel/app.js b/media/templatePanel/app.js index 7413c24f..efadee22 100644 --- a/media/templatePanel/app.js +++ b/media/templatePanel/app.js @@ -22,6 +22,7 @@ document.getElementById('rectModeBtn').textContent = t('modeRect'); document.getElementById('pointModeBtn').textContent = t('modePoint'); document.querySelector('.hint').textContent = t('templatesHint'); + ThumbnailActions.bindGrid(grid, t('resourcesTitle')); function shownCount() { let n = 0; @@ -82,11 +83,12 @@ m.append(nm, sz); card.append(box, m); - ThumbnailActions.bindClicks( - card, - () => vscode.postMessage({ type: 'insert', expression: meta.expression }), - () => vscode.postMessage({ type: 'copy', expression: meta.expression }), - ); + // 单击 = 选中;双击 / Enter = 插入引用;Ctrl+C = 复制引用(三处缩略图同一套手势) + ThumbnailActions.bindCard(card, { + label: meta.name, + onPrimary: () => vscode.postMessage({ type: 'insert', expression: meta.expression }), + onCopy: () => vscode.postMessage({ type: 'copy', expression: meta.expression }), + }); return card; } @@ -101,6 +103,7 @@ if (ok) shown++; } updateCount(); + ThumbnailActions.refreshTabStop(grid); emptyEl.style.display = 'none'; emptyEl.textContent = ''; if (metas.length === 0) { diff --git a/scripts/test_asset_swap_picker.js b/scripts/test_asset_swap_picker.js index 38a570a9..37c39b6a 100755 --- a/scripts/test_asset_swap_picker.js +++ b/scripts/test_asset_swap_picker.js @@ -46,7 +46,10 @@ const flush = w => new Promise(r => w.setTimeout(r, 0)); const card = name => d.querySelector(`.card[data-name="${name}"]`); assert.strictEqual(card('1.png').querySelectorAll('.actions button').length, 3, 'cards expose open/swap/delete only'); assert.strictEqual(card('1.png').querySelector('[data-action="edit"]'), null, 'edit button is removed'); + // 单击只选中;双击(或 Enter)才打开标注编辑器 card('1.png').dispatchEvent(new w.MouseEvent('click', { bubbles:true })); + assert.notStrictEqual(sent.at(-1)?.type, 'openAnnotation', 'single click only selects'); + card('1.png').dispatchEvent(new w.MouseEvent('dblclick', { bubbles:true })); assert.deepStrictEqual(sent.at(-1), { type:'openAnnotation', imagePath:'C:/p/1.png' }); card('1.png').querySelector('[data-action="open"]').click(); assert.deepStrictEqual(sent.at(-1), { type:'openSource', imagePath:'C:/p/1.png' }); diff --git a/scripts/test_thumbnail_actions.js b/scripts/test_thumbnail_actions.js index af82e7a8..70c6f83b 100644 --- a/scripts/test_thumbnail_actions.js +++ b/scripts/test_thumbnail_actions.js @@ -64,40 +64,37 @@ for (const { mode, resource } of previewCases) { assert.strictEqual(buttons.length, 3, `${mode} preview exposes insert, copy and view`); assert(buttons.every(button => button.title && button.getAttribute('aria-label') === button.title)); - const click = detail => card.dispatchEvent(new w.MouseEvent('click', { bubbles: true, detail })); - click(1); - flush(); - assert.strictEqual(sent.length, 1); - assert.strictEqual(sent[0].type, 'insert'); - assert.strictEqual(sent[0].expression, resource.expression); + const grid = w.document.getElementById('grid'); + assert.strictEqual(grid.getAttribute('role'), 'listbox', 'resource grid is a listbox'); + assert.strictEqual(card.getAttribute('role'), 'option', 'cards are listbox options'); + assert.strictEqual(card.tabIndex, 0, 'the only card is the keyboard entry point'); - sent.length = 0; - click(1); click(2); click(3); - card.dispatchEvent(new w.MouseEvent('dblclick', { bubbles: true })); + const key = (k, extra = {}) => card.dispatchEvent(new w.KeyboardEvent('keydown', { key: k, bubbles: true, ...extra })); + // 单击 = 选中,无副作用(不再有 500ms 延迟插入) + card.dispatchEvent(new w.MouseEvent('click', { bubbles: true, detail: 1 })); flush(); - assert.strictEqual(sent.length, 1, 'double-click never inserts before copying'); - assert.strictEqual(sent[0].type, 'copy'); - assert.strictEqual(sent[0].expression, resource.expression); + assert.strictEqual(sent.length, 0, 'single click only selects'); + assert(card.classList.contains('is-selected') && card.getAttribute('aria-selected') === 'true', 'single click selects the card'); - sent.length = 0; - // A native double-click arriving after the gesture window cannot turn an already committed click into a copy. - click(1); flush(); - click(2); + // 双击 / Enter = 插入 card.dispatchEvent(new w.MouseEvent('dblclick', { bubbles: true })); - flush(); - assert.strictEqual(sent.length, 2, 'expired clicks commit as independent single gestures'); - assert(sent.every(message => message.type === 'insert'), 'a late native dblclick cannot copy an already committed gesture'); + assert.deepStrictEqual(sent.splice(0), [{ type: 'insert', expression: resource.expression }], 'double-click inserts'); + key('Enter'); + assert.deepStrictEqual(sent.splice(0), [{ type: 'insert', expression: resource.expression }], 'Enter inserts'); + // Ctrl+C = 复制 + key('c', { ctrlKey: true }); + assert.deepStrictEqual(sent.splice(0), [{ type: 'copy', expression: resource.expression }], 'Ctrl+C copies the reference'); + key('Delete'); + assert.strictEqual(sent.length, 0, 'resource preview has no delete gesture'); - sent.length = 0; - click(1); buttons[2].click(); + // 卡内按钮只做自己的事,不触发卡片手势 + buttons[2].click(); buttons[2].dispatchEvent(new w.MouseEvent('dblclick', { bubbles: true })); - flush(); - assert.strictEqual(sent.length, 1, 'source button cancels pending insertion and never copies the card'); + assert.strictEqual(sent.length, 1, 'source button never triggers the card gesture'); assert.strictEqual(sent[0].type, 'open'); - sent.length = 0; - click(1); flush(); - assert.strictEqual(sent.length, 1, 'a direct button clears pending gesture state'); + buttons[1].click(); + assert.deepStrictEqual(sent.splice(0), [{ type: 'copy', expression: resource.expression }]); dom.window.close(); } @@ -108,7 +105,18 @@ const card = w.document.querySelector('.card'); assert.strictEqual(card.querySelectorAll('.thumbnail-actions button').length, 3, 'asset card exposes only source, swap and delete actions'); assert.strictEqual(card.querySelector('[data-action="edit"]'), null, 'clicking the card itself is the annotation edit action'); card.dispatchEvent(new w.MouseEvent('click', { bubbles: true })); +assert.strictEqual(sent.length, 0, 'single click on an asset card only selects it'); +card.dispatchEvent(new w.MouseEvent('dblclick', { bubbles: true })); +assert.deepStrictEqual(sent.pop(), { type: 'openAnnotation', imagePath: 'source.png' }); +card.dispatchEvent(new w.KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); assert.deepStrictEqual(sent.pop(), { type: 'openAnnotation', imagePath: 'source.png' }); +card.dispatchEvent(new w.KeyboardEvent('keydown', { key: 'e', bubbles: true })); +assert.deepStrictEqual(sent.pop(), { type: 'openAnnotation', imagePath: 'source.png' }); +card.dispatchEvent(new w.KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); +assert.deepStrictEqual(sent.pop(), { type: 'deleteImage', imagePath: 'source.png' }); +const deleteButton = card.querySelector('[data-action="delete"]'); +assert(deleteButton.closest('.thumbnail-actions--danger'), 'delete sits apart from view/swap actions'); +assert(!card.querySelector('[data-action="open"]').closest('.thumbnail-actions--danger'), 'view action is not in the danger slot'); card.querySelector('[data-action="open"]').click(); assert.deepStrictEqual(sent.pop(), { type: 'openSource', imagePath: 'source.png' }); assert.strictEqual(sent.length, 0, 'direct asset actions never trigger a second card action'); @@ -120,4 +128,21 @@ assert.strictEqual(w.document.querySelector('.card'), card, 'annotation refresh assert.strictEqual(card.querySelector('.cats').textContent, 'screen.updated', 'annotation metadata updates in place'); dom.window.close(); + +// 键盘:方向键 / Home / End 在网格内移动选中,Tab 入口只有一个 +{ + const many = ['a.png', 'b.png', 'c.png'].map(name => ({ name, imagePath: name, width: 1, height: 1, categories: [], annotations: 0 })); + const { dom: d2, w: w2 } = gallery('templateAssetPanel', { type: 'templates', templates: many }); + const cards = [...w2.document.querySelectorAll('.card')]; + assert.deepStrictEqual(cards.map(c => c.tabIndex), [0, -1, -1], 'one roving tab stop'); + const press = (el, k) => el.dispatchEvent(new w2.KeyboardEvent('keydown', { key: k, bubbles: true })); + press(cards[0], 'ArrowRight'); + assert(cards[1].classList.contains('is-selected') && w2.document.activeElement === cards[1], 'ArrowRight moves to the next card'); + press(cards[1], 'End'); + assert(cards[2].classList.contains('is-selected'), 'End jumps to the last card'); + press(cards[2], 'Home'); + assert(cards[0].classList.contains('is-selected'), 'Home jumps to the first card'); + assert.deepStrictEqual(cards.map(c => c.tabIndex), [0, -1, -1], 'tab stop follows the selection'); + d2.window.close(); +} console.log('thumbnail action parity: unified template/rect/point preview and asset actions OK'); From c2b24f31e4a82002d32e4b365a5fb1ec96669017 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:26:24 +0800 Subject: [PATCH 11/29] feat(console): move the character & skill manager entry to the Config segment It is project data, not part of the game connection; the entry stays hidden unless okScriptToolkit.hasCharacterData is true. --- media/console/console.js | 2 +- media/console/index.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/media/console/console.js b/media/console/console.js index 0e26761a..011ad974 100644 --- a/media/console/console.js +++ b/media/console/console.js @@ -829,7 +829,7 @@ post({ type: 'setOverlay', enabled: $('overlayToggle').checked }); }); const characterButton = $('openCharacterManager'); - characterButton.textContent = t('toolboxOpenCharacterManager'); + OkIcons.label(characterButton, 'person', t('toolboxOpenCharacterManager')); characterButton.addEventListener('click', () => post({ type: 'openCharacterManager' })); // 抽屉 diff --git a/media/console/index.html b/media/console/index.html index 5a1bba8c..debc05c7 100644 --- a/media/console/index.html +++ b/media/console/index.html @@ -102,6 +102,8 @@ @@ -132,7 +134,6 @@ - From 963654f599060097246894fd3affcc37b100adf3 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:27:13 +0800 Subject: [PATCH 12/29] docs: README (zh/en) and parity audit for the P0 UI polish Canonical names, new commands, status bar, inline errors, gestures, swap undo, settings categories; parity audit flags the gesture change as pending on the JetBrains side. --- README.en.md | 66 +++++++++++++++++------------ README.md | 62 +++++++++++++++------------ docs/interaction-parity-audit.en.md | 2 +- docs/interaction-parity-audit.md | 2 +- 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/README.en.md b/README.en.md index 534aca8a..88bce059 100644 --- a/README.en.md +++ b/README.en.md @@ -8,7 +8,7 @@ **Bring ok-script's language keys, OCR fixes, templates, skills and task data straight into your VS Code workflow.** -Language key completion · OCR fix hints · Template browsing · Task launching · Character skill management +Language key completion · OCR fix hints · Resource preview & annotation · Console · Character skill management [![VS Code Marketplace](https://img.shields.io/badge/VS%20Code%20Marketplace-ok--script%20Toolkit-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=AliceJump.ok-script-toolkit) [![JetBrains Marketplace](https://img.shields.io/badge/JetBrains%20Marketplace-ok--script%20Toolkit-000000?logo=jetbrains&logoColor=white)](https://plugins.jetbrains.com/plugin/34091-ok-script-toolkit) @@ -23,7 +23,7 @@ Language key completion · OCR fix hints · Template browsing · Task launching --- -A VS Code extension that brings ok-script language keys, OCR fixes, templates, skill effects, and task data directly into your Python development workflow. It also includes built-in template browsing, task launching, and character skill management panels. +A VS Code extension that brings ok-script language keys, OCR fixes, templates, skill effects, and task data directly into your Python development workflow. It also includes built-in Resource Preview, Annotation Management, Console, and Character & Skill Manager panels. > [!TIP] > The demo images at the beginning of each feature section below are clickable — if the GIFs don't load, click the links to view them. @@ -53,7 +53,7 @@ See [feature and documentation parity](docs/feature-parity.en.md) for current ho | [Code development assistance](#code-development-assistance) | Complete and explain `self.lang`, OCR regex, and skill effect IDs in the editor | | [Template management](#template-management) | Browse templates in a grid, click to insert `fL.`, with thumbnail preview | | [Temp screenshots](#temp-screenshots) | 10-slot scratch area in the activity bar — box-select to copy normalized coordinates | -| [Task launcher](#task-launcher) | Auto-generate parameter forms from `config.py` and run tasks, without writing to the target project's config | +| [Console](#console-task-running) | Auto-generate parameter forms from `config.py` and run tasks, without writing to the target project's config | | [Character skill management](#character-skill-management) | Visual management and diagnostics for characters / skills / effects / enhancement groups | | [Multi-language support](#multi-language-support) | UI supports 6 languages; data hints follow the target project | @@ -77,15 +77,16 @@ When editing Python code, the extension automatically detects ok-script-specific ### Template Management

- Template panel: search, thumbnail preview, and quick insert + Resource Preview: search, thumbnail preview, and quick insert

-- **Template panel**: Open via the sidebar icon or `Ctrl+Alt+T` shortcut (requires focus on a Python editor). Displays all workspace templates in a thumbnail grid with real-time name search and filtering. -- **Quick insert**: Click a card to insert `fL.` at the editor cursor, double-click to copy to clipboard, or use **View Original** to open the source. VS Code template and box galleries use a shared 500 ms card double-click window; clicks beyond that window are independent single clicks. The insert, copy and source buttons act immediately. +- **Resource Preview**: Open via the sidebar icon or `Ctrl+Alt+T` shortcut (requires focus on a Python editor). Displays published template / box / point resources in a thumbnail grid with real-time name search and filtering. When there is nothing to show, it lists the COCO paths it actually checked and offers an **Open Annotation Management** button. +- **Quick insert**: Thumbnail cards behave the same in Resource Preview, Annotation Management and Temp Screenshots — **click = select**, **double-click / Enter = primary action** (in Resource Preview: insert `fL.` at the editor cursor), `Ctrl+C` copies the reference; the insert / copy / view-original buttons on the card act immediately. Cards are reachable with Tab and move with the arrow keys / Home / End. - **Template code hints**: Type `fL.` or `FeatureList.` to complete template names with size info. Hover shows thumbnail preview, dimensions, and source info. -- You can also open a larger grid view in the editor area via the command **ok-script Toolkit: Open Template Panel in Editor**. -- Supports both sidebar (template panel and template asset views) and large editor window browsing modes. -- **Swap annotations**: in the template assets panel (annotation manager), the **⇄** button at the bottom-right of a thumbnail card swaps that image's whole annotation set with another image in the same list — the fix for "annotated the wrong image / images are in the wrong order". The target is picked from thumbnails (template names are mostly numeric, so names alone are not enough). When the two images differ in size, box coordinates are **scaled proportionally** and the confirmation dialog spells out both sizes. The swap is written in one go and **cannot be undone**, so it always asks before saving. +- You can also open a larger grid view in the editor area via the command **ok-script Toolkit: Open Resource Preview in Editor**. +- Supports both sidebar (Resource Preview and Annotation Management views) and large editor window browsing modes. +- **Annotation Management**: double-click / Enter / `E` opens the annotation editor, `Delete` deletes (after a confirmation). The delete button sits alone in the card's top-right corner, apart from view / swap. The **Open Annotation Editor…** command lets you pick an image directly. +- **Swap annotations**: in Annotation Management, the **Swap annotations** button (codicon `arrow-swap`) at the bottom-right of a thumbnail card swaps that image's whole annotation set with another image in the same list — the fix for "annotated the wrong image / images are in the wrong order". The target is picked from thumbnails (template names are mostly numeric, so names alone are not enough). When the two images differ in size, box coordinates are **scaled proportionally** and the confirmation dialog spells out both sizes. The swap is written in one go and always asks before saving; the completion notification has an **Undo** button that restores both images' original annotations as long as neither image changed since. ```python self.wait_click_feature(feature=fL.give_gift, time_out=10) @@ -110,20 +111,20 @@ Box resource management reuses source annotation images and the COCO editor. Edi | Enqueue | `Ctrl+V` to paste a clipboard image, click "Screenshot" to capture the game window, or drag image files in directly | | 0.1s carousel | Cycles through all screenshots at 100ms intervals, useful for observing buttons with movement boundaries | | Box-select copy | In "Box Select Coords" mode, box-select to copy `x,y,tox,toy` normalized coordinates to the clipboard | -| Import to annotation | The **→** button on the top-right of a thumbnail card imports that shot into the annotation manager (copies into `ok_templates` and registers COCO) | +| Import to annotation | The **Add to Annotation Management** button (codicon `arrow-right`) at the bottom-right of a thumbnail card imports that shot into Annotation Management (copies into `ok_templates` and registers COCO); delete is in the top-right corner | Details: - The carousel and box-select are independent — the carousel keeps playing during box-select, and the selection box stays in place after release, making it easy to compare and fine-tune repeatedly. - Normalized coordinates are scaled to 0..1 based on image width/height, with 4 decimal places. Scroll zoom and middle-click/blank-area drag panning are also supported. - After box-select, the **box remains and can be adjusted** (with 8-directional handles and full drag support, same interaction as the template annotation box): **coordinates are re-copied on creation and after each adjustment**; the box itself is never written to annotation data or saved to disk. Click outside the box (beyond the frame and handles) to clear it. -- Note: Cross-Webview drag-and-drop is not available in VS Code (each webview is an iframe with a different origin, `dataTransfer` is blocked), so the **→** button is the reliable entry point for import. +- Note: Cross-Webview drag-and-drop is not available in VS Code (each webview is an iframe with a different origin, `dataTransfer` is blocked), so the **Add to Annotation Management** button is the reliable entry point for import. - The **annotation editor** also supports this mode: click "Coords (C)" in the toolbar or press `C` to enter box-select mode, copying the same `x,y,tox,toy` normalized coordinates without creating an annotation box or modifying COCO. The resulting box also has handles for fine-tuning, with coordinates re-copied after each adjustment; clicking outside the box in non-interactive areas clears it. Override with `copyCoords` in `okScriptToolkit.annotationKeybindings`. -### Task Launcher +### Console (Task Running)

- Task launcher: parameter form and run log + Console: parameter form and run log

- Automatically parses all one-time tasks and trigger tasks from the target project's `src/config.py` / `config.py`, generating complete parameter configuration forms. @@ -137,7 +138,10 @@ Details: - **No target-project config pollution**: While the executor runs, all ok-framework reads/writes to `configs/` and screenshots are redirected into the workspace sandbox `.vscode/ok-script-toolkit/`, so **the target project's config files and screenshots stay untouched** (`devices.json` is bridged by copy so the game connection is reused). Tweak parameters freely while debugging without dirtying the project. - The executor can be paused/resumed at any time (globally suspending polling and tasks), and you can "stop the current task" without closing the executor. Run logs are output to a dedicated output channel. - **Global config takeover**: the target project's global config groups (visible groups of the ok framework's GlobalConfig) are parsed by the probe as well and shown as parameter snapshots in the console's "Config" segment — edits save instantly and can be reset to defaults; changes are pushed live to the running executor via `gparams`, and the full snapshot is injected as `OK_TOOLKIT_GCONFIG` at executor launch. Still sandbox-only — the target project's config is never touched. -- **Console view**: the task launcher and toolbox live together in the "ok-script console" with four segments (Tasks / Game / Config / Accounts); a health bar in the header reflects executor state in real time, and when idle the detail panel shows the run center (current task / execution queue / trigger polling). Task rows reveal a read-only summary on hover. +- **Console view**: task running and the game connection live together in the **Console** with four segments (Tasks / Game / Config / Accounts); a health bar in the header reflects executor state in real time, and when idle the detail panel shows the run center (current task / execution queue / trigger polling). Task rows reveal a read-only summary on hover. In a narrow sidebar the segment navigation collapses to an icon rail (≤640px) or a top segmented bar (≤480px). +- **Status bar and commands**: the status bar shows the executor state (idle / starting / running task + queued count / paused) and opens the Console on click. The Console view title bar and the Command Palette offer Run Task… / Start / Pause / Resume / Stop Current Task / Stop Executor / Show Logs / Connect Game, shown or hidden by executor state. The number of running + queued tasks appears as the view badge. +- **Errors in place**: a missing project path, a missing Python interpreter or an executor crash is shown in the Console banner with next-step buttons (Set Project Path / Set Python Path / Show Logs / Retry). Only when the Console is hidden does it fall back to a notification (with the same buttons). +- The **Character & Skill Manager** entry lives in the Config segment. It is hidden, together with its Command Palette command, when no character data is detected (neither `characterMasterFile` nor `characterSkillsDirectory` exists). ### Character Skill Management @@ -152,8 +156,8 @@ Run the command **ok-script Toolkit: Open Character Skill Management Panel** to ### Multi-language Support -- The extension UI (notifications, output channel, hover, template panel, task launcher, toolbox, and character skill management panel) supports Simplified Chinese, Traditional Chinese, English, Japanese, Korean, and Spanish, defaulting to the VS Code display language. -- The sidebar is split into three independent activity bar containers: **ok-script Toolbox** (toolbox + task launcher), **ok-script Templates** (template panel + template assets), and **ok-script Temp Shots**. +- The extension UI (notifications, output channel, hover, Resource Preview, Annotation Management, Console, and the Character & Skill Manager) supports Simplified Chinese, Traditional Chinese, English, Japanese, Korean, and Spanish, defaulting to the VS Code display language. +- The sidebar is split into three independent activity bar containers: **ok-script Tools** (Console), **ok-script Resources** (Resource Preview + Annotation Management), and **ok-script Temp Shots**. UI icons use VS Code codicons throughout. - Language data in code hints always uses the target project's own locale and original protocol values, unaffected by the plugin UI language. > [!NOTE] @@ -184,6 +188,8 @@ See [DEVELOPMENT.en.md](DEVELOPMENT.en.md) for project structure, JetBrains vers ## Configuration +In the Settings UI the options are grouped into 5 categories: **ok-script: Code Hints**, **Resources & Annotation**, **Screenshots**, **Console & Tasks**, and **Character Data**. + | Setting | Default | Description | |---|---|---| | `okScriptToolkit.langDirectory` | `assets/lang` | lang JSON directory (relative to workspace root) | @@ -196,15 +202,15 @@ See [DEVELOPMENT.en.md](DEVELOPMENT.en.md) for project structure, JetBrains vers | `okScriptToolkit.labelEnumPath` | empty | Path of the generated `LabelEnum.py` (relative to workspace root, with or without `.py`); empty follows the project convention. Maps to `labelEnum.path` | | `okScriptToolkit.labelEnumName` | empty | Class name of the generated enum; empty derives it from the file name. Maps to `labelEnum.name`. ⚠️ Project code imports this class by name, so renaming it breaks those imports — you will be asked to confirm before an existing file is overwritten | | `okScriptToolkit.effectsFile` | `src/data/effects.py` | Skill effect ID definition file (`EffectType` enum + `EFFECT_DESCRIPTIONS`), relative to workspace root | -| `okScriptToolkit.okScriptProjectPath` | empty | ok-script project root for the task launcher; tries the current workspace when empty | -| `okScriptToolkit.okScriptPython` | empty | Python for the task launcher; prefers `/.venv/Scripts/python.exe` when empty | +| `okScriptToolkit.okScriptProjectPath` | empty | ok-script project root used by the Console to run tasks; tries the current workspace when empty | +| `okScriptToolkit.okScriptPython` | empty | Python used by the Console; prefers `/.venv/Scripts/python.exe` when empty | | `okScriptToolkit.captureMethod` | `auto` | Game window capture method: `auto` / `wgc` / `bitblt` / `foreground` | | `okScriptToolkit.characterProjectPath` | empty | Data project for the character skill management panel; uses `okScriptProjectPath` or current workspace when empty | | `okScriptToolkit.characterMasterFile` | `assets/data/characters.json` | Character master table JSON | | `okScriptToolkit.characterSkillsDirectory` | `assets/data/character_skills` | Character skills JSON directory | | `okScriptToolkit.characterLocaleFile` | `assets/lang/characters.json` | Character name multi-language JSON | | `okScriptToolkit.characterAvatarTemplateRegex` | `^battle[_-]?icon[_-]?` | Character avatar template name regex; uses the first capture group if present, otherwise the name after the matched prefix, matched against the character master table's English slug; defaults to `battleicon`, `battle_icon`, and `battle-icon` prefixes | -| `okScriptToolkit.okTemplatesDirectory` | `ok_templates` | ok_templates folder name (relative to workspace root), used by the template asset manager | +| `okScriptToolkit.okTemplatesDirectory` | `ok_templates` | ok_templates folder name (relative to workspace root), used by Annotation Management | | `okScriptToolkit.annotationKeybindings` | see defaults | Annotation editor keyboard shortcuts; values are key names with modifier prefixes like `ctrl+z` | ### Project Convention File `ok-script-toolkit.json` @@ -284,15 +290,19 @@ Command categories display as "ok-script 工具箱" / "ok-script Toolkit" depend | Command | Shortcut | Description | |---|---|---| -| `ok-script 工具箱: Open Template Gallery` | `Ctrl+Alt+T` (macOS `Cmd+Alt+T`, requires Python editor focus) | Focus the template sidebar view in the activity bar | -| `ok-script 工具箱: Open Template Gallery in Editor` | — | Open a large grid view in the editor area | -| `ok-script 工具箱: Open Task Launcher` | — | Focus the task launcher view in the activity bar | -| `ok-script 工具箱: Open Character & Skill Manager` | — | Open the character, skill, effect, enhancement group, and name localization management page | -| `ok-script 工具箱: Open Template Assets` | — | Open the template asset management panel in the editor area | -| `ok-script 工具箱: Open Annotation Editor` | — | Prompts to click an image in the template asset panel to enter the COCO annotation editor (the command itself does not open the editor directly) | -| `ok-script 工具箱: Open Temp Screenshots` | — | Focus the temp screenshots view in the activity bar | -| `ok-script 工具箱: Screenshot to Template Assets` | `Ctrl+Alt+S` (macOS `Cmd+Alt+S`) | Open the template asset panel and take a screenshot immediately. Reuses the panel's own screenshot action; the shot is registered into COCO. Rebindable in Keyboard Shortcuts | -| `ok-script 工具箱: Project Convention vs My Settings` | — | List the settings that take part in the precedence chain and show **which layer each effective value comes from** (my settings / project convention / built-in default). Rows you have overridden carry a Revert button that drops your override and goes back to the project convention. Exists to fix "once I changed it, I can never see what the team changed" | +| `ok-script Toolkit: Open Resource Preview` | `Ctrl+Alt+T` (macOS `Cmd+Alt+T`, requires Python editor focus) | Focus the Resource Preview view in the activity bar | +| `ok-script Toolkit: Open Resource Preview in Editor` | — | Open a large grid view in the editor area | +| `ok-script Toolkit: Open Console` | — | Focus the Console view in the activity bar | +| `ok-script Toolkit: Console: Run Task…` | — | Pick a loaded one-time task and queue it (starts the executor first if needed) | +| `ok-script Toolkit: Console: Start / Pause / Resume Executor, Stop Current Task, Stop Executor` | — | Same logic as the Console buttons; shown in the Command Palette and view title bar according to executor state | +| `ok-script Toolkit: Console: Refresh Tasks / Show Logs / Connect Game / Disconnect Game` | — | Re-parse the task list, open the output channel, connect or disconnect the game window | +| `ok-script Toolkit: Open Character & Skill Manager` | — | Open the character, skill, effect, enhancement group, and name localization management page; hidden in the Command Palette when no character data is detected | +| `ok-script Toolkit: Open Annotation Management` | — | Open Annotation Management in the editor area | +| `ok-script Toolkit: Open Annotation Editor…` | — | Pick an image from Annotation Management (with annotation counts) and open the COCO annotation editor directly | +| `ok-script Toolkit: Publish Resources…` | — | Open Annotation Management and go straight into publishing: choose Template / Rect / Point, then publish | +| `ok-script Toolkit: Open Temp Screenshots` | — | Focus the temp screenshots view in the activity bar | +| `ok-script Toolkit: Screenshot to Annotation Management` | `Ctrl+Alt+S` (macOS `Cmd+Alt+S`) | Open Annotation Management and take a screenshot immediately. Reuses the panel's own screenshot action; the shot is registered into COCO. Rebindable in Keyboard Shortcuts | +| `ok-script Toolkit: Project Convention vs My Settings` | — | List the settings that take part in the precedence chain and show **which layer each effective value comes from** (my settings / project convention / built-in default). Rows you have overridden carry a Revert button that drops your override and goes back to the project convention. Exists to fix "once I changed it, I can never see what the team changed" | ## Update Not Taking Effect diff --git a/README.md b/README.md index 8bfdaa4c..99b676ea 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **把 ok-script 的语言、OCR、模板、技能和任务数据,直接搬进 VS Code 的开发流程。** -语言键补全 · OCR 修正提示 · 模板浏览 · 任务启动 · 角色技能管理 +语言键补全 · OCR 修正提示 · 资源预览与标注 · 控制台 · 角色技能管理 [![VS Code Marketplace](https://img.shields.io/badge/VS%20Code%20Marketplace-ok--script%20Toolkit-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=AliceJump.ok-script-toolkit) [![JetBrains Marketplace](https://img.shields.io/badge/JetBrains%20Marketplace-ok--script%20Toolkit-000000?logo=jetbrains&logoColor=white)](https://plugins.jetbrains.com/plugin/34091-ok-script-toolkit) @@ -23,7 +23,7 @@ --- -VS Code 扩展,为 ok-script 项目的 Python 开发提供语言键、OCR 修正、模板和技能效果的数据提示,同时内置模板浏览、任务启动和角色技能管理面板,让 ok-script 的语言、OCR、模板、技能和任务数据直接进入开发流程。 +VS Code 扩展,为 ok-script 项目的 Python 开发提供语言键、OCR 修正、模板和技能效果的数据提示,同时内置资源预览、标注管理、控制台和角色技能管理面板,让 ok-script 的语言、OCR、模板、技能和任务数据直接进入开发流程。 > [!TIP] > 下面每个功能章节开头的演示图都是可点的——如果动图没加载出来,直接点开链接看。 @@ -53,7 +53,7 @@ PyCharm / IntelliJ IDEA 用户请装 JetBrains 版:[ok-script Toolkit for JetB | [代码开发辅助](#代码开发辅助) | 在编辑器内补全和解释 `self.lang`、OCR 正则与技能效果 ID | | [模板管理](#模板管理) | 网格浏览模板,单击插入 `fL.<名称>`,支持缩略图预览 | | [临时截图](#临时截图) | 活动栏里的 10 张暂存区,框选即可复制归一化坐标 | -| [任务启动](#任务启动) | 从 `config.py` 自动生成参数表单并运行任务,全程不写目标项目配置 | +| [控制台](#控制台任务运行) | 从 `config.py` 自动生成参数表单并运行任务,全程不写目标项目配置 | | [角色技能管理](#角色技能管理) | 角色 / 技能 / 效果 / 强化组的可视化管理与诊断 | | [多语言支持](#多语言支持) | 界面支持 6 种语言,数据提示跟随目标项目 | @@ -77,15 +77,16 @@ PyCharm / IntelliJ IDEA 用户请装 JetBrains 版:[ok-script Toolkit for JetB ### 模板管理

- 模板面板:搜索、缩略图预览与快速插入 + 资源预览:搜索、缩略图预览与快速插入

-- **模板面板**:通过侧边栏图标或 `Ctrl+Alt+T` 快捷键(需聚焦 Python 编辑器时生效)打开,网格展示工作区全部模板的缩略图,支持按名称实时搜索和过滤。 -- **快速插入**:单击卡片将 `fL.<模板名>` 插入编辑器光标处,双击复制到剪贴板,使用「查看原图」按钮打开来源。VS Code 模板与框图库统一使用 500 ms 的卡片双击窗口;超过窗口的点击分别按单击处理。插入、复制和查看原图按钮立即执行。 +- **资源预览**:通过侧边栏图标或 `Ctrl+Alt+T` 快捷键(需聚焦 Python 编辑器时生效)打开,网格展示已发布的模板 / 框 / 点资源缩略图,支持按名称实时搜索和过滤。没有资源时会列出实际检查过的 COCO 路径,并提供「打开标注管理」按钮。 +- **快速插入**:缩略图卡片的手势在资源预览、标注管理、临时截图三处一致——**单击 = 选中**,**双击 / Enter = 主操作**(资源预览里是把 `fL.<模板名>` 插入编辑器光标处),`Ctrl+C` 复制引用;卡片右下角的「插入 / 复制 / 查看原图」按钮立即执行。卡片可用 Tab 进入、方向键 / Home / End 移动。 - **模板代码提示**:输入 `fL.` 或 `FeatureList.` 补全模板名称并显示尺寸,悬停显示缩略图预览、尺寸和来源信息。 -- 也可通过命令 **ok-script 工具箱: 在编辑器中打开模板面板** 在编辑器区打开更大的网格视图。 -- 支持侧边栏(模板面板和模板素材两个视图)和编辑器大窗口两种浏览方式。 -- **交换标注**:在模板素材面板(标注管理)里,缩略图卡片右下角的 **⇄** 按钮可以把这张图与同一列表内另一张图的标注**整套互换** —— 修"标错了图 / 图片顺序反了"用。选择目标时按缩略图挑(模板名多是数字序号,光看名字选不出来)。两张图尺寸不同时,框坐标会**按比例缩放**,并在确认框里写明缩放前后的尺寸;交换是整体写盘、**不可撤销**的,所以落盘前一定会先确认。 +- 也可通过命令 **ok-script 工具箱: 在编辑器中打开资源预览** 在编辑器区打开更大的网格视图。 +- 支持侧边栏(「资源预览」和「标注管理」两个视图)和编辑器大窗口两种浏览方式。 +- **标注管理**:双击 / Enter / `E` 打开标注编辑器,`Delete` 删除(会先确认);删除按钮单独放在卡片右上角,与查看 / 交换分开。也可以用命令 **打开标注编辑器…** 从图片列表里直接选图。 +- **交换标注**:在标注管理里,缩略图卡片右下角的「交换标注」按钮(codicon `arrow-swap`)可以把这张图与同一列表内另一张图的标注**整套互换** —— 修"标错了图 / 图片顺序反了"用。选择目标时按缩略图挑(模板名多是数字序号,光看名字选不出来)。两张图尺寸不同时,框坐标会**按比例缩放**,并在确认框里写明缩放前后的尺寸。交换是整体写盘,落盘前一定会先确认;完成后的通知里有「撤销」按钮,只要两张图在此之后没被改过,就能一键恢复原来的标注。 ```python self.wait_click_feature(feature=fL.give_gift, time_out=10) @@ -110,20 +111,20 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) | 入列 | `Ctrl+V` 粘贴剪贴板图片、点「截屏」截取游戏窗口,或直接把图片文件拖进来 | | 0.1s 轮播 | 按 100ms 间隔循环播放全部截图,方便观察有移动界限的按钮等目标 | | 框选复制 | 在「框选坐标」模式下框选,把 `x,y,tox,toy` 归一化坐标复制到剪贴板 | -| 导入标注 | 缩略图卡片右上角的 **→** 按钮把该张导入「标注管理」(即复制进 `ok_templates` 并登记 COCO) | +| 导入标注 | 缩略图卡片右下角的「添加到标注管理」按钮(codicon `arrow-right`)把该张导入「标注管理」(即复制进 `ok_templates` 并登记 COCO);删除按钮在右上角 | 细节说明: - 轮播与框选相互独立——框选期间轮播不停,选框松手后保留在原位,便于反复比对微调。 - 归一化坐标按图片宽高归一化到 0..1,保留 4 位小数,同样支持滚轮缩放、中键/空白拖拽平移。 - 框选完成后**框会保留并可继续调整**(带 8 向手柄、可整体拖动,交互与模板标注框一致):**创建时与每次调整结束都会重新复制一次当前框的坐标**;框本身不写入标注数据、不落盘。点击图片的非交互部分(框体与手柄之外)即清除。 -- 注:VS Code 的跨 Webview 拖拽不可用(各 webview 是不同 origin 的 iframe,`dataTransfer` 被屏蔽),所以 **→** 按钮是导入的可靠入口。 +- 注:VS Code 的跨 Webview 拖拽不可用(各 webview 是不同 origin 的 iframe,`dataTransfer` 被屏蔽),所以「添加到标注管理」按钮是导入的可靠入口。 - **标注编辑器**同样支持该模式:工具栏「坐标 (C)」或按 `C` 键后框选,直接复制同样的 `x,y,tox,toy` 归一化坐标,不会创建标注框、不改动 COCO。得到的框同样带手柄可继续微调,每次调整结束重新复制;点击图片非交互部分清除。快捷键可用 `okScriptToolkit.annotationKeybindings` 的 `copyCoords` 覆盖。 -### 任务启动 +### 控制台(任务运行)

- 任务启动:参数表单与运行日志 + 控制台:参数表单与运行日志

- 从目标项目的 `src/config.py` / `config.py` 自动解析所有一次性任务和触发任务,生成完整的参数配置表单。 @@ -138,7 +139,10 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) - **不污染目标项目配置**:执行器运行时,ok 框架对 `configs/` 与截图的读写全部改道到工作区的沙箱目录 `.vscode/ok-script-toolkit/`,**目标项目的配置文件与截图全程保持原样**(`devices.json` 做桥接拷贝以复用游戏连接)。调试时可以放心地改参数试跑,不会弄脏项目。 - 执行器可随时暂停/恢复(全局挂起轮询与任务),也可以「停止当前任务」而不关闭执行器;运行日志输出到专属输出频道。 - **全局配置接管**:目标项目的全局配置组(ok 框架 GlobalConfig 的可见分组)由探针一并解析,在控制台「配置」分段以参数快照呈现——修改即保存、可一键恢复默认;改动经 `gparams` 即时推送给运行中的执行器,执行器启动时以 `OK_TOOLKIT_GCONFIG` 注入全量快照。同样只写沙箱、不碰目标项目配置。 -- **控制台视图**:任务启动器与工具箱统一在「ok-script 控制台」的四个分段(任务 / 游戏 / 配置 / 账号)里管理;顶栏健康度条实时反映执行器状态,空闲时详情面板显示运行中心(当前任务 / 执行队列 / 触发轮询),任务卡悬停弹出只读摘要。 +- **控制台视图**:任务运行与游戏连接统一在「控制台」的四个分段(任务 / 游戏 / 配置 / 账号)里管理;顶栏健康度条实时反映执行器状态,空闲时详情面板显示运行中心(当前任务 / 执行队列 / 触发轮询),任务卡悬停弹出只读摘要。侧边栏较窄时,分段导航会收成图标条(≤640px)或顶部分段条(≤480px)。 +- **状态栏与命令**:状态栏左侧显示执行器状态(空闲 / 启动中 / 运行中的任务 + 排队数 / 已暂停),点击打开控制台;控制台视图标题栏和命令面板提供「运行任务… / 启动 / 暂停 / 继续 / 停止当前任务 / 关闭执行器 / 查看日志 / 连接游戏」等命令,按执行器状态显隐。运行中 + 排队的任务数显示在视图徽标上。 +- **错误就地显示**:项目路径缺失、Python 不存在、执行器异常退出等错误显示在控制台横幅里,并带「设置项目路径 / 设置 Python 路径 / 查看日志 / 重试」等下一步按钮;控制台不可见时才退回通知(同样带按钮)。 +- **角色技能管理入口**在「配置」分段;未检测到角色数据(`characterMasterFile` / `characterSkillsDirectory` 都不存在)时隐藏,命令面板里的同名命令也一并隐藏。 ### 角色技能管理 @@ -157,8 +161,8 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) ### 多语言支持 -- 扩展界面(通知、输出频道、hover、模板面板、任务启动器、工具箱和角色技能管理面板)支持简体中文、繁体中文、英文、日文、韩文和西班牙文,默认跟随 VS Code 显示语言。 -- 侧边栏分为 **ok-script 工具**(工具箱 + 任务启动)、**ok-script 模板**(模板面板 + 模板素材)和 **ok-script 临时截图** 三个独立活动栏容器。 +- 扩展界面(通知、输出频道、hover、资源预览、标注管理、控制台和角色技能管理面板)支持简体中文、繁体中文、英文、日文、韩文和西班牙文,默认跟随 VS Code 显示语言。 +- 侧边栏分为 **ok-script 工具**(控制台)、**ok-script 资源**(资源预览 + 标注管理)和 **ok-script 临时截图** 三个独立活动栏容器。界面图标统一使用 VS Code codicons。 - 代码提示中的语言数据始终使用目标项目自身的 locale 和原始协议值,不受插件界面语言影响。 > [!NOTE] @@ -189,6 +193,8 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) ## 配置 +设置界面按用途分为 5 个分类:**ok-script:代码提示**、**资源与标注**、**截图**、**控制台与任务**、**角色数据**。 + | 配置项 | 默认值 | 说明 | |---|---|---| | `okScriptToolkit.langDirectory` | `assets/lang` | lang JSON 目录(相对工作区根) | @@ -201,15 +207,15 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) | `okScriptToolkit.labelEnumPath` | 空 | 生成 `LabelEnum.py` 的路径(相对目标项目根,带不带 `.py` 都可以);留空则跟随项目约定和 `config.py`。对应项目文件的 `labelEnum.path` | | `okScriptToolkit.labelEnumName` | 空 | 生成枚举的类名;留空则由文件名推导。对应项目文件的 `labelEnum.name`。⚠️ 项目代码按这个名字 import,改名会让那些 import 失效 —— 覆盖已有文件前会先请你确认 | | `okScriptToolkit.effectsFile` | `src/data/effects.py` | 技能效果 ID 定义文件(`EffectType` 枚举与 `EFFECT_DESCRIPTIONS`),相对工作区根目录 | -| `okScriptToolkit.okScriptProjectPath` | 空 | 任务启动器使用的 ok-script 项目根目录;为空时尝试使用当前工作区 | -| `okScriptToolkit.okScriptPython` | 空 | 任务启动器使用的 Python;为空时优先使用目标项目 `.venv/Scripts/python.exe` | +| `okScriptToolkit.okScriptProjectPath` | 空 | 控制台运行任务使用的 ok-script 项目根目录;为空时尝试使用当前工作区 | +| `okScriptToolkit.okScriptPython` | 空 | 控制台运行任务使用的 Python;为空时优先使用目标项目 `.venv/Scripts/python.exe` | | `okScriptToolkit.captureMethod` | `auto` | 游戏窗口截图方式:`auto` / `wgc` / `bitblt` / `foreground` | | `okScriptToolkit.characterProjectPath` | 空 | 角色技能管理面板的数据项目;为空时使用 `okScriptProjectPath` 或当前工作区 | | `okScriptToolkit.characterMasterFile` | `assets/data/characters.json` | 角色主表 JSON | | `okScriptToolkit.characterSkillsDirectory` | `assets/data/character_skills` | 角色技能 JSON 目录 | | `okScriptToolkit.characterLocaleFile` | `assets/lang/characters.json` | 角色名称多语言 JSON | | `okScriptToolkit.characterAvatarTemplateRegex` | `^battle[_-]?icon[_-]?` | 角色头像模板名正则;有捕获组时使用第一组,否则使用匹配前缀后的剩余名称,与角色主表英文 slug 匹配;默认兼容 `battleicon`、`battle_icon` 和 `battle-icon` 前缀 | -| `okScriptToolkit.okTemplatesDirectory` | `ok_templates` | ok_templates 文件夹名(相对工作区根),供模板素材管理器使用 | +| `okScriptToolkit.okTemplatesDirectory` | `ok_templates` | ok_templates 文件夹名(相对工作区根),供标注管理使用 | | `okScriptToolkit.annotationKeybindings` | 见默认值 | 标注编辑器的键盘快捷键;值为按键名,支持 `ctrl+z` 等修饰符前缀 | ### 项目约定文件 `ok-script-toolkit.json` @@ -296,14 +302,18 @@ self.wait_click_ocr(match=self.lang.zip_line_mixin.k_2f4f4a2f, ...) | 命令 | 快捷键 | 说明 | |---|---|---| -| `ok-script 工具箱: 打开模板面板` | `Ctrl+Alt+T`(macOS `Cmd+Alt+T`,需聚焦 Python 编辑器) | 聚焦活动栏中的模板侧边栏视图 | -| `ok-script 工具箱: 在编辑器中打开模板面板` | — | 在编辑器区打开大窗口网格视图 | -| `ok-script 工具箱: 打开任务启动` | — | 聚焦活动栏中的任务启动器视图 | -| `ok-script 工具箱: 打开角色技能管理面板` | — | 打开角色、技能、效果、强化组和名称本地化管理页 | -| `ok-script 工具箱: 打开模板素材` | — | 在编辑器区打开模板素材管理大窗口面板 | -| `ok-script 工具箱: 打开标注编辑器` | — | 提示在模板素材面板中点击图片以进入 COCO 标注编辑器(命令本身不直接打开编辑器) | +| `ok-script 工具箱: 打开资源预览` | `Ctrl+Alt+T`(macOS `Cmd+Alt+T`,需聚焦 Python 编辑器) | 聚焦活动栏中的资源预览视图 | +| `ok-script 工具箱: 在编辑器中打开资源预览` | — | 在编辑器区打开大窗口网格视图 | +| `ok-script 工具箱: 打开控制台` | — | 聚焦活动栏中的控制台视图 | +| `ok-script 工具箱: 控制台:运行任务…` | — | 从已加载的一次性任务里选一个入队(执行器未启动时会先启动) | +| `ok-script 工具箱: 控制台:启动执行器 / 暂停执行器 / 继续执行器 / 停止当前任务 / 关闭执行器` | — | 与控制台按钮同一套逻辑;按执行器状态在命令面板和视图标题栏里显隐 | +| `ok-script 工具箱: 控制台:刷新任务 / 查看日志 / 连接游戏 / 断开游戏` | — | 重新解析任务列表、打开输出频道、连接或断开游戏窗口 | +| `ok-script 工具箱: 打开角色技能管理` | — | 打开角色、技能、效果、强化组和名称本地化管理页;未检测到角色数据时在命令面板中隐藏 | +| `ok-script 工具箱: 打开标注管理` | — | 在编辑器区打开标注管理大窗口面板 | +| `ok-script 工具箱: 打开标注编辑器…` | — | 从标注管理的图片列表中选一张(显示标注数),直接打开 COCO 标注编辑器 | +| `ok-script 工具箱: 发布资源…` | — | 打开标注管理并直接进入发布流程:选择 Template / Rect / Point 后发布 | | `ok-script 工具箱: 打开临时截图` | — | 聚焦活动栏中的临时截图视图 | -| `ok-script 工具箱: 截图并打开模板素材` | `Ctrl+Alt+S`(macOS `Cmd+Alt+S`) | 打开模板素材管理面板并立即截图。复用面板自己的截图动作,截图会登记进 COCO。键位可在「键盘快捷方式」里改 | +| `ok-script 工具箱: 截图并打开标注管理` | `Ctrl+Alt+S`(macOS `Cmd+Alt+S`) | 打开标注管理面板并立即截图。复用面板自己的截图动作,截图会登记进 COCO。键位可在「键盘快捷方式」里改 | | `ok-script 工具箱: 项目约定 vs 我的设置` | — | 列出参与取值链的设置项,显示每一项的**生效值来自哪一层**(我的设置 / 项目约定 / 内置默认)。被个人设置覆盖过的项带一个「恢复」按钮,一键回到项目约定。用于解决"我改过一次就再也看不到团队改了什么" | ## 更新后不生效 diff --git a/docs/interaction-parity-audit.en.md b/docs/interaction-parity-audit.en.md index a2bb7cfc..1caa7c1f 100644 --- a/docs/interaction-parity-audit.en.md +++ b/docs/interaction-parity-audit.en.md @@ -27,7 +27,7 @@ Sources: [parent registration](../package.json), [VS Code task page](../media/co | 06 | Template / Rect / Point modes | Three adjacent buttons, with the active mode highlighted | Mode combo box | All modes are immediately visible on one side; the other requires opening a dropdown | | 07 | Insert, copy, source buttons | Three persistent buttons at the bottom right of every thumbnail, directly bound to that card | Insert/copy/open in the top toolbar operate through `selectedValue`; no buttons inside the card | Direct target-card actions versus selecting a target and then using the toolbar | | 08 | Selection and target feedback | No list selection state used by these buttons; each button captures its resource | Single-selection `JBList`; renderer changes background, text, and border for selection | Persistent selection and toolbar dependence differ | -| 09 | Single/double-click timing | Delayed single-click insertion, double-click copy, fixed 500 ms window; card buttons execute immediately | Same basic gestures, using system `awt.multiClickInterval` or a 500 ms fallback; insertion also checks that selection still matches the clicked item | Gesture results broadly match, but timing and selection dependencies differ | +| 09 | Single/double-click timing | (Since 2026-10-09) click = select, double-click / Enter = insert, Ctrl+C = copy, no delay window; card buttons execute immediately; all three thumbnail grids share these gestures and support arrow keys | Still click-to-insert and double-click copy, using system `awt.multiClickInterval` or a 500 ms fallback; insertion also checks that selection still matches the clicked item | **Needs sync**: VS Code moved to "click selects / double-click or Enter runs the primary action"; JetBrains `ThumbnailActions.kt` has not followed yet | | 10 | Grid and card geometry | Responsive CSS grid, 118 px minimum column width, 96 px thumbnail area, left-aligned names | `JList.HORIZONTAL_WRAP`, fixed cell width of 120 plus scaled margins, scaled 72 thumbnail height, centered 10 pt bold names truncated by character count | Density, space usage, alignment, and truncation differ; values are source constants rather than measured screen pixels | | 11 | Counts, empty/loading states | Filter/total counts, successful/failed thumbnail counts, explicit empty/no-match messages | Current preview component contains only toolbar, search, and list; failed loading results become an empty collection without equivalent count/empty/failure widgets | Empty data and loading failure are less distinguishable in this page; this does not assess every lower-level error path | diff --git a/docs/interaction-parity-audit.md b/docs/interaction-parity-audit.md index 59cf0359..2e5a240a 100644 --- a/docs/interaction-parity-audit.md +++ b/docs/interaction-parity-audit.md @@ -27,7 +27,7 @@ | 06 | Template / Rect / Point 切换 | 三个并排按钮,当前模式按钮突出显示 | 下拉框选择模式 | 一端直接可见三个模式,另一端需要打开下拉框 | | 07 | 插入、复制、原图按钮 | 每张缩略图右下角常驻三个按钮,直接操作该卡片 | 顶部工具栏的插入、复制、打开按钮,通过 `selectedValue` 操作选中项;卡片内无按钮 | 直接点目标卡片按钮,与先选目标再点顶部按钮,两条操作路径不同 | | 08 | 选中反馈与目标 | 卡片无用于这些按钮的列表选中态;每个按钮闭包绑定自身资源 | `JBList` 单选;渲染器按选中态改变背景、文字和边框 | 另一端有持续选中项,顶部按钮依赖它;操作目标的表达方式不同 | -| 09 | 单击/双击判定 | 卡片单击延迟插入,双击复制;统一使用 500 ms;卡片按钮立即执行 | 同样单击插入、双击复制,但等待系统 `awt.multiClickInterval`,不可用时才是 500 ms;插入还要求当前选中项与点击项相同 | 单击/双击结果大体相同,但时序与选中依赖不同 | +| 09 | 单击/双击判定 | (2026-10-09 起)单击 = 选中、双击 / Enter = 插入、Ctrl+C = 复制,无延迟窗口;卡片按钮立即执行;三处缩略图同一套手势并支持方向键 | 仍是单击插入、双击复制,等待系统 `awt.multiClickInterval`,不可用时才是 500 ms;插入还要求当前选中项与点击项相同 | **待同步**:VS Code 已改为「单击选中 / 双击或 Enter 主操作」,JetBrains 端 `ThumbnailActions.kt` 尚未跟进 | | 10 | 网格与卡片规格 | CSS 自适应网格,最小列宽 118 px;缩略图区域 96 px;名称靠左 | `JList.HORIZONTAL_WRAP`,固定格宽为 120 加缩放边距;缩略图 72 加宿主缩放;名称居中,10 pt 粗体,按字符截短 | 排列密度、空间利用、标题对齐和截断方式不同;数值来自源码,非实测像素 | | 11 | 计数、空态、缩略图状态 | 展示筛选数/总数、加载成功/失败数;无资源、无匹配有明确文字 | 当前预览组件只有工具栏、搜索和列表;加载结果异常回退为空集合,没有同等计数、空态和失败统计控件 | 空列表与加载失败更难从页面区分;这里未判断全部底层错误处理 | From 082643924b116f585da1e1a3f2a58ce27dee34d7 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:27:33 +0800 Subject: [PATCH 13/29] =?UTF-8?q?test:=20command=20contract=20(declared=20?= =?UTF-8?q?=E2=86=94=20registered=20=E2=86=94=20nls=20=E2=86=94=20menus/wh?= =?UTF-8?q?en)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 ++- scripts/test_command_contract.js | 74 ++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 scripts/test_command_contract.js diff --git a/package.json b/package.json index f18c9ee6..9ae816e5 100644 --- a/package.json +++ b/package.json @@ -663,7 +663,7 @@ "verify:version": "node scripts/release/verify-version.js", "verify:l10n": "node scripts/release/verify-l10n.js", "release": "sh scripts/release.sh", - "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", +"test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", "test:webview-html": "node scripts/test_webview_html.js", "test:design-system": "node scripts/test_design_system.js", "test:thumbnail-actions": "node scripts/test_thumbnail_actions.js", @@ -714,7 +714,8 @@ "test:l10n-literals": "node scripts/test_l10n_literals.js", "test:position-publish-settings": "node scripts/test_position_publish_settings.js", "test:webview-l10n": "node scripts/test_webview_l10n.js", - "test:resource-host-contract": "node scripts/test_resource_host_contract.js" + "test:resource-host-contract": "node scripts/test_resource_host_contract.js", + "test:command-contract": "node scripts/test_command_contract.js" }, "devDependencies": { "@types/jpeg-js": "^0.3.0", diff --git a/scripts/test_command_contract.js b/scripts/test_command_contract.js new file mode 100644 index 00000000..cb21efb2 --- /dev/null +++ b/scripts/test_command_contract.js @@ -0,0 +1,74 @@ +/** + * 命令契约:package.json 声明 ↔ 源码注册 ↔ nls 文案 ↔ menus/walkthrough 引用 必须一一对上。 + * + * 防的是两类历史问题: + * 1. 命令名不副实 / 声明了却没注册(命令面板里点了没反应); + * 2. menus / walkthrough 引用了不存在的命令,或 when 子句用了没人设置的 context key。 + */ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const contributes = pkg.contributes; +const srcDir = path.join(root, 'src'); +const source = fs.readdirSync(srcDir).filter((f) => f.endsWith('.ts')) + .map((f) => fs.readFileSync(path.join(srcDir, f), 'utf8')).join('\n'); +const nlsFiles = fs.readdirSync(root).filter((f) => /^package\.nls(\.[a-z-]+)?\.json$/.test(f)); + +const declared = new Set(contributes.commands.map((c) => c.command)); +const registered = new Set([...source.matchAll(/registerCommand\(\s*'([^']+)'/g)].map((m) => m[1])); + +console.log('1. 每条声明的命令都已注册'); +for (const id of declared) assert(registered.has(id), `${id} 在 package.json 声明了,但 src 里没有 registerCommand`); + +console.log('2. 每条注册的 okScriptToolkit.* 命令都已声明(命令面板可见)'); +for (const id of registered) { + if (id.startsWith('okScriptToolkit.')) assert(declared.has(id), `${id} 已注册但未在 contributes.commands 声明`); +} + +console.log('3. 命令标题走 nls,且六种语言都有译文'); +for (const cmd of contributes.commands) { + const m = /^%(.+)%$/.exec(cmd.title); + assert(m, `${cmd.command} 的 title 没走 nls`); + for (const file of nlsFiles) { + const nls = JSON.parse(fs.readFileSync(path.join(root, file), 'utf8')); + assert(typeof nls[m[1]] === 'string' && nls[m[1]].trim(), `${file} 缺 ${m[1]}`); + } +} + +console.log('4. menus 引用的命令都存在,导航组命令带图标'); +for (const [menu, items] of Object.entries(contributes.menus || {})) { + for (const item of items) { + assert(declared.has(item.command), `menus.${menu} 引用了未声明的命令 ${item.command}`); + if (menu === 'view/title' && String(item.group || '').startsWith('navigation')) { + const cmd = contributes.commands.find((c) => c.command === item.command); + assert(cmd.icon, `${item.command} 在视图标题栏 navigation 组,必须有 icon`); + } + } +} + +console.log('5. when 子句里的 okScriptToolkit.* context key 都有 setContext'); +const setKeys = new Set(); +for (const m of source.matchAll(/'setContext',\s*'([^']+)'/g)) setKeys.add(m[1]); +for (const m of source.matchAll(/const\s+\w+_CONTEXT\s*=\s*'([^']+)'/g)) setKeys.add(m[1]); +const viewIds = new Set(Object.values(contributes.views || {}).flat().map((v) => v.id)); +const whenTexts = []; +for (const items of Object.values(contributes.menus || {})) for (const item of items) if (item.when) whenTexts.push(item.when); +for (const when of whenTexts) { + for (const m of when.matchAll(/okScriptToolkit\.[A-Za-z]+(?![A-Za-z.])/g)) { + if (viewIds.has(m[0])) continue; // view == + assert(setKeys.has(m[0]), `when 子句用了 ${m[0]},但源码里没有 setContext 它`); + } +} + +console.log('6. walkthrough / nls 里的 command: 链接指向已声明命令'); +for (const file of nlsFiles) { + const text = fs.readFileSync(path.join(root, file), 'utf8'); + for (const m of text.matchAll(/command:(okScriptToolkit\.[A-Za-z.]+)/g)) { + assert(declared.has(m[1]), `${file} 链接了未声明的命令 ${m[1]}`); + } +} + +console.log('\n全部通过'); From 85309a1b3cad7d254f41e55b2cd639d87307c5bf Mon Sep 17 00:00:00 2001 From: AliceJump Date: Fri, 9 Oct 2026 13:32:12 +0800 Subject: [PATCH 14/29] fix(console): keep task-card actions and status bar inside narrow sidebars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gbar is a grid: give its rows min-width:0 so long game/executor text no longer widens the bar past the viewport (overflowed at ~700px). - ≤640: status-bar action group may shrink and wrap (was flex:0 0 auto, so '关闭执行器' was cut off at 260px). - Task cards: the primary action (启动 / 启用) never shrinks; the 参数 button is the one that gives way. ≤340: the title may wrap instead of collapsing to an ellipsis. Checked at 700/640/560/480/400/340/300/260 with no horizontal overflow. --- media/console/console.css | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/media/console/console.css b/media/console/console.css index e64fbc06..8236af6c 100644 --- a/media/console/console.css +++ b/media/console/console.css @@ -83,6 +83,9 @@ gap: 5px; } +/* 状态条是 grid:子项默认 min-width:auto 会按内容撑宽,窄宽时整行溢出视口 */ +.gbar > * { min-width: 0; } + .gbar__sub { padding-left: 31px; color: var(--muted); @@ -313,6 +316,9 @@ } .task-card__actions button { white-space: nowrap; } +/* 主操作(启动 / 启用开关)永远完整可见:不被压缩;可压缩的是「参数」按钮 */ +.task-card__actions > :last-child { flex-shrink: 0; } +.task-card__config { min-width: 0; overflow: hidden; } /* 参数入口:统一「中性图标按钮」形态(design-system 第 3 条:默认态即可识别, 不靠 hover 首次显形)。全部状态保持中性,不用警示色——「已修改」以描边强化表达。 */ @@ -1053,7 +1059,7 @@ @media (max-width: 640px) { .gbar__row { flex-wrap: wrap; row-gap: 2px; } .gbar__val { flex: 1 1 0; } - .gbar__actions { flex-wrap: wrap; margin-left: auto; } + .gbar__actions { flex: 0 1 auto; flex-wrap: wrap; justify-content: flex-end; min-width: 0; max-width: 100%; margin-left: auto; } .task-card__actions { flex-wrap: wrap; } .shell { gap: var(--space-sm); } .lay-side { width: 44px; padding: 6px 4px 6px 0; } @@ -1093,6 +1099,13 @@ } @media (max-width: 340px) { .lay-side__ico { display: none; } + /* 极窄(≤340,含 260):标题允许换行而不是省略成「每周…」,类名/描述继续截断; + 「参数」只留图标,把宽度让给「启动」 */ + .task-card__title-row { flex-wrap: wrap; row-gap: 3px; } + .task-card__name { white-space: normal; overflow-wrap: anywhere; } + .task-card__actions button { flex: 0 0 auto; } + .task-card__actions .task-card__config { flex: 1 1 auto; justify-content: center; } + .task-card__actions > :last-child { flex: 1 0 auto; } } /* ═══ 配置健康度条(任务分段顶部,真实快照数据) ══════════════ */ From 418820e91a4ba5e05caec910fc9d29e3667ab2e7 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 00:54:34 +0800 Subject: [PATCH 15/29] refactor(run): extract types, python helpers and status labels from consolePanel Pure move for the P1 TreeView/params split. consolePanel re-exports the same helpers so toolboxConnect and extension keep working. --- src/consolePanel.ts | 446 +++++--------------------------------------- src/run/index.ts | 3 + src/run/python.ts | 238 +++++++++++++++++++++++ src/run/status.ts | 28 +++ src/run/types.ts | 154 +++++++++++++++ 5 files changed, 474 insertions(+), 395 deletions(-) create mode 100644 src/run/index.ts create mode 100644 src/run/python.ts create mode 100644 src/run/status.ts create mode 100644 src/run/types.ts diff --git a/src/consolePanel.ts b/src/consolePanel.ts index a8dba6a8..60f9bf94 100644 --- a/src/consolePanel.ts +++ b/src/consolePanel.ts @@ -9,407 +9,63 @@ import { GameConnectService } from './toolboxConnect'; import { applySharedAssets, errorPage, getNonce } from './webviewHtml'; import { reconcileCachedTaskList, reconcileTaskList } from './taskReconcile'; import { prewarmScreenshotCapture } from './screenshotCapture'; - -/** 单个任务的元信息 */ -interface TaskInfo { - module: string; - className: string; - /** 显示名:优先任务的 name,回退类名 */ - displayName: string; - /** - * 任务类型:触发任务走「勾选启用 → 入列轮询」,一次性任务走「启动 → 入队执行一次」。 - * 由 parse_config_tasks.py 直接给出,不依赖 schema 采集完成。 - */ - kind?: 'onetime' | 'trigger'; -} - -/** 任务列表请求结果 */ -interface TaskListResult { - ok: boolean; - error?: string; - tasks?: TaskInfo[]; - /** config 模块路径:`src.config` 或 `config`(取决于 config.py 所在位置) */ - configModule?: string; -} - -/** 每个任务可编辑的一项参数(对应项目任务 default_config 里的一个 key) */ -interface TaskParamField { - key: string; - displayKey?: string; - /** 默认值(决定控件类型:bool→开关、int/float→数字、str→文本框、list→多选/列表) */ - default?: unknown; - /** 当前已保存值 */ - value?: unknown; - /** config_type 元信息(drop_down / multi_selection 的 options 等) */ - type?: Record; - /** config_description 说明 */ - desc?: string; - displayDesc?: string; -} - -/** 从项目任务类采集到的 schema */ -interface TaskSchema { - /** 该任务可编辑的参数列表(按 default_config 顺序) */ - fields: TaskParamField[]; - /** 是否采集失败(broken) */ - broken?: boolean; - error?: string; - displayName?: string; - description?: string; - kind?: 'onetime' | 'trigger'; - /** 任务分组(BaseTask.group_name,源文案 key 走 tr);空串 = standalone */ - groupName?: string; - /** 组图标名(FluentIcon/Icon 的 enum name) */ - groupIcon?: string; - /** 项目游戏 UI 的可见性声明;工具箱保留任务以供开发调试。 */ - showInTaskTab?: boolean; - /** 项目声明的配置分组/子任务树:组名 -> 字段或子组 key。 */ - configGroups?: Record; - groupLabels?: Record; - /** register_config_groups 生成的分组下拉字段。 */ - groupSelector?: string; - locale?: string; -} - -/** 全局配置组(框架 GlobalConfig 可见组,probe 的 globalConfigGroups 段) */ -interface GlobalConfigGroup { - name: string; - displayName?: string; - description?: string; - fields: TaskParamField[]; - /** 来源:framework = 框架 GlobalConfig;project_store = 项目自建 store(Phase 5) */ - source?: 'framework' | 'project_store'; -} - -/** 每个任务的独立配置(持久化到 .vscode/ok-script-toolkit-tasks.json)。 - * - * params 在「配置接管」模式下是**全量快照**:schema 的每个键都有值(首建继承项目 - * configs 当前值,之后独立演化),执行器按它全量注入 —— UI 显示 = 实际执行。 - * extraArgs / env 是历史字段:常驻执行器把全部任务跑在同一个进程里,进程级参数 - * 无法再按任务区分,因此不再生效(仅保留数据,不做删除)。UI 早已移除这两项。 - */ -interface TaskConfig { - extraArgs?: string; - env?: Record; - /** 任务参数快照:key=任务 default_config 的 key(含孤儿键),value=快照值 */ - params?: Record; -} - -/** 单个项目的持久化数据 */ -interface ProjectStore { - tasks: Record; - /** 已勾选「启用」的触发任务 key(module::Class),重开面板 / IDE 自动入列 */ - enabledTriggers?: string[]; - /** 全局配置组快照:组名 -> {key: value}(配置接管模式的持久值) */ - globalConfigs?: Record>; - /** 用户展开过的全局配置组(默认收起;重开面板按上次状态) */ - expandedGlobalGroups?: string[]; - /** webview UI 折叠状态(键 -> 值):启动设置区、配置分组、卡片等,重开面板复用 */ - uiState?: Record; -} - -/** 所有任务配置的持久化结构 */ -interface TaskConfigStore { - projects: Record; -} - -/** schema 采集结果(刷新时全量 import 项目任务后落盘缓存) */ -interface SchemaProbeResult { - ok: boolean; - error?: string; - schemas?: Record; - /** 参与采集的任务总数 */ - total?: number; - /** 全局配置组(框架 GlobalConfig 可见组) */ - globalConfigGroups?: GlobalConfigGroup[]; - /** 多账户存储只读概要(configs/account_scoped_overrides.json) */ - multiAccount?: MultiAccountInfo; -} - -/** 多账户存储只读概要(Phase 6 只读呈现,不做编辑器) */ -interface MultiAccountInfo { - available: boolean; - storePath?: string; - readable?: boolean; - hasStoreModule?: boolean; - accountCount?: number; - overrideAccounts?: number; - overriddenTasks?: string[]; - /** 可进多账户的任务:taskKey -> {storageName, keys}(键筛选数据源) */ - enabledTasks?: Record; -} - -/** 多账户存储数据(python/account_store.py get 的结果,编辑器数据源) */ -interface AccountStoreData { - /** 账号列表原文(每行一个账号名) */ - accountListText?: string; - /** 注册表:acc_id -> {username, aliases} */ - registry?: Record; - /** 覆盖表:acc_id -> {任务类名(或全局组名): {键: 值}} */ - accounts?: Record>>; - /** 每账号的地图 content(acc_id -> 文本) */ - mapContents?: Record; -} - -/** 常驻执行器回推的状态快照(对应 run_executor.py 的 OK_TOOLKIT_STATE 标记行) */ -interface ExecutorSnapshot { - paused?: boolean; - /** 当前正在执行的任务 key(module::Class),空闲时为 null */ - current?: string | null; - currentIsTrigger?: boolean; - /** 执行器侧的触发任务启用状态(权威值,宿主据此回写勾选集合) */ - triggers?: { key?: string; enabled?: boolean }[]; - /** 一次性任务等待队列 */ - onetimeQueue?: string[]; -} - -/** 扩展根目录下 python/ 脚本的绝对路径 */ -export function pythonScript(extensionUri: vscode.Uri, name: string): string { - return path.join(extensionUri.fsPath, 'python', name); -} - -/** All three Python entry points use the same VS Code sandbox for one project. */ -function runDirForProject(projectDir: string): string { - return path.join(projectDir, '.vscode', 'ok-script-toolkit'); -} - -/** 解析 Python 子进程 stdout 中最后一个 JSON 行(前面的输出可能是日志) */ -export function parseJsonFromStdout(stdout: string): any { - const lines = stdout.split('\n').filter(Boolean); - for (let i = lines.length - 1; i >= 0; i--) { - try { - return JSON.parse(lines[i]); - } catch { /* 跳过非 JSON 行 */ } - } - return null; -} - -/** JSON-compatible config values use deep value equality; primitives retain strict comparison. */ -export function configValuesEqual(left: unknown, right: unknown): boolean { - if (left === right) return true; - if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false; - if (Array.isArray(left) || Array.isArray(right)) { - if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false; - for (let index = 0; index < left.length; index += 1) { - if (!configValuesEqual(left[index], right[index])) return false; - } - return true; - } - const leftRecord = left as Record; - const rightRecord = right as Record; - const leftKeys = Object.keys(leftRecord); - const rightKeys = Object.keys(rightRecord); - if (leftKeys.length !== rightKeys.length) return false; - return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key) - && configValuesEqual(leftRecord[key], rightRecord[key])); -} - -interface PythonResult { - stdout: string; - stderr: string; -} - -/** 异步运行 Python,避免耗时探针阻塞 VS Code 扩展宿主。 */ -export function runPython( - pythonPath: string, - args: string[], - projectDir: string, - timeout: number, - extraEnv: NodeJS.ProcessEnv = {}, -): Promise { - const env = { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', ...extraEnv }; - return new Promise((resolve, reject) => { - cp.execFile(pythonPath, args, { - cwd: projectDir, - env, - encoding: 'utf-8', - timeout, - windowsHide: true, - maxBuffer: 16 * 1024 * 1024, - }, (error, stdout, stderr) => { - if (error) { - // 项目脚本约定:失败时往 stdout 末尾打印 {"ok": false, "error": "..."}; - // 优先回传结构化错误,其次 stderr,最后才是通用命令失败信息。 - const parsed = parseJsonFromStdout(stdout || ''); - const message = (parsed && typeof parsed.error === 'string' && parsed.error) - || stderr?.trim() - || error.message; - reject(new Error(message)); - return; - } - resolve({ stdout: stdout || '', stderr: stderr || '' }); - }); - }); -} - -/** - * 用 Python 子进程 + AST 安全解析 ok-script 项目的 src/config.py, - * 提取 onetime_tasks / trigger_tasks 注册表,不导入任何模块。 - */ -async function parseConfigTasks(extensionUri: vscode.Uri, projectDir: string, pythonPath: string): Promise { - try { - const result = await runPython( - pythonPath, - [pythonScript(extensionUri, 'parse_config_tasks.py'), projectDir], - projectDir, - 15000, - ); - const parsed = parseJsonFromStdout(result.stdout || ''); - if (!parsed || !parsed.ok) { - return { ok: false, error: parsed?.error || tr('Failed to parse task list') }; - } - const configModule = parsed.config_module || 'src.config'; - const tasks: TaskInfo[] = [ - ...(parsed.onetime || []).map((t: any) => ({ ...t, kind: 'onetime' })), - ...(parsed.trigger || []).map((t: any) => ({ ...t, kind: 'trigger' })), - ].map((t: any) => ({ - module: t.module, - className: t['class'] || t.class, - displayName: t.name || t['class'] || t.module, - kind: t.kind === 'trigger' ? 'trigger' : 'onetime', - })); - return { ok: true, tasks, configModule }; - } catch (e) { - return { ok: false, error: e instanceof Error ? e.message : String(e) }; - } -} - -/** - * 常驻执行器命令行:单一进程连接游戏并轮询全部已启用的触发任务。 - * - * 与旧 run_task.py 的差异见 python/run_executor.py 顶部说明——旧路径走 - * `ok.run_task(config, task=<单个任务>)`,框架会把 executor.trigger_tasks 收窄成 - * 单个任务并 disable 其余触发任务,因此无法多触发任务串连轮询。 - */ -function buildExecutorCommand(extensionUri: vscode.Uri, configModule: string): string[] { - return [ - pythonScript(extensionUri, 'run_executor.py'), - '--config-module', configModule, - ]; -} - -/** - * 用 Python 子进程 + 全量 import 采集项目所有任务的配置 schema —— spawn - * python/probe_task_schemas.py。复用 ok-script 的 OK(config) + TaskManager - * 初始化来实例化任务,拿到经过继承链合并的真实 default_config / config_type / - * config_description / 已保存 config。逐任务 try/except 容错,坏任务标记 broken。 - */ -async function probeTaskSchemas( - extensionUri: vscode.Uri, - projectDir: string, - pythonPath: string, - locale: string, - poDirectory: string, -): Promise { - try { - const result = await runPython( - pythonPath, - [pythonScript(extensionUri, 'probe_task_schemas.py'), projectDir, locale, poDirectory], - projectDir, - 120000, - { OK_TOOLKIT_RUN_DIR: runDirForProject(projectDir) }, - ); - const parsed = parseJsonFromStdout(result.stdout || ''); - if (!parsed || !parsed.ok) { - return { ok: false, error: parsed?.error || tr('Failed to collect task schema') }; - } - return { - ok: true, - schemas: parsed.schemas, - total: parsed.total, - globalConfigGroups: parsed.globalConfigGroups || [], - multiAccount: parsed.multiAccount || { available: false }, - }; - } catch (e) { - return { ok: false, error: e instanceof Error ? e.message : String(e) }; - } -} - -/** - * 解析 ok-script 项目路径与 Python 解释器(工具箱与任务启动器共用)。 - * 配置未填写时,自动检测当前工作区根目录(若含 src/config.py 则视为 ok-script 项目)。 - */ -export function resolveProjectContext(): { projectDir: string; pythonPath: string; fromConfig: boolean } { - const cfg = vscode.workspace.getConfiguration('okScriptToolkit'); - // 项目根解析统一走 projectConfig.resolveProjectDir()。这里原先是本文件与 - // screenshotCapture.ts 各复制一份同样的逻辑,两处容易漂移成"界面与脚本看的不是 - // 同一目录"。fromConfig 表示"来自显式设置"而不是自动探测到的。 - const projectDir = resolveProjectDir(); - const fromConfig = (cfg.get('okScriptProjectPath') || '').trim().length > 0; - - const python = cfg.get('okScriptPython') || ''; - let pythonPath = python; - if (!pythonPath) { - const venvPy = path.join(projectDir, '.venv', 'Scripts', 'python.exe'); - pythonPath = fs.existsSync(venvPy) ? venvPy : 'python'; - } - return { projectDir, pythonPath, fromConfig }; -} - -/** 强制结束执行器进程的超时(秒):收到 stop 后仍未退出则 taskkill 兜底 */ -const FORCE_KILL_DELAY_MS = 12000; - -/** 参数覆盖推送防抖(毫秒):表单自动保存很频繁,合并后再推给执行器 */ -const PARAMS_PUSH_DEBOUNCE_MS = 400; - -/** 已由 Python/framework logger 写出的时间戳不要重复添加。 */ -const OUTPUT_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[,.]\d+)?(?:\s|$)/; - -function padNumber(value: number, width = 2): string { - return String(value).padStart(width, '0'); -} - -/** - * VS Code 普通 OutputChannel 不会替 append/appendLine 内容生成时间戳。 - * 执行器里的框架日志通常已经带时间;插件自己的状态行 / toolkit 行则没有。 - * 这里只给缺失时间戳的非空行补本地时间,保持 Python 原始日志不变且避免双时间戳。 - */ -export function formatConsoleOutputLine(line: string, now = new Date()): string { - if (!line || OUTPUT_TIMESTAMP_RE.test(line)) return line; - const timestamp = [ - now.getFullYear(), '-', padNumber(now.getMonth() + 1), '-', padNumber(now.getDate()), ' ', - padNumber(now.getHours()), ':', padNumber(now.getMinutes()), ':', padNumber(now.getSeconds()), ',', - padNumber(now.getMilliseconds(), 3), - ].join(''); - return `${timestamp} ${line}`; -} +import { + AccountStoreData, + ExecutorSnapshot, + GlobalConfigGroup, + MultiAccountInfo, + ProjectStore, + SchemaProbeResult, + TaskConfig, + TaskConfigStore, + TaskInfo, + TaskListResult, + TaskParamField, + TaskSchema, +} from './run/types'; +import { + FORCE_KILL_DELAY_MS, + PARAMS_PUSH_DEBOUNCE_MS, + buildExecutorCommand, + configValuesEqual, + formatConsoleOutputLine, + parseConfigTasks, + parseJsonFromStdout, + probeTaskSchemas, + pythonScript, + resolveProjectContext, + runDirForProject, + runPython, +} from './run/python'; +import { ExecutorStateSummary, StatusAction, statusActionLabel } from './run/status'; + +// Re-exports for callers that historically imported helpers from consolePanel. +export { + configValuesEqual, + formatConsoleOutputLine, + parseJsonFromStdout, + pythonScript, + resolveProjectContext, + runPython, +}; +export type { ExecutorStateSummary } from './run/status'; +export type { + TaskInfo, + TaskSchema, + GlobalConfigGroup, + TaskConfig, + MultiAccountInfo, + AccountStoreData, +} from './run/types'; /** * 侧边栏「ok-script 控制台」视图 —— 任务启动器 + 游戏工具箱的统一宿主。 * - * 布局(media/console/):顶部全局状态条(游戏连接 + 执行器),下方「任务 / 游戏」 - * 两个分段。原侧边栏工具箱视图(okScriptToolkit.toolbox)已并入这里。 + * P1 起状态与执行器逻辑逐步抽到 src/run/,本类继续托管旧 webview; + * RunTreeDataProvider / ParamsViewProvider 共用同一套导出函数与类型。 * - * 进程模型:整个项目只维持 **一个常驻执行器进程**(python/run_executor.py)—— - * 它连接一次游戏,然后按 ok-script 框架原生的 TaskExecutor 循环轮询所有已启用的 - * 触发任务;一次性任务以入队方式交给同一个执行器跑一次。 - * 因此本类不再有「当前任务 / 单进程」语义,改为维护执行器会话与启用集合。 + * 进程模型:整个项目只维持 **一个常驻执行器进程**(python/run_executor.py)。 */ -/** 状态横幅里的「下一步」按钮:webview 只回传动作 id,宿主决定怎么做 */ -type StatusAction = 'showLogs' | 'openProjectSetting' | 'openPythonSetting' | 'startExecutor' | 'refresh' | 'stopExecutor'; - -function statusActionLabel(action: StatusAction): string { - switch (action) { - case 'showLogs': return tr('Show Logs'); - case 'openProjectSetting': return tr('Set Project Path'); - case 'openPythonSetting': return tr('Set Python Path'); - case 'startExecutor': return tr('Retry'); - case 'refresh': return tr('Reload Tasks'); - case 'stopExecutor': return tr('Stop Executor'); - } -} - -/** 宿主侧执行器状态摘要:供状态栏、视图徽标和 context key 使用 */ -export interface ExecutorStateSummary { - status: 'idle' | 'connecting' | 'running'; - paused: boolean; - /** 当前任务显示名(空闲为空) */ - current: string; - /** 一次性任务等待数 */ - queued: number; - /** 已启用的触发任务数 */ - triggers: number; -} export class ConsoleViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'okScriptToolkit.console'; diff --git a/src/run/index.ts b/src/run/index.ts new file mode 100644 index 00000000..bbf0d822 --- /dev/null +++ b/src/run/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './python'; +export * from './status'; diff --git a/src/run/python.ts b/src/run/python.ts new file mode 100644 index 00000000..f3e9eaf7 --- /dev/null +++ b/src/run/python.ts @@ -0,0 +1,238 @@ +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { selectedProjectLocale, tr } from '../localization'; +import { i18nPoDirectorySetting, resolveProjectDir } from '../projectConfig'; +import { + AccountStoreData, + GlobalConfigGroup, + MultiAccountInfo, + SchemaProbeResult, + TaskInfo, + TaskListResult, + TaskSchema, +} from './types'; + +/** 扩展根目录下 python/ 脚本的绝对路径 */ +export function pythonScript(extensionUri: vscode.Uri, name: string): string { + return path.join(extensionUri.fsPath, 'python', name); +} + +/** All three Python entry points use the same VS Code sandbox for one project. */ +export function runDirForProject(projectDir: string): string { + return path.join(projectDir, '.vscode', 'ok-script-toolkit'); +} + +/** 解析 Python 子进程 stdout 中最后一个 JSON 行(前面的输出可能是日志) */ +export function parseJsonFromStdout(stdout: string): any { + const lines = stdout.split('\n').filter(Boolean); + for (let i = lines.length - 1; i >= 0; i--) { + try { + return JSON.parse(lines[i]); + } catch { /* 跳过非 JSON 行 */ } + } + return null; +} + +/** JSON-compatible config values use deep value equality; primitives retain strict comparison. */ +export function configValuesEqual(left: unknown, right: unknown): boolean { + if (left === right) return true; + if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false; + if (Array.isArray(left) || Array.isArray(right)) { + if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false; + for (let index = 0; index < left.length; index += 1) { + if (!configValuesEqual(left[index], right[index])) return false; + } + return true; + } + const leftRecord = left as Record; + const rightRecord = right as Record; + const leftKeys = Object.keys(leftRecord); + const rightKeys = Object.keys(rightRecord); + if (leftKeys.length !== rightKeys.length) return false; + return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key) + && configValuesEqual(leftRecord[key], rightRecord[key])); +} + +interface PythonResult { + stdout: string; + stderr: string; +} + +/** 异步运行 Python,避免耗时探针阻塞 VS Code 扩展宿主。 */ +export function runPython( + pythonPath: string, + args: string[], + projectDir: string, + timeout: number, + extraEnv: NodeJS.ProcessEnv = {}, +): Promise { + const env = { ...process.env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', ...extraEnv }; + return new Promise((resolve, reject) => { + cp.execFile(pythonPath, args, { + cwd: projectDir, + env, + encoding: 'utf-8', + timeout, + windowsHide: true, + maxBuffer: 16 * 1024 * 1024, + }, (error, stdout, stderr) => { + if (error) { + // 项目脚本约定:失败时往 stdout 末尾打印 {"ok": false, "error": "..."}; + // 优先回传结构化错误,其次 stderr,最后才是通用命令失败信息。 + const parsed = parseJsonFromStdout(stdout || ''); + const message = (parsed && typeof parsed.error === 'string' && parsed.error) + || stderr?.trim() + || error.message; + reject(new Error(message)); + return; + } + resolve({ stdout: stdout || '', stderr: stderr || '' }); + }); + }); +} + +/** + * 用 Python 子进程 + AST 安全解析 ok-script 项目的 src/config.py, + * 提取 onetime_tasks / trigger_tasks 注册表,不导入任何模块。 + */ +export async function parseConfigTasks(extensionUri: vscode.Uri, projectDir: string, pythonPath: string): Promise { + try { + const result = await runPython( + pythonPath, + [pythonScript(extensionUri, 'parse_config_tasks.py'), projectDir], + projectDir, + 15000, + ); + const parsed = parseJsonFromStdout(result.stdout || ''); + if (!parsed || !parsed.ok) { + return { ok: false, error: parsed?.error || tr('Failed to parse task list') }; + } + const configModule = parsed.config_module || 'src.config'; + const tasks: TaskInfo[] = [ + ...(parsed.onetime || []).map((t: any) => ({ ...t, kind: 'onetime' })), + ...(parsed.trigger || []).map((t: any) => ({ ...t, kind: 'trigger' })), + ].map((t: any) => ({ + module: t.module, + className: t['class'] || t.class, + displayName: t.name || t['class'] || t.module, + kind: t.kind === 'trigger' ? 'trigger' : 'onetime', + })); + return { ok: true, tasks, configModule }; + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) }; + } +} + +/** + * 常驻执行器命令行:单一进程连接游戏并轮询全部已启用的触发任务。 + * + * 与旧 run_task.py 的差异见 python/run_executor.py 顶部说明——旧路径走 + * `ok.run_task(config, task=<单个任务>)`,框架会把 executor.trigger_tasks 收窄成 + * 单个任务并 disable 其余触发任务,因此无法多触发任务串连轮询。 + */ +export function buildExecutorCommand(extensionUri: vscode.Uri, configModule: string): string[] { + return [ + pythonScript(extensionUri, 'run_executor.py'), + '--config-module', configModule, + ]; +} + +/** + * 用 Python 子进程 + 全量 import 采集项目所有任务的配置 schema —— spawn + * python/probe_task_schemas.py。复用 ok-script 的 OK(config) + TaskManager + * 初始化来实例化任务,拿到经过继承链合并的真实 default_config / config_type / + * config_description / 已保存 config。逐任务 try/except 容错,坏任务标记 broken。 + */ +export async function probeTaskSchemas( + extensionUri: vscode.Uri, + projectDir: string, + pythonPath: string, + locale: string, + poDirectory: string, +): Promise { + try { + const result = await runPython( + pythonPath, + [pythonScript(extensionUri, 'probe_task_schemas.py'), projectDir, locale, poDirectory], + projectDir, + 120000, + { OK_TOOLKIT_RUN_DIR: runDirForProject(projectDir) }, + ); + const parsed = parseJsonFromStdout(result.stdout || ''); + if (!parsed || !parsed.ok) { + return { ok: false, error: parsed?.error || tr('Failed to collect task schema') }; + } + return { + ok: true, + schemas: parsed.schemas, + total: parsed.total, + globalConfigGroups: parsed.globalConfigGroups || [], + multiAccount: parsed.multiAccount || { available: false }, + }; + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) }; + } +} + +/** + * 解析 ok-script 项目路径与 Python 解释器(工具箱与任务启动器共用)。 + * 配置未填写时,自动检测当前工作区根目录(若含 src/config.py 则视为 ok-script 项目)。 + */ +export function resolveProjectContext(): { projectDir: string; pythonPath: string; fromConfig: boolean } { + const cfg = vscode.workspace.getConfiguration('okScriptToolkit'); + // 项目根解析统一走 projectConfig.resolveProjectDir()。这里原先是本文件与 + // screenshotCapture.ts 各复制一份同样的逻辑,两处容易漂移成"界面与脚本看的不是 + // 同一目录"。fromConfig 表示"来自显式设置"而不是自动探测到的。 + const projectDir = resolveProjectDir(); + const fromConfig = (cfg.get('okScriptProjectPath') || '').trim().length > 0; + + const python = cfg.get('okScriptPython') || ''; + let pythonPath = python; + if (!pythonPath) { + const venvPy = path.join(projectDir, '.venv', 'Scripts', 'python.exe'); + pythonPath = fs.existsSync(venvPy) ? venvPy : 'python'; + } + return { projectDir, pythonPath, fromConfig }; +} + +/** 强制结束执行器进程的超时(秒):收到 stop 后仍未退出则 taskkill 兜底 */ +export const FORCE_KILL_DELAY_MS = 12000; + +/** 参数覆盖推送防抖(毫秒):表单自动保存很频繁,合并后再推给执行器 */ +export const PARAMS_PUSH_DEBOUNCE_MS = 400; + +/** 已由 Python/framework logger 写出的时间戳不要重复添加。 */ +export const OUTPUT_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[,.]\d+)?(?:\s|$)/; + +export function padNumber(value: number, width = 2): string { + return String(value).padStart(width, '0'); +} + +/** + * VS Code 普通 OutputChannel 不会替 append/appendLine 内容生成时间戳。 + * 执行器里的框架日志通常已经带时间;插件自己的状态行 / toolkit 行则没有。 + * 这里只给缺失时间戳的非空行补本地时间,保持 Python 原始日志不变且避免双时间戳。 + */ +export function formatConsoleOutputLine(line: string, now = new Date()): string { + if (!line || OUTPUT_TIMESTAMP_RE.test(line)) return line; + const timestamp = [ + now.getFullYear(), '-', padNumber(now.getMonth() + 1), '-', padNumber(now.getDate()), ' ', + padNumber(now.getHours()), ':', padNumber(now.getMinutes()), ':', padNumber(now.getSeconds()), ',', + padNumber(now.getMilliseconds(), 3), + ].join(''); + return `${timestamp} ${line}`; +} + +/** + * 侧边栏「ok-script 控制台」视图 —— 任务启动器 + 游戏工具箱的统一宿主。 + * + * 布局(media/console/):顶部全局状态条(游戏连接 + 执行器),下方「任务 / 游戏」 + * 两个分段。原侧边栏工具箱视图(okScriptToolkit.toolbox)已并入这里。 + * + * 进程模型:整个项目只维持 **一个常驻执行器进程**(python/run_executor.py)—— + * 它连接一次游戏,然后按 ok-script 框架原生的 TaskExecutor 循环轮询所有已启用的 + * 触发任务;一次性任务以入队方式交给同一个执行器跑一次。 + * 因此本类不再有「当前任务 / 单进程」语义,改为维护执行器会话与启用集合。 + */ diff --git a/src/run/status.ts b/src/run/status.ts new file mode 100644 index 00000000..a254b4ab --- /dev/null +++ b/src/run/status.ts @@ -0,0 +1,28 @@ +import { tr } from '../localization'; + +/** 状态横幅里的「下一步」按钮:webview 只回传动作 id,宿主决定怎么做 */ +export type StatusAction = 'showLogs' | 'openProjectSetting' | 'openPythonSetting' | 'startExecutor' | 'refresh' | 'stopExecutor'; + +export function statusActionLabel(action: StatusAction): string { + switch (action) { + case 'showLogs': return tr('Show Logs'); + case 'openProjectSetting': return tr('Set Project Path'); + case 'openPythonSetting': return tr('Set Python Path'); + case 'startExecutor': return tr('Retry'); + case 'refresh': return tr('Reload Tasks'); + case 'stopExecutor': return tr('Stop Executor'); + } +} + +/** 宿主侧执行器状态摘要:供状态栏、视图徽标和 context key 使用 */ +export interface ExecutorStateSummary { + status: 'idle' | 'connecting' | 'running'; + paused: boolean; + /** 当前任务显示名(空闲为空) */ + current: string; + /** 一次性任务等待数 */ + queued: number; + /** 已启用的触发任务数 */ + triggers: number; +} + diff --git a/src/run/types.ts b/src/run/types.ts new file mode 100644 index 00000000..72ed4f6d --- /dev/null +++ b/src/run/types.ts @@ -0,0 +1,154 @@ +/** Shared run-view / executor types (extracted from consolePanel for TreeView + params). */ +/** 单个任务的元信息 */ +export interface TaskInfo { + module: string; + className: string; + /** 显示名:优先任务的 name,回退类名 */ + displayName: string; + /** + * 任务类型:触发任务走「勾选启用 → 入列轮询」,一次性任务走「启动 → 入队执行一次」。 + * 由 parse_config_tasks.py 直接给出,不依赖 schema 采集完成。 + */ + kind?: 'onetime' | 'trigger'; +} + +/** 任务列表请求结果 */ +export interface TaskListResult { + ok: boolean; + error?: string; + tasks?: TaskInfo[]; + /** config 模块路径:`src.config` 或 `config`(取决于 config.py 所在位置) */ + configModule?: string; +} + +/** 每个任务可编辑的一项参数(对应项目任务 default_config 里的一个 key) */ +export interface TaskParamField { + key: string; + displayKey?: string; + /** 默认值(决定控件类型:bool→开关、int/float→数字、str→文本框、list→多选/列表) */ + default?: unknown; + /** 当前已保存值 */ + value?: unknown; + /** config_type 元信息(drop_down / multi_selection 的 options 等) */ + type?: Record; + /** config_description 说明 */ + desc?: string; + displayDesc?: string; +} + +/** 从项目任务类采集到的 schema */ +export interface TaskSchema { + /** 该任务可编辑的参数列表(按 default_config 顺序) */ + fields: TaskParamField[]; + /** 是否采集失败(broken) */ + broken?: boolean; + error?: string; + displayName?: string; + description?: string; + kind?: 'onetime' | 'trigger'; + /** 任务分组(BaseTask.group_name,源文案 key 走 tr);空串 = standalone */ + groupName?: string; + /** 组图标名(FluentIcon/Icon 的 enum name) */ + groupIcon?: string; + /** 项目游戏 UI 的可见性声明;工具箱保留任务以供开发调试。 */ + showInTaskTab?: boolean; + /** 项目声明的配置分组/子任务树:组名 -> 字段或子组 key。 */ + configGroups?: Record; + groupLabels?: Record; + /** register_config_groups 生成的分组下拉字段。 */ + groupSelector?: string; + locale?: string; +} + +/** 全局配置组(框架 GlobalConfig 可见组,probe 的 globalConfigGroups 段) */ +export interface GlobalConfigGroup { + name: string; + displayName?: string; + description?: string; + fields: TaskParamField[]; + /** 来源:framework = 框架 GlobalConfig;project_store = 项目自建 store(Phase 5) */ + source?: 'framework' | 'project_store'; +} + +/** 每个任务的独立配置(持久化到 .vscode/ok-script-toolkit-tasks.json)。 + * + * params 在「配置接管」模式下是**全量快照**:schema 的每个键都有值(首建继承项目 + * configs 当前值,之后独立演化),执行器按它全量注入 —— UI 显示 = 实际执行。 + * extraArgs / env 是历史字段:常驻执行器把全部任务跑在同一个进程里,进程级参数 + * 无法再按任务区分,因此不再生效(仅保留数据,不做删除)。UI 早已移除这两项。 + */ +export interface TaskConfig { + extraArgs?: string; + env?: Record; + /** 任务参数快照:key=任务 default_config 的 key(含孤儿键),value=快照值 */ + params?: Record; +} + +/** 单个项目的持久化数据 */ +export interface ProjectStore { + tasks: Record; + /** 已勾选「启用」的触发任务 key(module::Class),重开面板 / IDE 自动入列 */ + enabledTriggers?: string[]; + /** 全局配置组快照:组名 -> {key: value}(配置接管模式的持久值) */ + globalConfigs?: Record>; + /** 用户展开过的全局配置组(默认收起;重开面板按上次状态) */ + expandedGlobalGroups?: string[]; + /** webview UI 折叠状态(键 -> 值):启动设置区、配置分组、卡片等,重开面板复用 */ + uiState?: Record; +} + +/** 所有任务配置的持久化结构 */ +export interface TaskConfigStore { + projects: Record; +} + +/** schema 采集结果(刷新时全量 import 项目任务后落盘缓存) */ +export interface SchemaProbeResult { + ok: boolean; + error?: string; + schemas?: Record; + /** 参与采集的任务总数 */ + total?: number; + /** 全局配置组(框架 GlobalConfig 可见组) */ + globalConfigGroups?: GlobalConfigGroup[]; + /** 多账户存储只读概要(configs/account_scoped_overrides.json) */ + multiAccount?: MultiAccountInfo; +} + +/** 多账户存储只读概要(Phase 6 只读呈现,不做编辑器) */ +export interface MultiAccountInfo { + available: boolean; + storePath?: string; + readable?: boolean; + hasStoreModule?: boolean; + accountCount?: number; + overrideAccounts?: number; + overriddenTasks?: string[]; + /** 可进多账户的任务:taskKey -> {storageName, keys}(键筛选数据源) */ + enabledTasks?: Record; +} + +/** 多账户存储数据(python/account_store.py get 的结果,编辑器数据源) */ +export interface AccountStoreData { + /** 账号列表原文(每行一个账号名) */ + accountListText?: string; + /** 注册表:acc_id -> {username, aliases} */ + registry?: Record; + /** 覆盖表:acc_id -> {任务类名(或全局组名): {键: 值}} */ + accounts?: Record>>; + /** 每账号的地图 content(acc_id -> 文本) */ + mapContents?: Record; +} + +/** 常驻执行器回推的状态快照(对应 run_executor.py 的 OK_TOOLKIT_STATE 标记行) */ +export interface ExecutorSnapshot { + paused?: boolean; + /** 当前正在执行的任务 key(module::Class),空闲时为 null */ + current?: string | null; + currentIsTrigger?: boolean; + /** 执行器侧的触发任务启用状态(权威值,宿主据此回写勾选集合) */ + triggers?: { key?: string; enabled?: boolean }[]; + /** 一次性任务等待队列 */ + onetimeQueue?: string[]; +} + From 9a3175323ce211975c98d2022907e0fc5c6ca36b Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 01:00:01 +0800 Subject: [PATCH 16/29] feat(run): TreeView model, params host, publish-diff & diagnostics helpers --- media/params/app.js | 69 +++++++ media/params/index.html | 33 +++ media/params/params.css | 7 + scripts/test_character_diagnostics.js | 18 ++ scripts/test_publish_diff.js | 30 +++ scripts/test_run_tree_model.js | 75 +++++++ src/consolePanel.ts | 112 ++++++++++- src/run/characterDiagnostics.ts | 37 ++++ src/run/index.ts | 3 + src/run/paramsView.ts | 127 ++++++++++++ src/run/publishDiff.ts | 58 ++++++ src/run/runTree.ts | 136 +++++++++++++ src/run/treeModel.ts | 278 ++++++++++++++++++++++++++ 13 files changed, 981 insertions(+), 2 deletions(-) create mode 100644 media/params/app.js create mode 100644 media/params/index.html create mode 100644 media/params/params.css create mode 100644 scripts/test_character_diagnostics.js create mode 100644 scripts/test_publish_diff.js create mode 100644 scripts/test_run_tree_model.js create mode 100644 src/run/characterDiagnostics.ts create mode 100644 src/run/paramsView.ts create mode 100644 src/run/publishDiff.ts create mode 100644 src/run/runTree.ts create mode 100644 src/run/treeModel.ts diff --git a/media/params/app.js b/media/params/app.js new file mode 100644 index 00000000..ba308894 --- /dev/null +++ b/media/params/app.js @@ -0,0 +1,69 @@ +(function () { + const vscode = acquireVsCodeApi(); + const I18N = JSON.parse(document.getElementById('paramsI18n')?.textContent || '{}'); + const t = (k, a = {}) => (I18N[k] || k).replace(/\{(\w+)\}/g, (_, n) => String(a[n] ?? '{' + n + '}')); + const empty = document.getElementById('empty'); + const body = document.getElementById('body'); + const formHost = document.getElementById('formHost'); + document.getElementById('emptyTitle').textContent = t('paramsEmptyTitle'); + document.getElementById('emptyBody').textContent = t('paramsEmptyBody'); + + function showEmpty() { + empty.hidden = false; + body.hidden = true; + formHost.replaceChildren(); + } + + function render(msg) { + empty.hidden = true; + body.hidden = false; + document.getElementById('title').textContent = msg.title || ''; + document.getElementById('subtitle').textContent = msg.subtitle || ''; + document.getElementById('status').textContent = msg.statusText || t('paramsSandboxHint'); + formHost.replaceChildren(); + const fields = msg.fields || []; + const values = msg.values || {}; + if (!fields.length) { + const p = document.createElement('p'); + p.className = 'panel-hint'; + p.textContent = t('paramsNoFields'); + formHost.appendChild(p); + return; + } + // Prefer shared ConfigPanel when available; fall back to plain inputs. + if (typeof ConfigPanel !== 'undefined' && ConfigPanel.mount) { + ConfigPanel.mount(formHost, { + fields, + values, + onChange: (next) => vscode.postMessage({ type: 'save', target: msg.target, key: msg.key, values: next }), + }); + return; + } + const list = document.createElement('div'); + list.className = 'params-fields'; + for (const f of fields) { + const row = document.createElement('label'); + row.className = 'params-field'; + const name = document.createElement('span'); + name.textContent = f.displayKey || f.key; + const input = document.createElement('input'); + input.type = 'text'; + input.value = values[f.key] != null ? String(values[f.key]) : (f.default != null ? String(f.default) : ''); + input.addEventListener('change', () => { + const next = { ...values, [f.key]: input.value }; + vscode.postMessage({ type: 'save', target: msg.target, key: msg.key, values: next }); + }); + row.append(name, input); + list.appendChild(row); + } + formHost.appendChild(list); + } + + window.addEventListener('message', (e) => { + const msg = e.data; + if (msg?.type === 'clear') showEmpty(); + if (msg?.type === 'params') render(msg); + }); + showEmpty(); + vscode.postMessage({ type: 'ready' }); +})(); diff --git a/media/params/index.html b/media/params/index.html new file mode 100644 index 00000000..7dfa4f65 --- /dev/null +++ b/media/params/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + +
+
+
+
+
+ +
+ + + + + + + diff --git a/media/params/params.css b/media/params/params.css new file mode 100644 index 00000000..544d5d93 --- /dev/null +++ b/media/params/params.css @@ -0,0 +1,7 @@ +body { margin: 0; background: var(--bg-page); color: var(--text-primary); } +.params { padding: var(--space-md); } +.params-empty { padding: var(--space-lg) var(--space-sm); text-align: center; color: var(--muted); } +.params-head { margin-bottom: var(--space-md); } +.params-head__title { font-size: var(--font-md); font-weight: 650; } +.params-head__sub { margin-top: 2px; color: var(--muted); font-family: var(--font-mono); font-size: var(--font-2xs); } +.params-head__status { margin-top: var(--space-xs); color: var(--muted); font-size: var(--font-xs); } diff --git a/scripts/test_character_diagnostics.js b/scripts/test_character_diagnostics.js new file mode 100644 index 00000000..05bd58a6 --- /dev/null +++ b/scripts/test_character_diagnostics.js @@ -0,0 +1,18 @@ +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const ts = require('typescript'); +const root = path.resolve(__dirname, '..'); +const src = fs.readFileSync(path.join(root, 'src/run/characterDiagnostics.ts'), 'utf8'); +const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText; +const mod = { exports: {} }; +Function('exports', js)(mod.exports); +const { issuesToDiagnostics } = mod.exports; +const out = issuesToDiagnostics( + [{ message: 'missing id', severity: 'error', source: { fileName: 'char_a.json' } }], + { characterDir: '/proj/skills' }, +); +assert.strictEqual(out.length, 1); +assert.strictEqual(out[0].line, 0); +assert.ok(out[0].file.includes('char_a.json')); +console.log('ok character diagnostics'); diff --git a/scripts/test_publish_diff.js b/scripts/test_publish_diff.js new file mode 100644 index 00000000..9c7baa60 --- /dev/null +++ b/scripts/test_publish_diff.js @@ -0,0 +1,30 @@ +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const ts = require('typescript'); +const root = path.resolve(__dirname, '..'); +const src = fs.readFileSync(path.join(root, 'src/run/publishDiff.ts'), 'utf8'); +const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText; +const mod = { exports: {} }; +Function('require', 'module', 'exports', js)(require, mod, mod.exports); +const { diffPublishSets, countUnpublished, fingerprintBox } = mod.exports; +const diffs = diffPublishSets( + [ + { name: 'a', draftFingerprint: '1,2,3,4' }, + { name: 'b', draftFingerprint: '0,0,10,10' }, + { name: 'c', draftFingerprint: '1' }, + ], + [ + { name: 'a', publishedFingerprint: '1,2,3,4' }, + { name: 'b', publishedFingerprint: '0,0,11,10' }, + { name: 'd', publishedFingerprint: '9' }, + ], +); +const by = Object.fromEntries(diffs.map((d) => [d.name, d.status])); +assert.strictEqual(by.a, 'published'); +assert.strictEqual(by.b, 'modified'); +assert.strictEqual(by.c, 'draft-only'); +assert.strictEqual(by.d, 'orphan-published'); +assert.strictEqual(countUnpublished(diffs), 2); +assert.strictEqual(fingerprintBox({ bbox: [1.2, 3.7, 4, 5] }), '1,4,4,5'); +console.log('ok publish diff'); diff --git a/scripts/test_run_tree_model.js b/scripts/test_run_tree_model.js new file mode 100644 index 00000000..8c198b7e --- /dev/null +++ b/scripts/test_run_tree_model.js @@ -0,0 +1,75 @@ +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const ts = require('typescript'); + +// Compile treeModel + deps on the fly without pulling vscode. +const root = path.resolve(__dirname, '..'); +function loadTs(rel) { + const src = fs.readFileSync(path.join(root, rel), 'utf8'); + // Strip vscode-only type imports; treeModel only imports types from ./status and ./types + const js = ts.transpileModule(src, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true }, + }).outputText; + const mod = { exports: {} }; + const dirname = path.dirname(path.join(root, rel)); + const req = (id) => { + if (id.startsWith('.')) { + const resolved = path.join(dirname, id.endsWith('.ts') ? id : id + '.ts'); + if (fs.existsSync(resolved)) return loadTs(path.relative(root, resolved)); + const jsPath = resolved.replace(/\.ts$/, '.js'); + if (fs.existsSync(jsPath)) return require(jsPath); + } + return require(id); + }; + Function('require', 'module', 'exports', '__dirname', '__filename', js)(req, mod, mod.exports, dirname, path.join(root, rel)); + return mod.exports; +} + +const { buildRunTree, taskKeyOf } = loadTs('src/run/treeModel.ts'); + +const tasks = [ + { module: 'a', className: 'T1', displayName: '自动对话', kind: 'trigger' }, + { module: 'a', className: 'T2', displayName: '自动战斗', kind: 'trigger' }, + { module: 'b', className: 'Daily', displayName: '每日任务', kind: 'onetime' }, + { module: 'b', className: 'Mail', displayName: '领取邮件', kind: 'onetime' }, +]; +const schemas = { + 'b::Daily': { fields: [], groupName: '日常' }, + 'b::Mail': { fields: [] }, +}; +const tree = buildRunTree({ + game: { connected: true, title: 'Endfield' }, + executor: { status: 'running', paused: false, current: '每日任务', queued: 1, triggers: 1 }, + tasks, + schemas, + enabledTriggers: ['a::T1'], + currentTaskKey: 'b::Daily', + onetimeQueue: ['b::Mail'], + globalGroups: [{ name: 'window', displayName: '窗口', fields: [] }], + globalOverrides: { window: { w: 1 } }, + accounts: { available: true, names: ['主号'], active: '主号' }, +}); + +assert.strictEqual(tree.length, 6); +assert.strictEqual(tree[0].kind, 'game'); +assert.ok(tree[0].description.includes('Endfield')); +assert.strictEqual(tree[1].kind, 'executor'); +assert.ok(tree[1].description.includes('每日任务')); +const triggers = tree[2]; +assert.strictEqual(triggers.children.length, 2); +assert.strictEqual(triggers.children[0].checked, true); +assert.strictEqual(triggers.children[1].checked, false); +assert.strictEqual(triggers.description, '1/2 已启用'); +const onetimes = tree[3]; +assert.strictEqual(onetimes.children.length, 2); // 日常 + 未分组 +const daily = onetimes.children.find((g) => g.label === '日常'); +assert.ok(daily); +assert.strictEqual(daily.children[0].contextValue, 'run.task.running'); +assert.strictEqual(daily.children[0].description, '运行中'); +const ungrouped = onetimes.children.find((g) => g.label === '未分组'); +assert.strictEqual(ungrouped.children[0].description, '排队 #1'); +assert.strictEqual(tree[4].description.includes('1 处覆盖'), true); +assert.strictEqual(tree[5].children[0].label, '主号'); +assert.strictEqual(taskKeyOf(tasks[0]), 'a::T1'); +console.log('ok run tree model'); diff --git a/src/consolePanel.ts b/src/consolePanel.ts index 60f9bf94..e4a978cd 100644 --- a/src/consolePanel.ts +++ b/src/consolePanel.ts @@ -925,7 +925,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { * 不等于「现在开始跑」。想真正启动请点状态条的启动按钮(`startExecutor`)。 * 执行器已在运行时勾选依然即时生效。 */ - private async setTriggerEnabled(view: vscode.WebviewView, task: TaskInfo, enabled: boolean): Promise { + private async setTriggerEnabled(view: vscode.WebviewView | undefined, task: TaskInfo, enabled: boolean): Promise { const key = this.taskKey(task); if (enabled) this.enabledTriggers.add(key); else this.enabledTriggers.delete(key); @@ -1242,6 +1242,112 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { // ── 命令面板 / 视图标题栏 / 状态栏入口(与 webview 按钮走同一套逻辑)── + private readonly treeChangeEmitter = new vscode.EventEmitter(); + /** 任务列表 / 执行器 / 勾选 / 游戏状态变化时触发,供 Run TreeView 刷新 */ + readonly onDidChangeTree = this.treeChangeEmitter.event; + + private fireTreeChange(): void { + this.treeChangeEmitter.fire(); + } + + /** + * 无 webview 时也能驱动执行器(P1 TreeView / 参数视图)。 + * 挂上游戏状态回调并拉一次任务列表。 + */ + async activateHeadless(): Promise { + this.gameConnect.attachUi( + (text) => this.setStatus(text ? 'info' : 'ok', text), + () => { this.pushGameState(); this.fireTreeChange(); }, + ); + this.toolboxSubscription?.dispose(); + this.toolboxSubscription = onToolboxStateChange(() => { this.pushGameState(); this.fireTreeChange(); }); + await this.refreshTasksHeadless(); + } + + private async refreshTasksHeadless(): Promise { + // refreshTasks 需要 WebviewView;headless 时用一个最小 shim 只接收 postMessage 忽略 + const shim = { + webview: { postMessage: () => undefined }, + } as unknown as vscode.WebviewView; + await this.refreshTasks(shim); + this.fireTreeChange(); + } + + /** Run TreeView / 参数视图共用的只读快照 */ + getRunSnapshot(): { + tasks: TaskInfo[]; + schemas: Record; + enabledTriggers: string[]; + globalGroups: GlobalConfigGroup[]; + globalSnapshots: Record>; + currentTaskKey: string | null; + onetimeQueue: string[]; + multiAccount: MultiAccountInfo; + accountNames: string[]; + game: { connected: boolean; title?: string; detail?: string }; + } { + const state = this.gameConnect.currentState(); + const game = state.game; + return { + tasks: [...this.knownTasks], + schemas: { ...this.schemas }, + enabledTriggers: [...this.enabledTriggers], + globalGroups: [...this.globalGroups], + globalSnapshots: { ...this.globalSnapshots }, + currentTaskKey: this.isCurrentProjectExecutor() ? (this.snapshot.current || null) : null, + onetimeQueue: this.isCurrentProjectExecutor() ? [...(this.snapshot.onetimeQueue || [])] : [], + multiAccount: this.multiAccount, + accountNames: Object.keys(this.accountStoreData?.accounts || {}), + game: { + connected: Boolean(game), + title: game?.title || game?.exe, + detail: game ? `PID ${game.pid}` : undefined, + }, + }; + } + + getTaskByKey(key: string): TaskInfo | undefined { + return this.knownTasks.find((task) => this.taskKey(task) === key); + } + + getSchema(key: string): TaskSchema | undefined { + return this.schemas[key]; + } + + getTaskConfig(key: string): TaskConfig | undefined { + return this.taskConfigs[key]; + } + + async setTriggerEnabledByKey(key: string, enabled: boolean): Promise { + const task = this.getTaskByKey(key); + if (!task) return; + await this.setTriggerEnabled(this.view ?? undefined, task, enabled); + this.fireTreeChange(); + } + + async enqueueByKey(key: string): Promise { + const task = this.getTaskByKey(key); + if (!task) return; + await this.enqueueOnetime(this.view ?? undefined, task); + this.fireTreeChange(); + } + + async saveTaskParams(key: string, params: Record): Promise { + const task = this.getTaskByKey(key); + if (!task) return; + this.saveTaskConfig(task, this.sanitizeTaskConfig(task, { ...(this.taskConfigs[key] || {}), params })); + this.fireTreeChange(); + } + + setGlobalParam(group: string, key: string, value: unknown): void { + this.setGlobalValue(group, key, value); + this.fireTreeChange(); + } + + getGlobalGroup(name: string): GlobalConfigGroup | undefined { + return this.globalGroups.find((g) => g.name === name); + } + executorState(): ExecutorStateSummary { const own = this.executor !== null && this.isCurrentProjectExecutor(); const currentKey = own ? (this.snapshot.current || '') : ''; @@ -1296,9 +1402,10 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { async commandRefresh(): Promise { if (this.view) { await this.refreshTasks(this.view); + this.fireTreeChange(); return; } - await vscode.commands.executeCommand(`${ConsoleViewProvider.viewType}.focus`); + await this.refreshTasksHeadless(); } /** 「运行任务…」:从已加载的一次性任务里选一个入队(与卡片「启动」等价,必要时会启动执行器) */ @@ -1482,6 +1589,7 @@ export class ConsoleViewProvider implements vscode.WebviewViewProvider { this.toolboxSubscription = undefined; this.gameConnect.detachUi(); this.executorStateEmitter.dispose(); + this.treeChangeEmitter.dispose(); this.output.dispose(); } diff --git a/src/run/characterDiagnostics.ts b/src/run/characterDiagnostics.ts new file mode 100644 index 00000000..8991ac5e --- /dev/null +++ b/src/run/characterDiagnostics.ts @@ -0,0 +1,37 @@ +/** + * Map character-data issues to VS Code Diagnostic-shaped objects (pure). + * First version: file-level range (line 0). Precise JSON offsets are P2. + */ +export interface CharacterIssueLike { + message: string; + severity: 'error' | 'warning' | 'info'; + source?: { fileName?: string; kind?: string; characterId?: string }; +} + +export interface DiagnosticLike { + file: string; + message: string; + severity: 'error' | 'warning' | 'info'; + line: number; + character: number; +} + +export function issuesToDiagnostics( + issues: CharacterIssueLike[], + roots: { masterFile?: string; localeFile?: string; characterDir?: string } = {}, +): DiagnosticLike[] { + return issues.map((issue) => { + const fileName = issue.source?.fileName; + let file = fileName || roots.masterFile || 'characters.json'; + if (fileName && roots.characterDir && !fileName.includes('/') && !fileName.includes('\\')) { + file = roots.characterDir.replace(/[\\/]$/, '') + '/' + fileName; + } + return { + file, + message: issue.message, + severity: issue.severity, + line: 0, + character: 0, + }; + }); +} diff --git a/src/run/index.ts b/src/run/index.ts index bbf0d822..0d68a953 100644 --- a/src/run/index.ts +++ b/src/run/index.ts @@ -1,3 +1,6 @@ export * from './types'; export * from './python'; export * from './status'; +export * from './treeModel'; +export * from './publishDiff'; +export * from './characterDiagnostics'; diff --git a/src/run/paramsView.ts b/src/run/paramsView.ts new file mode 100644 index 00000000..dad5fcc2 --- /dev/null +++ b/src/run/paramsView.ts @@ -0,0 +1,127 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { ConsoleViewProvider } from '../consolePanel'; +import { injectWebviewLocalization, tr } from '../localization'; +import { applySharedAssets, errorPage, getNonce } from '../webviewHtml'; +import type { RunTreeNode } from './treeModel'; +import type { RunTreeDataProvider } from './runTree'; + +/** Parameters inspector: follows the Run tree selection. */ +export class ParamsViewProvider implements vscode.WebviewViewProvider, vscode.Disposable { + static readonly viewType = 'okScriptToolkit.params'; + + private view?: vscode.WebviewView; + private readonly subs: vscode.Disposable[] = []; + + constructor( + private readonly extensionUri: vscode.Uri, + private readonly backend: ConsoleViewProvider, + private readonly runTree: RunTreeDataProvider, + ) { + this.subs.push(runTree.onDidChangeSelection((node) => void this.showNode(node))); + this.subs.push(backend.onDidChangeTree(() => void this.showNode(runTree.getSelection()))); + } + + resolveWebviewView(view: vscode.WebviewView): void { + this.view = view; + view.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this.extensionUri, 'media', 'params'), + vscode.Uri.joinPath(this.extensionUri, 'media', 'console'), + vscode.Uri.joinPath(this.extensionUri, 'media', 'shared'), + ], + }; + view.webview.html = this.buildHtml(view.webview); + view.webview.onDidReceiveMessage(async (msg) => { + if (msg?.type === 'ready') { + await this.showNode(this.runTree.getSelection()); + return; + } + if (msg?.type === 'save' && msg.key && msg.values && typeof msg.values === 'object') { + if (msg.target === 'global') { + for (const [k, v] of Object.entries(msg.values as Record)) { + this.backend.setGlobalParam(String(msg.key), k, v); + } + } else { + await this.backend.saveTaskParams(String(msg.key), msg.values as Record); + } + } + }); + } + + async openForNode(node: RunTreeNode | undefined): Promise { + await vscode.commands.executeCommand(`${ParamsViewProvider.viewType}.focus`); + await this.showNode(node); + } + + private async showNode(node: RunTreeNode | undefined): Promise { + const webview = this.view?.webview; + if (!webview) return; + if (!node || (node.kind !== 'task' && node.kind !== 'trigger' && node.kind !== 'globalGroup')) { + void webview.postMessage({ type: 'clear' }); + return; + } + if (node.kind === 'globalGroup' && node.groupName) { + const group = this.backend.getGlobalGroup(node.groupName); + const snap = this.backend.getRunSnapshot(); + void webview.postMessage({ + type: 'params', + target: 'global', + key: node.groupName, + title: group?.displayName || node.groupName, + subtitle: node.groupName, + statusText: tr('paramsSandboxHint'), + fields: group?.fields || [], + values: snap.globalSnapshots[node.groupName] || {}, + }); + return; + } + if (!node.taskKey) { + void webview.postMessage({ type: 'clear' }); + return; + } + const task = this.backend.getTaskByKey(node.taskKey); + const schema = this.backend.getSchema(node.taskKey); + const config = this.backend.getTaskConfig(node.taskKey); + void webview.postMessage({ + type: 'params', + target: 'task', + key: node.taskKey, + title: task?.displayName || node.label, + subtitle: node.taskKey, + statusText: tr('paramsSandboxHint'), + fields: schema?.fields || [], + values: config?.params || {}, + }); + } + + private buildHtml(webview: vscode.Webview): string { + const nonce = getNonce(); + const htmlPath = path.join(this.extensionUri.fsPath, 'media', 'params', 'index.html'); + let html = ''; + try { + html = fs.readFileSync(htmlPath, 'utf-8'); + } catch (e) { + return errorPage(tr('Error'), tr('Unable to read view file: {error}', { + error: e instanceof Error ? e.message : String(e), + })); + } + const resource = (folder: string, name: string) => webview.asWebviewUri( + vscode.Uri.joinPath(this.extensionUri, 'media', folder, name), + ).toString(true); + return injectWebviewLocalization(applySharedAssets(webview, this.extensionUri, html + .split('__CSP_NONCE__').join(nonce) + .split('__CSP_SOURCE__').join(webview.cspSource) + .split('__STYLE_URI__').join(resource('params', 'params.css')) + .split('__FIELDS_SCRIPT_URI__').join(resource('console', 'fields.js')) + .split('__CONFIG_PANEL_SCRIPT_URI__').join(resource('console', 'configPanel.js')) + .split('__APP_SCRIPT_URI__').join(resource('params', 'app.js')), + )); + } + + dispose(): void { + for (const s of this.subs) s.dispose(); + } +} diff --git a/src/run/publishDiff.ts b/src/run/publishDiff.ts new file mode 100644 index 00000000..9aee139b --- /dev/null +++ b/src/run/publishDiff.ts @@ -0,0 +1,58 @@ +/** + * Draft (authoring / ok_templates) vs published (runtime COCO / features) comparison. + * Pure: no vscode. Used for status dots and "N unpublished" badges. + */ +export type PublishStatus = 'draft-only' | 'modified' | 'published' | 'orphan-published'; + +export interface PublishItem { + name: string; + /** Draft bbox or size fingerprint when available */ + draftFingerprint?: string; + publishedFingerprint?: string; +} + +export interface PublishDiff { + name: string; + status: PublishStatus; +} + +export function fingerprintBox(box: { bbox?: number[]; width?: number; height?: number }): string { + if (box.bbox && box.bbox.length >= 4) { + return box.bbox.map((n) => Math.round(Number(n))).join(','); + } + return `${Math.round(Number(box.width) || 0)}x${Math.round(Number(box.height) || 0)}`; +} + +/** Compare draft names/fingerprints to published ones. */ +export function diffPublishSets(draft: PublishItem[], published: PublishItem[]): PublishDiff[] { + const pub = new Map(published.map((p) => [p.name, p])); + const seen = new Set(); + const out: PublishDiff[] = []; + for (const d of draft) { + seen.add(d.name); + const p = pub.get(d.name); + if (!p) { + out.push({ name: d.name, status: 'draft-only' }); + continue; + } + if ( + d.draftFingerprint != null + && p.publishedFingerprint != null + && d.draftFingerprint !== p.publishedFingerprint + ) { + out.push({ name: d.name, status: 'modified' }); + } else { + out.push({ name: d.name, status: 'published' }); + } + } + for (const p of published) { + if (seen.has(p.name)) continue; + out.push({ name: p.name, status: 'orphan-published' }); + } + out.sort((a, b) => a.name.localeCompare(b.name)); + return out; +} + +export function countUnpublished(diffs: PublishDiff[]): number { + return diffs.filter((d) => d.status === 'draft-only' || d.status === 'modified').length; +} diff --git a/src/run/runTree.ts b/src/run/runTree.ts new file mode 100644 index 00000000..0feeeac2 --- /dev/null +++ b/src/run/runTree.ts @@ -0,0 +1,136 @@ +import * as vscode from 'vscode'; +import { ConsoleViewProvider } from '../consolePanel'; +import { buildRunTree, RunTreeNode, taskKeyOf } from './treeModel'; +import { tr } from '../localization'; + +/** Map tree model nodes to VS Code TreeItems for the Run view. */ +export class RunTreeDataProvider implements vscode.TreeDataProvider, vscode.Disposable { + static readonly viewId = 'okScriptToolkit.run'; + + private readonly changeEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.changeEmitter.event; + private roots: RunTreeNode[] = []; + private readonly byId = new Map(); + private selection: RunTreeNode | undefined; + private readonly selectionEmitter = new vscode.EventEmitter(); + readonly onDidChangeSelection = this.selectionEmitter.event; + private readonly subs: vscode.Disposable[] = []; + private treeView?: vscode.TreeView; + + constructor(private readonly backend: ConsoleViewProvider) { + this.subs.push(backend.onDidChangeTree(() => this.refresh())); + this.subs.push(backend.onDidChangeExecutorState(() => this.refresh())); + } + + attachTreeView(view: vscode.TreeView): void { + this.treeView = view; + this.subs.push(view.onDidChangeSelection((e) => { + this.selection = e.selection[0]; + this.selectionEmitter.fire(this.selection); + })); + this.subs.push(view.onDidChangeCheckboxState(async (e) => { + for (const [node, state] of e.items) { + if (node.taskKey && node.checkbox) { + await this.backend.setTriggerEnabledByKey( + node.taskKey, + state === vscode.TreeItemCheckboxState.Checked, + ); + } + } + })); + } + + refresh(): void { + const snap = this.backend.getRunSnapshot(); + const exec = this.backend.executorState(); + this.roots = buildRunTree({ + game: snap.game, + executor: exec, + tasks: snap.tasks, + schemas: snap.schemas, + enabledTriggers: snap.enabledTriggers, + currentTaskKey: snap.currentTaskKey, + onetimeQueue: snap.onetimeQueue, + globalGroups: snap.globalGroups, + globalOverrides: snap.globalSnapshots, + accounts: { + available: snap.multiAccount.available, + names: snap.accountNames, + active: undefined, + }, + }); + this.byId.clear(); + const walk = (nodes: RunTreeNode[]) => { + for (const n of nodes) { + this.byId.set(n.id, n); + if (n.children) walk(n.children); + } + }; + walk(this.roots); + this.changeEmitter.fire(); + this.updateBadge(exec); + } + + private updateBadge(exec: { status: string; queued: number; current: string }): void { + if (!this.treeView) return; + const n = (exec.status === 'running' && exec.current ? 1 : 0) + exec.queued; + this.treeView.badge = n > 0 + ? { value: n, tooltip: tr('Running / queued tasks') } + : undefined; + } + + getTreeItem(element: RunTreeNode): vscode.TreeItem { + const item = new vscode.TreeItem( + element.label, + element.collapsible + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.None, + ); + item.id = element.id; + item.description = element.description; + item.contextValue = element.contextValue; + if (element.icon) item.iconPath = new vscode.ThemeIcon(element.icon); + if (element.checkbox) { + item.checkboxState = element.checked + ? vscode.TreeItemCheckboxState.Checked + : vscode.TreeItemCheckboxState.Unchecked; + } + if (element.kind === 'task' || element.kind === 'trigger') { + item.command = { + command: 'okScriptToolkit.run.openParams', + title: tr('Open Parameters'), + arguments: [element], + }; + } + return item; + } + + getChildren(element?: RunTreeNode): RunTreeNode[] { + if (!element) return this.roots; + return element.children || []; + } + + getParent(element: RunTreeNode): RunTreeNode | undefined { + for (const root of this.roots) { + if (root.children?.includes(element)) return root; + for (const child of root.children || []) { + if (child.children?.includes(element)) return child; + } + } + return undefined; + } + + getNode(id: string): RunTreeNode | undefined { + return this.byId.get(id); + } + + getSelection(): RunTreeNode | undefined { + return this.selection; + } + + dispose(): void { + this.changeEmitter.dispose(); + this.selectionEmitter.dispose(); + for (const s of this.subs) s.dispose(); + } +} diff --git a/src/run/treeModel.ts b/src/run/treeModel.ts new file mode 100644 index 00000000..2ce16aa3 --- /dev/null +++ b/src/run/treeModel.ts @@ -0,0 +1,278 @@ +/** + * Pure tree model for the Run view. UI-free so it can be unit-tested without vscode. + * Node ids are stable strings used as TreeItem.id / contextValue prefixes. + */ +import type { ExecutorStateSummary } from './status'; +import type { GlobalConfigGroup, TaskInfo, TaskSchema } from './types'; + +export type RunNodeKind = + | 'game' + | 'executor' + | 'triggers' + | 'trigger' + | 'onetimes' + | 'group' + | 'task' + | 'globals' + | 'globalGroup' + | 'accounts' + | 'account'; + +export interface RunTreeNode { + id: string; + kind: RunNodeKind; + label: string; + description?: string; + /** VS Code contextValue for menus (e.g. run.task.running) */ + contextValue: string; + collapsible: boolean; + checkbox?: boolean; + checked?: boolean; + icon?: string; + children?: RunTreeNode[]; + /** Task key module::className when applicable */ + taskKey?: string; + groupName?: string; + accountName?: string; +} + +export interface GameSnapshot { + connected: boolean; + title?: string; + detail?: string; +} + +export interface RunTreeInput { + game: GameSnapshot; + executor: ExecutorStateSummary; + tasks: TaskInfo[]; + schemas: Record; + enabledTriggers: Set | string[]; + currentTaskKey?: string | null; + onetimeQueue?: string[]; + globalGroups: GlobalConfigGroup[]; + globalOverrides: Record>; + accounts?: { available: boolean; names: string[]; active?: string }; +} + +export function taskKeyOf(task: TaskInfo): string { + return `${task.module}::${task.className}`; +} + +function enabledSet(input: RunTreeInput): Set { + return input.enabledTriggers instanceof Set + ? input.enabledTriggers + : new Set(input.enabledTriggers); +} + +function groupOf(task: TaskInfo, schemas: Record): string { + const schema = schemas[taskKeyOf(task)]; + return (schema?.groupName || '').trim() || ''; +} + +function taskDescription( + task: TaskInfo, + input: RunTreeInput, + enabled: Set, +): string { + const key = taskKeyOf(task); + const isTrigger = task.kind === 'trigger'; + if (input.currentTaskKey === key) { + return input.executor.paused ? '已暂停' : (isTrigger ? '轮询中' : '运行中'); + } + if (isTrigger) { + if (!enabled.has(key)) return '未启用'; + return input.executor.status === 'running' ? '已入列' : '已启用'; + } + const queue = input.onetimeQueue || []; + const idx = queue.indexOf(key); + if (idx >= 0) return `排队 #${idx + 1}`; + return ''; +} + +function taskContext(task: TaskInfo, input: RunTreeInput): string { + const key = taskKeyOf(task); + if (input.currentTaskKey === key) return 'run.task.running'; + if (task.kind === 'trigger') return 'run.task.trigger'; + return 'run.task.onetime'; +} + +/** Build the full Run tree from a snapshot. */ +export function buildRunTree(input: RunTreeInput): RunTreeNode[] { + const enabled = enabledSet(input); + const triggers = input.tasks.filter((t) => t.kind === 'trigger'); + const onetimes = input.tasks.filter((t) => t.kind !== 'trigger'); + const enabledCount = triggers.filter((t) => enabled.has(taskKeyOf(t))).length; + + const game: RunTreeNode = { + id: 'game', + kind: 'game', + label: '游戏', + description: input.game.connected + ? (input.game.title ? `已连接 · ${input.game.title}` : '已连接') + : '未连接', + contextValue: input.game.connected ? 'run.game.connected' : 'run.game.disconnected', + collapsible: false, + icon: input.game.connected ? 'vm-active' : 'vm', + }; + + const execLabel = (() => { + const s = input.executor; + if (s.status === 'idle') return '未启动'; + if (s.status === 'connecting') return '启动中…'; + if (s.paused) return s.current ? `已暂停 · ${s.current}` : '已暂停'; + if (s.current) return `运行中 · ${s.current}`; + return '就绪'; + })(); + const executor: RunTreeNode = { + id: 'executor', + kind: 'executor', + label: '执行器', + description: execLabel, + contextValue: `run.executor.${input.executor.status}${input.executor.paused ? '.paused' : ''}`, + collapsible: false, + icon: input.executor.status === 'running' + ? (input.executor.paused ? 'debug-pause' : 'play-circle') + : 'circle-outline', + }; + + const triggerChildren: RunTreeNode[] = triggers.map((task) => { + const key = taskKeyOf(task); + return { + id: `trigger:${key}`, + kind: 'trigger' as const, + label: task.displayName, + description: taskDescription(task, input, enabled), + contextValue: taskContext(task, input), + collapsible: false, + checkbox: true, + checked: enabled.has(key), + icon: 'zap', + taskKey: key, + }; + }); + + const triggerRoot: RunTreeNode = { + id: 'triggers', + kind: 'triggers', + label: '触发任务', + description: triggers.length + ? `${enabledCount}/${triggers.length} 已启用` + : '无', + contextValue: 'run.triggers', + collapsible: true, + icon: 'zap', + children: triggerChildren, + }; + + // Group onetimes by schema.groupName; empty → 未分组 + const groups = new Map(); + for (const task of onetimes) { + const g = groupOf(task, input.schemas) || '未分组'; + if (!groups.has(g)) groups.set(g, []); + groups.get(g)!.push(task); + } + const groupNames = [...groups.keys()].sort((a, b) => { + if (a === '未分组') return 1; + if (b === '未分组') return -1; + return a.localeCompare(b, 'zh'); + }); + + const queueLen = (input.onetimeQueue || []).length; + const onetimeChildren: RunTreeNode[] = groupNames.map((name) => { + const tasks = groups.get(name)!; + const children = tasks.map((task) => { + const key = taskKeyOf(task); + return { + id: `task:${key}`, + kind: 'task' as const, + label: task.displayName, + description: taskDescription(task, input, enabled), + contextValue: taskContext(task, input), + collapsible: false, + icon: input.currentTaskKey === key ? 'play' : 'circle-large-outline', + taskKey: key, + }; + }); + return { + id: `group:${name}`, + kind: 'group' as const, + label: name, + description: String(tasks.length), + contextValue: 'run.group', + collapsible: true, + icon: 'folder', + groupName: name, + children, + }; + }); + + const onetimeRoot: RunTreeNode = { + id: 'onetimes', + kind: 'onetimes', + label: '一次性任务', + description: onetimes.length + ? `${onetimes.length}${queueLen ? ` · 队列 ${queueLen}` : ''}` + : '无', + contextValue: 'run.onetimes', + collapsible: true, + icon: 'list-ordered', + children: onetimeChildren, + }; + + const overrideCount = Object.values(input.globalOverrides || {}) + .reduce((n, g) => n + Object.keys(g || {}).length, 0); + const globalChildren: RunTreeNode[] = (input.globalGroups || []).map((g) => { + const overrides = Object.keys(input.globalOverrides?.[g.name] || {}).length; + return { + id: `global:${g.name}`, + kind: 'globalGroup' as const, + label: g.displayName || g.name, + description: overrides ? `${overrides} 处覆盖` : undefined, + contextValue: 'run.globalGroup', + collapsible: false, + icon: 'settings-gear', + groupName: g.name, + }; + }); + const globals: RunTreeNode = { + id: 'globals', + kind: 'globals', + label: '全局配置', + description: input.globalGroups.length + ? `${input.globalGroups.length} 组${overrideCount ? ` · ${overrideCount} 处覆盖` : ''}` + : '无', + contextValue: 'run.globals', + collapsible: true, + icon: 'settings-gear', + children: globalChildren, + }; + + const accountNames = input.accounts?.names || []; + const accountChildren: RunTreeNode[] = accountNames.map((name) => ({ + id: `account:${name}`, + kind: 'account' as const, + label: name, + description: input.accounts?.active === name ? '当前' : undefined, + contextValue: 'run.account', + collapsible: false, + icon: 'account', + accountName: name, + })); + const accounts: RunTreeNode = { + id: 'accounts', + kind: 'accounts', + label: '账号', + description: input.accounts?.available + ? (input.accounts.active + ? `${input.accounts.active} · ${accountNames.length}` + : String(accountNames.length)) + : '不可用', + contextValue: 'run.accounts', + collapsible: accountNames.length > 0, + icon: 'organization', + children: accountChildren, + }; + + return [game, executor, triggerRoot, onetimeRoot, globals, accounts]; +} From 5768d34c5a7d1c1f625146bf6edf8c274694fbbe Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 01:00:01 +0800 Subject: [PATCH 17/29] =?UTF-8?q?feat(ia):=20single=20ok-script=20activity?= =?UTF-8?q?=20bar=20=E2=80=94=20Run=20/=20Parameters=20/=20Resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n/bundle.l10n.es.json | 11 +- l10n/bundle.l10n.ja.json | 11 +- l10n/bundle.l10n.json | 11 +- l10n/bundle.l10n.ko.json | 11 +- l10n/bundle.l10n.zh-cn.json | 11 +- l10n/bundle.l10n.zh-tw.json | 11 +- l10n/webview.en.json | 26 +++- l10n/webview.es.json | 26 +++- l10n/webview.ja.json | 26 +++- l10n/webview.ko.json | 26 +++- l10n/webview.zh-cn.json | 26 +++- l10n/webview.zh-tw.json | 26 +++- media/resources/app.js | 184 ++++++++++++++++++++++++++ media/resources/index.html | 52 ++++++++ media/resources/resources.css | 47 +++++++ package.json | 224 ++++++++++++++++++-------------- package.nls.es.json | 16 ++- package.nls.ja.json | 16 ++- package.nls.json | 16 ++- package.nls.ko.json | 16 ++- package.nls.zh-cn.json | 16 ++- package.nls.zh-tw.json | 16 ++- scripts/test_design_system.js | 2 + src/extension.ts | 108 +++++++++++++--- src/run/resourcesView.ts | 234 ++++++++++++++++++++++++++++++++++ 25 files changed, 1022 insertions(+), 147 deletions(-) create mode 100644 media/resources/app.js create mode 100644 media/resources/index.html create mode 100644 media/resources/resources.css create mode 100644 src/run/resourcesView.ts diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 0401f0e0..251cb530 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -290,5 +290,14 @@ "Undo": "Deshacer", "Annotations changed after the swap, so it cannot be undone automatically.": "Las anotaciones cambiaron después del intercambio; no se puede deshacer automáticamente.", "Failed to undo the swap.": "No se pudo deshacer el intercambio.", - "Swap undone: '{first}' and '{second}' have their original annotations again.": "Intercambio deshecho: '{first}' y '{second}' recuperan sus anotaciones originales." + "Swap undone: '{first}' and '{second}' have their original annotations again.": "Intercambio deshecho: '{first}' y '{second}' recuperan sus anotaciones originales.", + "Running / queued tasks": "En ejecución / en cola", + "Inbox screenshots": "Capturas de la bandeja", + "Screenshot failed": "Error al capturar", + "Failed to add screenshot to drafts: {error}": "No se pudo añadir a borradores: {error}", + "Open Parameters": "Abrir parámetros", + "paramsSandboxHint": "Solo en el sandbox · se envía al ejecutor si está activo", + "Resource expression (e.g. fL.btn_claim)": "Expresión de recurso (p. ej. fL.btn_claim)", + "Locate “{name}” in Resources": "Localizar “{name}” en Recursos", + "Use as ok-script project": "Usar como proyecto ok-script" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 112c4cb7..20e2ad80 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -290,5 +290,14 @@ "Undo": "元に戻す", "Annotations changed after the swap, so it cannot be undone automatically.": "交換後にアノテーションが変更されたため、自動で元に戻せません。", "Failed to undo the swap.": "交換を元に戻せませんでした。", - "Swap undone: '{first}' and '{second}' have their original annotations again.": "交換を元に戻しました: '{first}' と '{second}' は元のアノテーションに戻りました。" + "Swap undone: '{first}' and '{second}' have their original annotations again.": "交換を元に戻しました: '{first}' と '{second}' は元のアノテーションに戻りました。", + "Running / queued tasks": "実行中 / 待機タスク", + "Inbox screenshots": "受信箱のスクリーンショット", + "Screenshot failed": "キャプチャに失敗しました", + "Failed to add screenshot to drafts: {error}": "下書きへの追加に失敗:{error}", + "Open Parameters": "パラメータを開く", + "paramsSandboxHint": "サンドボックスにのみ保存 · 実行中はエグゼキュータへ送出", + "Resource expression (e.g. fL.btn_claim)": "リソース式(例: fL.btn_claim)", + "Locate “{name}” in Resources": "「リソース」で “{name}” を表示", + "Use as ok-script project": "ok-script プロジェクトとして使う" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 2b51bbef..ae11e42c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -290,5 +290,14 @@ "Undo": "Undo", "Annotations changed after the swap, so it cannot be undone automatically.": "Annotations changed after the swap, so it cannot be undone automatically.", "Failed to undo the swap.": "Failed to undo the swap.", - "Swap undone: '{first}' and '{second}' have their original annotations again.": "Swap undone: '{first}' and '{second}' have their original annotations again." + "Swap undone: '{first}' and '{second}' have their original annotations again.": "Swap undone: '{first}' and '{second}' have their original annotations again.", + "Running / queued tasks": "Running / queued tasks", + "Inbox screenshots": "Inbox screenshots", + "Screenshot failed": "Screenshot failed", + "Failed to add screenshot to drafts: {error}": "Failed to add screenshot to drafts: {error}", + "Open Parameters": "Open Parameters", + "paramsSandboxHint": "Saved to the sandbox only · pushed to the executor when running", + "Resource expression (e.g. fL.btn_claim)": "Resource expression (e.g. fL.btn_claim)", + "Locate “{name}” in Resources": "Locate “{name}” in Resources", + "Use as ok-script project": "Use as ok-script project" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index e2e90232..e0312927 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -290,5 +290,14 @@ "Undo": "실행 취소", "Annotations changed after the swap, so it cannot be undone automatically.": "교환 후 어노테이션이 변경되어 자동으로 되돌릴 수 없습니다.", "Failed to undo the swap.": "교환을 되돌리지 못했습니다.", - "Swap undone: '{first}' and '{second}' have their original annotations again.": "교환 취소됨: '{first}'와 '{second}'가 원래 어노테이션으로 돌아갔습니다." + "Swap undone: '{first}' and '{second}' have their original annotations again.": "교환 취소됨: '{first}'와 '{second}'가 원래 어노테이션으로 돌아갔습니다.", + "Running / queued tasks": "실행 중 / 대기 작업", + "Inbox screenshots": "받은편지함 스크린샷", + "Screenshot failed": "스크린샷 실패", + "Failed to add screenshot to drafts: {error}": "초안에 스크린샷 추가 실패: {error}", + "Open Parameters": "매개 변수 열기", + "paramsSandboxHint": "샌드박스에만 저장 · 실행 중이면 실행기로 전송", + "Resource expression (e.g. fL.btn_claim)": "리소스 식(예: fL.btn_claim)", + "Locate “{name}” in Resources": "리소스에서 “{name}” 찾기", + "Use as ok-script project": "ok-script 프로젝트로 사용" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 5b2d9f93..5d6559ef 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -290,5 +290,14 @@ "Undo": "撤销", "Annotations changed after the swap, so it cannot be undone automatically.": "交换之后标注又有改动,无法自动撤销。", "Failed to undo the swap.": "撤销交换失败。", - "Swap undone: '{first}' and '{second}' have their original annotations again.": "已撤销交换:「{first}」和「{second}」恢复了原来的标注。" + "Swap undone: '{first}' and '{second}' have their original annotations again.": "已撤销交换:「{first}」和「{second}」恢复了原来的标注。", + "Running / queued tasks": "运行中 / 排队任务", + "Inbox screenshots": "收件箱截图", + "Screenshot failed": "截图失败", + "Failed to add screenshot to drafts: {error}": "无法把截图加入草稿:{error}", + "Open Parameters": "打开参数", + "paramsSandboxHint": "仅写入沙箱 · 执行器运行中时会推送", + "Resource expression (e.g. fL.btn_claim)": "资源表达式(例如 fL.btn_claim)", + "Locate “{name}” in Resources": "在「资源」中定位 “{name}”", + "Use as ok-script project": "用作 ok-script 项目" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index c98f343b..a6a4c7fd 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -290,5 +290,14 @@ "Undo": "復原", "Annotations changed after the swap, so it cannot be undone automatically.": "交換之後標註又有變動,無法自動復原。", "Failed to undo the swap.": "復原交換失敗。", - "Swap undone: '{first}' and '{second}' have their original annotations again.": "已復原交換:「{first}」和「{second}」恢復了原本的標註。" + "Swap undone: '{first}' and '{second}' have their original annotations again.": "已復原交換:「{first}」和「{second}」恢復了原本的標註。", + "Running / queued tasks": "執行中 / 排隊任務", + "Inbox screenshots": "收件匣截圖", + "Screenshot failed": "截圖失敗", + "Failed to add screenshot to drafts: {error}": "無法把截圖加入草稿:{error}", + "Open Parameters": "開啟參數", + "paramsSandboxHint": "僅寫入沙箱 · 執行器執行中時會推送", + "Resource expression (e.g. fL.btn_claim)": "資源運算式(例如 fL.btn_claim)", + "Locate “{name}” in Resources": "在「資源」中定位 “{name}”", + "Use as ok-script project": "用作 ok-script 專案" } diff --git a/l10n/webview.en.json b/l10n/webview.en.json index 3447417a..a0a197c9 100644 --- a/l10n/webview.en.json +++ b/l10n/webview.en.json @@ -380,8 +380,8 @@ "annotationModeLabel": "Resource type", "sharedHistoryTooltip": "Undo and redo across all resource modes.", "conflictFields": "Fields", - "resourcesTitle": "Resource Preview", - "resourcesSearch": "Search resource names or expressions…", + "resourcesTitle": "Resources", + "resourcesSearch": "Search name / category / fL.xxx", "resourcesEmpty": "No resources in this mode.", "assetsEmptyTitle": "No images to annotate yet", "assetsEmptyBody": "Capture or import an image, or drag one in from Temp Shots.", @@ -392,5 +392,25 @@ "previewCheckedPaths": "Checked:\n{paths}", "openAnnotationManagement": "Open Annotation Management", "assetCardHint": "Double-click / Enter / E: open annotation editor · Delete: delete", - "tempGridLabel": "Temporary screenshots" + "tempGridLabel": "Temporary screenshots", + "paramsEmptyTitle": "No selection", + "paramsEmptyBody": "Select a task, trigger, or global group in the Run view.", + "paramsNoFields": "This item has no configurable fields.", + "paramsSandboxHint": "Saved to the sandbox only · pushed to the executor when running", + "inboxTitle": "Inbox", + "inboxCount": "{n}/{max}", + "inboxEmpty": "Empty — capture with Ctrl+Alt+S or the toolbar camera.", + "modeDraft": "Draft", + "modePublished": "Published", + "kindTemplate": "Template", + "kindRect": "Box", + "kindPoint": "Point", + "publishEllipsis": "Publish…", + "resourcesHint": "Double-click / Enter = primary · E = annotate · Ctrl+C = copy", + "libCount": "{n} items", + "draftEmpty": "No draft images yet. Capture to the inbox and press E.", + "publishedEmpty": "No published templates yet. Annotate drafts, then Publish…", + "unpublishedCount": "{n} unpublished", + "allPublished": "All in sync with published", + "inspectorSelected": "Selected" } diff --git a/l10n/webview.es.json b/l10n/webview.es.json index dde1ab0b..745b23b9 100644 --- a/l10n/webview.es.json +++ b/l10n/webview.es.json @@ -380,8 +380,8 @@ "annotationModeLabel": "Tipo de recurso", "sharedHistoryTooltip": "Deshacer y rehacer en todos los modos de recursos.", "conflictFields": "Campos", - "resourcesTitle": "Vista previa de recursos", - "resourcesSearch": "Buscar nombres o expresiones de recursos…", + "resourcesTitle": "Recursos", + "resourcesSearch": "Buscar nombre / categoría / fL.xxx", "resourcesEmpty": "No hay recursos en este modo.", "assetsEmptyTitle": "Aún no hay imágenes para anotar", "assetsEmptyBody": "Captura o importa una imagen, o arrástrala desde Capturas temporales.", @@ -392,5 +392,25 @@ "previewCheckedPaths": "Rutas comprobadas:\n{paths}", "openAnnotationManagement": "Abrir gestión de anotaciones", "assetCardHint": "Doble clic / Enter / E: abrir editor de anotaciones · Supr: eliminar", - "tempGridLabel": "Capturas temporales" + "tempGridLabel": "Capturas temporales", + "paramsEmptyTitle": "Sin selección", + "paramsEmptyBody": "Selecciona una tarea, disparador o grupo global en Ejecutar.", + "paramsNoFields": "Este elemento no tiene campos.", + "paramsSandboxHint": "Solo en el sandbox · se envía al ejecutor si está activo", + "inboxTitle": "Bandeja", + "inboxCount": "{n}/{max}", + "inboxEmpty": "Vacía — captura con Ctrl+Alt+S o la cámara", + "modeDraft": "Borrador", + "modePublished": "Publicado", + "kindTemplate": "Plantilla", + "kindRect": "Caja", + "kindPoint": "Punto", + "publishEllipsis": "Publicar…", + "resourcesHint": "Doble clic / Enter = principal · E = anotar · Ctrl+C = copiar", + "libCount": "{n} elementos", + "draftEmpty": "Aún no hay borradores. Captura a la bandeja y pulsa E.", + "publishedEmpty": "Aún no hay publicados. Anota borradores y Publicar…", + "unpublishedCount": "{n} sin publicar", + "allPublished": "Todo sincronizado", + "inspectorSelected": "Seleccionado" } diff --git a/l10n/webview.ja.json b/l10n/webview.ja.json index 027bc0f3..ed4f390b 100644 --- a/l10n/webview.ja.json +++ b/l10n/webview.ja.json @@ -380,8 +380,8 @@ "annotationModeLabel": "リソースの種類", "sharedHistoryTooltip": "すべてのリソースモードで元に戻す・やり直す。", "conflictFields": "フィールド", - "resourcesTitle": "リソースプレビュー", - "resourcesSearch": "リソース名や式を検索…", + "resourcesTitle": "リソース", + "resourcesSearch": "名前 / カテゴリ / fL.xxx", "resourcesEmpty": "このモードにはリソースがありません。", "assetsEmptyTitle": "アノテーションする画像がまだありません", "assetsEmptyBody": "スクリーンショットを撮るか画像をインポートしてください。一時スクリーンショットからドラッグすることもできます。", @@ -392,5 +392,25 @@ "previewCheckedPaths": "確認したパス:\n{paths}", "openAnnotationManagement": "アノテーション管理を開く", "assetCardHint": "ダブルクリック / Enter / E: アノテーションエディターを開く · Delete: 削除", - "tempGridLabel": "一時スクリーンショット" + "tempGridLabel": "一時スクリーンショット", + "paramsEmptyTitle": "選択なし", + "paramsEmptyBody": "「実行」ビューでタスク / トリガー / グローバル設定を選択してください。", + "paramsNoFields": "設定可能なフィールドはありません。", + "paramsSandboxHint": "サンドボックスにのみ保存 · 実行中はエグゼキュータへ送出", + "inboxTitle": "受信箱", + "inboxCount": "{n}/{max}", + "inboxEmpty": "空です · Ctrl+Alt+S かツールバーでキャプチャ", + "modeDraft": "下書き", + "modePublished": "公開済み", + "kindTemplate": "テンプレ", + "kindRect": "枠", + "kindPoint": "点", + "publishEllipsis": "公開…", + "resourcesHint": "ダブルクリック / Enter = 主操作 · E = 注釈 · Ctrl+C = コピー", + "libCount": "{n} 件", + "draftEmpty": "下書き画像がありません。受信箱にキャプチャして E。", + "publishedEmpty": "公開済みテンプレートがありません。下書きを注釈してから公開…", + "unpublishedCount": "未公開 {n} 件", + "allPublished": "公開版と一致", + "inspectorSelected": "選択中" } diff --git a/l10n/webview.ko.json b/l10n/webview.ko.json index eeaef89e..785dfd2e 100644 --- a/l10n/webview.ko.json +++ b/l10n/webview.ko.json @@ -380,8 +380,8 @@ "annotationModeLabel": "리소스 유형", "sharedHistoryTooltip": "모든 리소스 모드에서 실행 취소 및 다시 실행합니다.", "conflictFields": "필드", - "resourcesTitle": "리소스 미리보기", - "resourcesSearch": "리소스 이름 또는 표현식 검색…", + "resourcesTitle": "리소스", + "resourcesSearch": "이름 / 범주 / fL.xxx", "resourcesEmpty": "이 모드에 리소스가 없습니다.", "assetsEmptyTitle": "어노테이션할 이미지가 아직 없습니다", "assetsEmptyBody": "스크린샷을 찍거나 이미지를 가져오세요. 임시 스크린샷에서 끌어다 놓을 수도 있습니다.", @@ -392,5 +392,25 @@ "previewCheckedPaths": "확인한 경로:\n{paths}", "openAnnotationManagement": "어노테이션 관리 열기", "assetCardHint": "더블클릭 / Enter / E: 어노테이션 편집기 열기 · Delete: 삭제", - "tempGridLabel": "임시 스크린샷" + "tempGridLabel": "임시 스크린샷", + "paramsEmptyTitle": "선택 없음", + "paramsEmptyBody": "실행 보기에서 작업·트리거·전역 그룹을 선택하세요.", + "paramsNoFields": "구성할 필드가 없습니다.", + "paramsSandboxHint": "샌드박스에만 저장 · 실행 중이면 실행기로 전송", + "inboxTitle": "받은편지함", + "inboxCount": "{n}/{max}", + "inboxEmpty": "비어 있음 · Ctrl+Alt+S 또는 도구 모음으로 캡처", + "modeDraft": "초안", + "modePublished": "게시됨", + "kindTemplate": "템플릿", + "kindRect": "박스", + "kindPoint": "점", + "publishEllipsis": "게시…", + "resourcesHint": "더블클릭 / Enter = 기본 · E = 주석 · Ctrl+C = 복사", + "libCount": "{n}개", + "draftEmpty": "초안 이미지 없음. 받은편지함에 캡처 후 E.", + "publishedEmpty": "게시된 템플릿 없음. 초안을 주석한 뒤 게시…", + "unpublishedCount": "미게시 {n}개", + "allPublished": "게시본과 일치", + "inspectorSelected": "선택됨" } diff --git a/l10n/webview.zh-cn.json b/l10n/webview.zh-cn.json index a2198ed9..0c520303 100644 --- a/l10n/webview.zh-cn.json +++ b/l10n/webview.zh-cn.json @@ -380,8 +380,8 @@ "annotationModeLabel": "资源类型", "sharedHistoryTooltip": "跨资源模式撤销和重做。", "conflictFields": "字段", - "resourcesTitle": "资源预览", - "resourcesSearch": "搜索资源名或表达式…", + "resourcesTitle": "资源", + "resourcesSearch": "搜索名称 / 类别 / fL.xxx", "resourcesEmpty": "当前模式没有资源。", "assetsEmptyTitle": "还没有可标注的图片", "assetsEmptyBody": "截图或导入图片,也可以把「临时截图」里的图拖到这里。", @@ -392,5 +392,25 @@ "previewCheckedPaths": "已检查:\n{paths}", "openAnnotationManagement": "打开标注管理", "assetCardHint": "双击 / Enter / E:打开标注编辑器 · Delete:删除", - "tempGridLabel": "临时截图" + "tempGridLabel": "临时截图", + "paramsEmptyTitle": "未选择项", + "paramsEmptyBody": "在「运行」视图中选择任务、触发任务或全局配置组。", + "paramsNoFields": "此项没有可配置字段。", + "paramsSandboxHint": "仅写入沙箱 · 执行器运行中时会推送", + "inboxTitle": "收件箱", + "inboxCount": "{n}/{max}", + "inboxEmpty": "空 · 用 Ctrl+Alt+S 或工具栏相机截图", + "modeDraft": "草稿", + "modePublished": "已发布", + "kindTemplate": "模板", + "kindRect": "框", + "kindPoint": "点", + "publishEllipsis": "发布…", + "resourcesHint": "双击 / Enter = 主操作 · E = 标注 · Ctrl+C = 复制", + "libCount": "{n} 项", + "draftEmpty": "还没有草稿图片。截到收件箱后按 E。", + "publishedEmpty": "还没有已发布的模板。先标注草稿,再点「发布…」。", + "unpublishedCount": "{n} 项未发布", + "allPublished": "与已发布版本一致", + "inspectorSelected": "选中" } diff --git a/l10n/webview.zh-tw.json b/l10n/webview.zh-tw.json index b821c523..5461da96 100644 --- a/l10n/webview.zh-tw.json +++ b/l10n/webview.zh-tw.json @@ -380,8 +380,8 @@ "annotationModeLabel": "資源類型", "sharedHistoryTooltip": "跨資源模式復原和重做。", "conflictFields": "欄位", - "resourcesTitle": "資源預覽", - "resourcesSearch": "搜尋資源名稱或運算式…", + "resourcesTitle": "資源", + "resourcesSearch": "搜尋名稱 / 類別 / fL.xxx", "resourcesEmpty": "目前模式沒有資源。", "assetsEmptyTitle": "還沒有可標註的圖片", "assetsEmptyBody": "截圖或匯入圖片,也可以把「臨時截圖」裡的圖拖到這裡。", @@ -392,5 +392,25 @@ "previewCheckedPaths": "已檢查:\n{paths}", "openAnnotationManagement": "開啟標註管理", "assetCardHint": "雙擊 / Enter / E:開啟標註編輯器 · Delete:刪除", - "tempGridLabel": "臨時截圖" + "tempGridLabel": "臨時截圖", + "paramsEmptyTitle": "未選擇項目", + "paramsEmptyBody": "在「執行」檢視中選擇任務、觸發任務或全域設定組。", + "paramsNoFields": "此項目沒有可設定欄位。", + "paramsSandboxHint": "僅寫入沙箱 · 執行器執行中時會推送", + "inboxTitle": "收件匣", + "inboxCount": "{n}/{max}", + "inboxEmpty": "空 · 用 Ctrl+Alt+S 或工具列相機截圖", + "modeDraft": "草稿", + "modePublished": "已發佈", + "kindTemplate": "模板", + "kindRect": "框", + "kindPoint": "點", + "publishEllipsis": "發佈…", + "resourcesHint": "雙擊 / Enter = 主操作 · E = 標註 · Ctrl+C = 複製", + "libCount": "{n} 項", + "draftEmpty": "還沒有草稿圖片。截到收件匣後按 E。", + "publishedEmpty": "還沒有已發佈的模板。先標註草稿,再點「發佈…」。", + "unpublishedCount": "{n} 項未發佈", + "allPublished": "與已發佈版本一致", + "inspectorSelected": "選中" } diff --git a/media/resources/app.js b/media/resources/app.js new file mode 100644 index 00000000..895d1c47 --- /dev/null +++ b/media/resources/app.js @@ -0,0 +1,184 @@ +(function () { + const vscode = acquireVsCodeApi(); + const I18N = JSON.parse(document.getElementById('resourcesI18n')?.textContent || '{}'); + const t = (k, a = {}) => (I18N[k] || k).replace(/\{(\w+)\}/g, (_, n) => String(a[n] ?? '{' + n + '}')); + + const state = { + mode: 'draft', + kind: 'template', + inbox: [], + items: [], + thumbs: {}, + statuses: {}, + unpublished: 0, + query: '', + }; + + const els = { + inboxTitle: document.getElementById('inboxTitle'), + inboxMeta: document.getElementById('inboxMeta'), + inboxStrip: document.getElementById('inboxStrip'), + inboxEmpty: document.getElementById('inboxEmpty'), + tabDraft: document.getElementById('tabDraft'), + tabPublished: document.getElementById('tabPublished'), + libMeta: document.getElementById('libMeta'), + search: document.getElementById('search'), + hint: document.getElementById('hint'), + grid: document.getElementById('grid'), + libEmpty: document.getElementById('libEmpty'), + unpubText: document.getElementById('unpubText'), + publishBtn: document.getElementById('publishBtn'), + kindTemplate: document.getElementById('kindTemplate'), + kindRect: document.getElementById('kindRect'), + kindPoint: document.getElementById('kindPoint'), + }; + + els.inboxTitle.textContent = t('inboxTitle'); + els.tabDraft.textContent = t('modeDraft'); + els.tabPublished.textContent = t('modePublished'); + els.kindTemplate.textContent = t('kindTemplate'); + els.kindRect.textContent = t('kindRect'); + els.kindPoint.textContent = t('kindPoint'); + els.search.placeholder = t('resourcesSearch'); + els.publishBtn.textContent = t('publishEllipsis'); + els.hint.textContent = t('resourcesHint'); + els.inboxEmpty.textContent = t('inboxEmpty'); + + function setSeg(container, attr, value) { + container.querySelectorAll('.seg__btn').forEach((b) => { + b.classList.toggle('is-active', b.getAttribute(attr) === value); + }); + } + + function renderInbox() { + els.inboxMeta.textContent = t('inboxCount', { n: state.inbox.length, max: 10 }); + els.inboxStrip.replaceChildren(); + els.inboxEmpty.hidden = state.inbox.length > 0; + for (const shot of state.inbox) { + const card = document.createElement('div'); + card.className = 'res-inbox__card'; + card.role = 'option'; + card.tabIndex = 0; + card.dataset.id = shot.id; + const img = document.createElement('img'); + img.alt = ''; + img.src = shot.url || ''; + const cap = document.createElement('span'); + cap.textContent = shot.name || shot.id; + card.append(img, cap); + card.addEventListener('dblclick', () => vscode.postMessage({ type: 'annotateInbox', id: shot.id })); + card.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === 'e' || e.key === 'E') { + e.preventDefault(); + vscode.postMessage({ type: 'annotateInbox', id: shot.id }); + } + if (e.key === 'Delete') vscode.postMessage({ type: 'deleteInbox', id: shot.id }); + }); + els.inboxStrip.appendChild(card); + } + } + + function filtered() { + const q = state.query.trim().toLowerCase(); + return state.items.filter((it) => { + if (state.kind !== 'template' && it.kind && it.kind !== state.kind) return false; + if (!q) return true; + return (it.name || '').toLowerCase().includes(q) || (it.expression || '').toLowerCase().includes(q); + }); + } + + function renderGrid() { + const items = filtered(); + els.libMeta.textContent = t('libCount', { n: items.length }); + els.grid.replaceChildren(); + els.libEmpty.hidden = items.length > 0; + if (!items.length) { + els.libEmpty.hidden = false; + els.libEmpty.textContent = state.mode === 'draft' ? t('draftEmpty') : t('publishedEmpty'); + } + ThumbnailActions.bindGrid(els.grid, t('resourcesTitle')); + for (const it of items) { + const card = document.createElement('div'); + card.className = 'res-card'; + card.dataset.name = it.name; + const dot = document.createElement('span'); + const st = state.statuses[it.name] || (state.mode === 'published' ? 'published' : 'draft-only'); + dot.className = 'res-dot is-' + st; + dot.title = st; + const thumb = document.createElement('div'); + thumb.className = 'res-card__thumb'; + const url = state.thumbs[it.name]; + if (url) { + const img = document.createElement('img'); + img.alt = ''; + img.src = url; + thumb.appendChild(img); + } + const meta = document.createElement('div'); + meta.className = 'res-card__meta'; + const name = document.createElement('div'); + name.className = 'res-card__name'; + name.textContent = it.name; + const sub = document.createElement('div'); + sub.className = 'res-card__sub'; + sub.textContent = it.expression || `${it.width || '?'}×${it.height || '?'}`; + meta.append(name, sub); + card.append(dot, thumb, meta); + ThumbnailActions.bindCard(card, { + primary: () => vscode.postMessage({ + type: state.mode === 'draft' ? 'openAnnotation' : 'insert', + name: it.name, + expression: it.expression, + imagePath: it.imagePath, + }), + edit: () => vscode.postMessage({ type: 'openAnnotation', name: it.name, imagePath: it.imagePath }), + copy: () => vscode.postMessage({ type: 'copy', expression: it.expression || it.name }), + }); + els.grid.appendChild(card); + } + ThumbnailActions.refreshTabStop(els.grid); + els.unpubText.textContent = state.unpublished + ? t('unpublishedCount', { n: state.unpublished }) + : t('allPublished'); + } + + document.getElementById('modeTabs').addEventListener('click', (e) => { + const btn = e.target.closest('[data-mode]'); + if (!btn) return; + state.mode = btn.getAttribute('data-mode'); + setSeg(document.getElementById('modeTabs'), 'data-mode', state.mode); + vscode.postMessage({ type: 'setMode', mode: state.mode }); + }); + document.getElementById('kindTabs').addEventListener('click', (e) => { + const btn = e.target.closest('[data-kind]'); + if (!btn) return; + state.kind = btn.getAttribute('data-kind'); + setSeg(document.getElementById('kindTabs'), 'data-kind', state.kind); + renderGrid(); + }); + els.search.addEventListener('input', () => { state.query = els.search.value; renderGrid(); }); + els.publishBtn.addEventListener('click', () => vscode.postMessage({ type: 'publish' })); + + window.addEventListener('message', (e) => { + const msg = e.data; + if (!msg || !msg.type) return; + if (msg.type === 'inbox') { + state.inbox = msg.items || []; + renderInbox(); + } + if (msg.type === 'library') { + state.mode = msg.mode || state.mode; + state.items = msg.items || []; + state.statuses = msg.statuses || {}; + state.unpublished = msg.unpublished || 0; + setSeg(document.getElementById('modeTabs'), 'data-mode', state.mode); + renderGrid(); + } + if (msg.type === 'thumbs') { + for (const it of (msg.items || [])) state.thumbs[it.name] = it.url; + renderGrid(); + } + }); + + vscode.postMessage({ type: 'ready' }); +})(); diff --git a/media/resources/index.html b/media/resources/index.html new file mode 100644 index 00000000..806d99fa --- /dev/null +++ b/media/resources/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+
+
+
+ + +
+ +
+
+ +
+ + + +
+
+
+
+ +
+ + +
+
+
+ + + + + + diff --git a/media/resources/resources.css b/media/resources/resources.css new file mode 100644 index 00000000..7ec5b4f5 --- /dev/null +++ b/media/resources/resources.css @@ -0,0 +1,47 @@ +body { margin: 0; background: var(--bg-page); color: var(--text-primary); } +.res { display: flex; flex-direction: column; gap: var(--space-md); padding: var(--space-sm); min-height: 100vh; box-sizing: border-box; } +.res-sec-head { display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-xs); } +.res-sec-title { font-weight: 650; font-size: var(--font-sm); } +.res-sec-meta { color: var(--muted); font-size: var(--font-xs); margin-left: auto; } +.res-inbox__strip { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 4px; min-height: 64px; } +.res-inbox__card { + flex: 0 0 auto; width: 88px; border: 1px solid var(--border); border-radius: var(--radius-md); + background: var(--surface); overflow: hidden; cursor: pointer; +} +.res-inbox__card.is-selected { border-color: var(--border-strong); box-shadow: 0 0 0 1px var(--border-strong); } +.res-inbox__card img { display: block; width: 88px; height: 50px; object-fit: cover; background: var(--bg-row); } +.res-inbox__card span { display: block; padding: 2px 4px; font-size: var(--font-2xs); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-pill); overflow: hidden; background: var(--bg-control); } +.seg__btn { border: 0; background: transparent; color: var(--text-primary); padding: 3px 10px; font-size: var(--font-xs); cursor: pointer; } +.seg__btn.is-active { background: var(--bg-control-active); font-weight: 600; } +.seg--sm .seg__btn { padding: 2px 8px; } +.res-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-bottom: var(--space-xs); } +.res-toolbar .control-input { flex: 1 1 120px; min-width: 0; } +.res-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; +} +.res-card { + border: 1px solid color-mix(in srgb, var(--border) 55%, transparent); border-radius: var(--radius-lg); + background: var(--surface); overflow: hidden; position: relative; +} +.res-card.is-selected { border-color: var(--border-strong); box-shadow: 0 0 0 1px var(--border-strong); } +.res-card__thumb { aspect-ratio: 16/10; background: var(--bg-row); display: flex; align-items: center; justify-content: center; } +.res-card__thumb img { max-width: 100%; max-height: 100%; object-fit: contain; } +.res-card__meta { padding: 4px 6px 6px; } +.res-card__name { font-size: var(--font-xs); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.res-card__sub { color: var(--muted); font-size: var(--font-2xs); } +.res-dot { position: absolute; top: 6px; left: 6px; width: 8px; height: 8px; border-radius: 50%; } +.res-dot.is-draft-only { background: var(--muted); } +.res-dot.is-modified { background: var(--warn); } +.res-dot.is-published { background: var(--ok); } +.res-dot.is-orphan-published { background: var(--err); } +.res-foot { + display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-sm); + padding-top: var(--space-sm); border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + font-size: var(--font-xs); color: var(--muted); +} +.res-foot #publishBtn { margin-left: auto; } +.panel-hint { color: var(--muted); font-size: var(--font-xs); margin-bottom: var(--space-xs); } +@media (max-width: 300px) { + .res-grid { grid-template-columns: 1fr; } +} diff --git a/package.json b/package.json index 9ae816e5..0e8109e2 100644 --- a/package.json +++ b/package.json @@ -163,6 +163,59 @@ "title": "%command.console.disconnectGame%", "category": "%command.category%", "icon": "$(debug-disconnect)" + }, + { + "command": "okScriptToolkit.captureToInbox", + "title": "%command.captureToInbox%", + "category": "%command.category%", + "icon": "$(device-camera)" + }, + { + "command": "okScriptToolkit.annotateImage", + "title": "%command.annotateImage%", + "category": "%command.category%", + "icon": "$(edit)" + }, + { + "command": "okScriptToolkit.publishResources", + "title": "%command.publishResources%", + "category": "%command.category%", + "icon": "$(cloud-upload)" + }, + { + "command": "okScriptToolkit.insertResourceReference", + "title": "%command.insertResourceReference%", + "category": "%command.category%", + "icon": "$(add)" + }, + { + "command": "okScriptToolkit.revealResource", + "title": "%command.revealResource%", + "category": "%command.category%", + "icon": "$(search)" + }, + { + "command": "okScriptToolkit.run.openParams", + "title": "%command.run.openParams%", + "category": "%command.category%", + "icon": "$(settings-gear)" + }, + { + "command": "okScriptToolkit.run.runTaskInline", + "title": "%command.run.runTaskInline%", + "category": "%command.category%", + "icon": "$(play)" + }, + { + "command": "okScriptToolkit.run.toggleTrigger", + "title": "%command.run.toggleTrigger%", + "category": "%command.category%" + }, + { + "command": "okScriptToolkit.pickProject", + "title": "%command.pickProject%", + "category": "%command.category%", + "icon": "$(folder)" } ], "walkthroughs": [ @@ -243,64 +296,32 @@ "viewsContainers": { "activitybar": [ { - "id": "okTools", - "title": "%viewContainer.tools.title%", + "id": "okScript", + "title": "%viewContainer.okScript.title%", "icon": "media/icons/toolbox.svg" - }, - { - "id": "okTemplates", - "title": "%viewContainer.templates.title%", - "icon": "media/icons/templates.svg" - }, - { - "id": "okTempShots", - "title": "%viewContainer.tempShots.title%", - "icon": "media/icons/tempShots.svg" } ] }, "views": { - "okTools": [ - { - "type": "webview", - "id": "okScriptToolkit.console", - "name": "%view.console%", - "title": "%view.console.title%", - "icon": "media/icons/toolbox.svg", - "webviewOptions": { - "retainContextWhenHidden": true - } - } - ], - "okTemplates": [ + "okScript": [ { - "type": "webview", - "id": "okScriptToolkit.templateGallery", - "name": "%view.templateGallery%", - "title": "%view.templateGallery.title%", - "icon": "media/icons/templates.svg", - "webviewOptions": { - "retainContextWhenHidden": true - } + "id": "okScriptToolkit.run", + "name": "%view.run%", + "type": "tree" }, { "type": "webview", - "id": "okScriptToolkit.templateAssets", - "name": "%view.templateAssets%", - "title": "%view.templateAssets.title%", - "icon": "media/icons/templates.svg", + "id": "okScriptToolkit.params", + "name": "%view.params%", + "visibility": "collapsed", "webviewOptions": { "retainContextWhenHidden": true } - } - ], - "okTempShots": [ + }, { "type": "webview", - "id": "okScriptToolkit.tempScreenshots", - "name": "%view.tempScreenshots%", - "title": "%view.tempScreenshots.title%", - "icon": "media/icons/tempShots.svg", + "id": "okScriptToolkit.resources", + "name": "%view.resources%", "webviewOptions": { "retainContextWhenHidden": true } @@ -315,7 +336,7 @@ "when": "editorLangId == python" }, { - "command": "okScriptToolkit.screenshotToTemplate", + "command": "okScriptToolkit.captureToInbox", "key": "ctrl+alt+s", "mac": "cmd+alt+s" } @@ -570,91 +591,97 @@ { "command": "okScriptToolkit.openCharacterManager", "when": "okScriptToolkit.hasCharacterData" - } - ], - "view/title": [ - { - "command": "okScriptToolkit.console.runTask", - "when": "view == okScriptToolkit.console", - "group": "navigation@1" }, { - "command": "okScriptToolkit.console.startExecutor", - "when": "view == okScriptToolkit.console && !okScriptToolkit.executorRunning", - "group": "navigation@2" + "command": "okScriptToolkit.captureToInbox", + "when": "okScriptToolkit.projectReady" }, { - "command": "okScriptToolkit.console.pause", - "when": "view == okScriptToolkit.console && okScriptToolkit.executorRunning && !okScriptToolkit.executorPaused", - "group": "navigation@2" + "command": "okScriptToolkit.annotateImage", + "when": "okScriptToolkit.projectReady" }, { - "command": "okScriptToolkit.console.resume", - "when": "view == okScriptToolkit.console && okScriptToolkit.executorPaused", - "group": "navigation@2" + "command": "okScriptToolkit.publishResources", + "when": "okScriptToolkit.projectReady" }, { - "command": "okScriptToolkit.console.stopExecutor", - "when": "view == okScriptToolkit.console && okScriptToolkit.executorRunning", - "group": "navigation@3" + "command": "okScriptToolkit.pickProject", + "when": "okScriptToolkit.projectReady" }, { - "command": "okScriptToolkit.console.refresh", - "when": "view == okScriptToolkit.console", - "group": "navigation@4" + "command": "okScriptToolkit.run.openParams", + "when": "false" }, { - "command": "okScriptToolkit.console.stopCurrentTask", - "when": "view == okScriptToolkit.console && okScriptToolkit.taskRunning", - "group": "1_run@1" + "command": "okScriptToolkit.run.runTaskInline", + "when": "false" }, { - "command": "okScriptToolkit.console.showLogs", - "when": "view == okScriptToolkit.console", - "group": "2_view@1" + "command": "okScriptToolkit.run.toggleTrigger", + "when": "false" }, { - "command": "okScriptToolkit.openCharacterManager", - "when": "view == okScriptToolkit.console && okScriptToolkit.hasCharacterData", - "group": "2_view@2" + "command": "okScriptToolkit.insertResourceReference", + "when": "false" + } + ], + "view/title": [ + { + "command": "okScriptToolkit.console.startExecutor", + "when": "view == okScriptToolkit.run", + "group": "navigation@1" }, { "command": "okScriptToolkit.console.connectGame", - "when": "view == okScriptToolkit.console", - "group": "3_game@1" + "when": "view == okScriptToolkit.run && !okScriptToolkit.executorRunning", + "group": "navigation@2" }, { - "command": "okScriptToolkit.console.disconnectGame", - "when": "view == okScriptToolkit.console", - "group": "3_game@2" + "command": "okScriptToolkit.console.refresh", + "when": "view == okScriptToolkit.run", + "group": "navigation@3" }, { - "command": "okScriptToolkit.openTemplatesEditor", - "when": "view == okScriptToolkit.templateGallery", - "group": "navigation@1" + "command": "okScriptToolkit.console.showLogs", + "when": "view == okScriptToolkit.run", + "group": "navigation@4" }, { - "command": "okScriptToolkit.screenshotToTemplate", - "when": "view == okScriptToolkit.templateAssets", + "command": "okScriptToolkit.captureToInbox", + "when": "view == okScriptToolkit.resources", "group": "navigation@1" }, { - "command": "okScriptToolkit.openAnnotationEditor", - "when": "view == okScriptToolkit.templateAssets", + "command": "okScriptToolkit.publishResources", + "when": "view == okScriptToolkit.resources", "group": "navigation@2" + } + ], + "view/item/context": [ + { + "command": "okScriptToolkit.run.runTaskInline", + "when": "view == okScriptToolkit.run && viewItem == run.task.onetime", + "group": "inline@1" }, { - "command": "okScriptToolkit.openResourcePublisher", - "when": "view == okScriptToolkit.templateAssets", - "group": "navigation@3" + "command": "okScriptToolkit.run.openParams", + "when": "view == okScriptToolkit.run && viewItem =~ /^run\\.task/", + "group": "inline@2" }, { - "command": "okScriptToolkit.openTemplateAssets", - "when": "view == okScriptToolkit.templateAssets", - "group": "navigation@4" + "command": "okScriptToolkit.console.stopCurrentTask", + "when": "view == okScriptToolkit.run && viewItem == run.task.running", + "group": "inline@1" } ] - } + }, + "viewsWelcome": [ + { + "view": "okScriptToolkit.run", + "when": "!okScriptToolkit.projectReady", + "contents": "%viewsWelcome.run%" + } + ] }, "scripts": { "compile": "tsc -p ./", @@ -663,7 +690,7 @@ "verify:version": "node scripts/release/verify-version.js", "verify:l10n": "node scripts/release/verify-l10n.js", "release": "sh scripts/release.sh", -"test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", + "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", "test:webview-html": "node scripts/test_webview_html.js", "test:design-system": "node scripts/test_design_system.js", "test:thumbnail-actions": "node scripts/test_thumbnail_actions.js", @@ -715,7 +742,10 @@ "test:position-publish-settings": "node scripts/test_position_publish_settings.js", "test:webview-l10n": "node scripts/test_webview_l10n.js", "test:resource-host-contract": "node scripts/test_resource_host_contract.js", - "test:command-contract": "node scripts/test_command_contract.js" + "test:command-contract": "node scripts/test_command_contract.js", + "test:run-tree-model": "node scripts/test_run_tree_model.js", + "test:publish-diff": "node scripts/test_publish_diff.js", + "test:character-diagnostics": "node scripts/test_character_diagnostics.js" }, "devDependencies": { "@types/jpeg-js": "^0.3.0", diff --git a/package.nls.es.json b/package.nls.es.json index 94e89522..80b033ec 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -77,5 +77,19 @@ "command.console.stopExecutor": "Consola: detener ejecutor", "command.console.showLogs": "Consola: mostrar registros", "command.console.connectGame": "Consola: conectar juego", - "command.console.disconnectGame": "Consola: desconectar juego" + "command.console.disconnectGame": "Consola: desconectar juego", + "viewContainer.okScript.title": "ok-script", + "view.run": "Ejecutar", + "view.params": "Parámetros", + "view.resources": "Recursos", + "viewsWelcome.run": "Abre una carpeta de proyecto ok-script (con src/config.py) o [elige una ruta](command:okScriptToolkit.pickProject).", + "command.captureToInbox": "Capturar a la bandeja", + "command.annotateImage": "Anotar imagen…", + "command.publishResources": "Publicar recursos…", + "command.insertResourceReference": "Insertar referencia", + "command.revealResource": "Mostrar recurso", + "command.run.openParams": "Abrir parámetros", + "command.run.runTaskInline": "Ejecutar tarea", + "command.run.toggleTrigger": "Alternar disparador", + "command.pickProject": "Elegir carpeta del proyecto…" } diff --git a/package.nls.ja.json b/package.nls.ja.json index 1c3a054c..dbbd7a49 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -77,5 +77,19 @@ "command.console.stopExecutor": "コンソール: エグゼキューターを停止", "command.console.showLogs": "コンソール: ログを表示", "command.console.connectGame": "コンソール: ゲームに接続", - "command.console.disconnectGame": "コンソール: ゲームから切断" + "command.console.disconnectGame": "コンソール: ゲームから切断", + "viewContainer.okScript.title": "ok-script", + "view.run": "実行", + "view.params": "パラメータ", + "view.resources": "リソース", + "viewsWelcome.run": "src/config.py のある ok-script プロジェクトフォルダーを開くか、[プロジェクトパスを選択](command:okScriptToolkit.pickProject)してください。", + "command.captureToInbox": "受信箱にキャプチャ", + "command.annotateImage": "画像を注釈…", + "command.publishResources": "リソースを公開…", + "command.insertResourceReference": "リソース参照を挿入", + "command.revealResource": "リソースを表示", + "command.run.openParams": "パラメータを開く", + "command.run.runTaskInline": "タスクを実行", + "command.run.toggleTrigger": "トリガー切替", + "command.pickProject": "ok-script プロジェクトフォルダーを選択…" } diff --git a/package.nls.json b/package.nls.json index 3db709c7..41740a6c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -77,5 +77,19 @@ "command.console.stopExecutor": "Console: Stop Executor", "command.console.showLogs": "Console: Show Logs", "command.console.connectGame": "Console: Connect Game", - "command.console.disconnectGame": "Console: Disconnect Game" + "command.console.disconnectGame": "Console: Disconnect Game", + "viewContainer.okScript.title": "ok-script", + "view.run": "Run", + "view.params": "Parameters", + "view.resources": "Resources", + "viewsWelcome.run": "Open an ok-script project folder (with src/config.py), or [choose a project path](command:okScriptToolkit.pickProject).", + "command.captureToInbox": "Capture to Inbox", + "command.annotateImage": "Annotate Image…", + "command.publishResources": "Publish Resources…", + "command.insertResourceReference": "Insert Resource Reference", + "command.revealResource": "Reveal Resource", + "command.run.openParams": "Open Parameters", + "command.run.runTaskInline": "Run Task", + "command.run.toggleTrigger": "Toggle Trigger", + "command.pickProject": "Choose ok-script Project Folder…" } diff --git a/package.nls.ko.json b/package.nls.ko.json index 10bdfc6a..8d48409c 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -77,5 +77,19 @@ "command.console.stopExecutor": "콘솔: 실행기 종료", "command.console.showLogs": "콘솔: 로그 보기", "command.console.connectGame": "콘솔: 게임 연결", - "command.console.disconnectGame": "콘솔: 게임 연결 해제" + "command.console.disconnectGame": "콘솔: 게임 연결 해제", + "viewContainer.okScript.title": "ok-script", + "view.run": "실행", + "view.params": "매개 변수", + "view.resources": "리소스", + "viewsWelcome.run": "src/config.py 가 있는 ok-script 프로젝트 폴더를 열거나 [프로젝트 경로 선택](command:okScriptToolkit.pickProject).", + "command.captureToInbox": "받은 편지로 캡처", + "command.annotateImage": "이미지 주석…", + "command.publishResources": "리소스 게시…", + "command.insertResourceReference": "리소스 참조 삽입", + "command.revealResource": "리소스 표시", + "command.run.openParams": "매개 변수 열기", + "command.run.runTaskInline": "작업 실행", + "command.run.toggleTrigger": "트리거 전환", + "command.pickProject": "ok-script 프로젝트 폴더 선택…" } diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index cf3fd7dc..89917360 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -77,5 +77,19 @@ "command.console.stopExecutor": "控制台:关闭执行器", "command.console.showLogs": "控制台:查看日志", "command.console.connectGame": "控制台:连接游戏", - "command.console.disconnectGame": "控制台:断开游戏" + "command.console.disconnectGame": "控制台:断开游戏", + "viewContainer.okScript.title": "ok-script", + "view.run": "运行", + "view.params": "参数", + "view.resources": "资源", + "viewsWelcome.run": "打开含 src/config.py 的 ok-script 项目文件夹,或 [选择项目路径](command:okScriptToolkit.pickProject)。", + "command.captureToInbox": "截到收件箱", + "command.annotateImage": "标注图片…", + "command.publishResources": "发布资源…", + "command.insertResourceReference": "插入资源引用", + "command.revealResource": "在资源中定位", + "command.run.openParams": "打开参数", + "command.run.runTaskInline": "运行任务", + "command.run.toggleTrigger": "切换触发启用", + "command.pickProject": "选择 ok-script 项目目录…" } diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index d6f112f6..561a40ae 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -77,5 +77,19 @@ "command.console.stopExecutor": "控制台:關閉執行器", "command.console.showLogs": "控制台:查看日誌", "command.console.connectGame": "控制台:連線遊戲", - "command.console.disconnectGame": "控制台:中斷遊戲連線" + "command.console.disconnectGame": "控制台:中斷遊戲連線", + "viewContainer.okScript.title": "ok-script", + "view.run": "執行", + "view.params": "參數", + "view.resources": "資源", + "viewsWelcome.run": "打開含 src/config.py 的 ok-script 專案資料夾,或 [選擇專案路徑](command:okScriptToolkit.pickProject)。", + "command.captureToInbox": "截到收件匣", + "command.annotateImage": "標註圖片…", + "command.publishResources": "發佈資源…", + "command.insertResourceReference": "插入資源引用", + "command.revealResource": "在資源中定位", + "command.run.openParams": "開啟參數", + "command.run.runTaskInline": "執行任務", + "command.run.toggleTrigger": "切換觸發啟用", + "command.pickProject": "選擇 ok-script 專案目錄…" } diff --git a/scripts/test_design_system.js b/scripts/test_design_system.js index e893d4a4..125cdcb6 100644 --- a/scripts/test_design_system.js +++ b/scripts/test_design_system.js @@ -30,6 +30,8 @@ const PANEL_HOST = { tempScreenshots: 'src/tempScreenshotPanel.ts', annotationPanel: 'src/annotationPanel.ts', characterManager: 'src/characterPanel.ts', + params: 'src/run/paramsView.ts', + resources: 'src/run/resourcesView.ts', }; const read = (p) => fs.readFileSync(p, 'utf8'); diff --git a/src/extension.ts b/src/extension.ts index ce4953a2..28f5b391 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,6 +23,13 @@ import { repaintAllGalleries, } from './templatePanel'; import { ConsoleViewProvider, ExecutorStateSummary } from './consolePanel'; +import { RunTreeDataProvider } from './run/runTree'; +import { ParamsViewProvider } from './run/paramsView'; +import { ResourcesViewProvider } from './run/resourcesView'; +import type { RunTreeNode } from './run/treeModel'; +import { issuesToDiagnostics } from './run/characterDiagnostics'; +import { configuredCharacterDataPaths, loadCharacterManagerData } from './characterData'; +import { insertIntoPythonEditor } from './pythonEditor'; import { CharacterManagerPanel, detectCharacterData } from './characterPanel'; import { GameConnectService } from './toolboxConnect'; import { TemplateAssetData } from './templateAssetData'; @@ -293,6 +300,18 @@ export function activate(context: vscode.ExtensionContext): void { path.join(context.globalStorageUri.fsPath, 'temp-screenshots', wsHash), ); tempScreenshotStore.ensure(); + const runTree = new RunTreeDataProvider(okConsole); + const paramsView = new ParamsViewProvider(context.extensionUri, okConsole, runTree); + const resourcesView = new ResourcesViewProvider( + context.extensionUri, templateAssetData, features, tempScreenshotStore, thumbDir, + ); + void okConsole.activateHeadless(); + const runTreeView = vscode.window.createTreeView(RunTreeDataProvider.viewId, { + treeDataProvider: runTree, + showCollapseAll: true, + }); + runTree.attachTreeView(runTreeView); + runTree.refresh(); const tempThumbDir = path.join(context.globalStorageUri.fsPath, 'temp-thumbs', wsHash); if (context.globalState.get(LEGACY_THUMB_PURGE_KEY) !== THUMB_KEY_VERSION) { purgeLegacyThumbFiles(thumbDir); @@ -334,10 +353,40 @@ export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push(executorStatusItem, okConsole.onDidChangeExecutorState(renderExecutorStatus)); // ── 角色数据检测 → 隐藏/露出「角色技能管理」入口 ── + const characterDiagnostics = vscode.languages.createDiagnosticCollection('okScriptToolkit.characters'); + context.subscriptions.push(characterDiagnostics); const updateCharacterDataContext = () => { const available = detectCharacterData(); void vscode.commands.executeCommand('setContext', 'okScriptToolkit.hasCharacterData', available); okConsole.setCharacterDataAvailable(available); + characterDiagnostics.clear(); + if (!available) return; + try { + const projectDir = resolveProjectDir(); + const paths = configuredCharacterDataPaths(projectDir); + const loaded = loadCharacterManagerData(paths); + const mapped = issuesToDiagnostics(loaded.snapshot.issues || [], { + masterFile: paths.masterFile, + localeFile: paths.localeFile, + characterDir: paths.skillsDir, + }); + const byFile = new Map(); + for (const item of mapped) { + const sev = item.severity === 'error' ? vscode.DiagnosticSeverity.Error + : item.severity === 'warning' ? vscode.DiagnosticSeverity.Warning + : vscode.DiagnosticSeverity.Information; + const diag = new vscode.Diagnostic(new vscode.Range(item.line, item.character, item.line, item.character + 1), item.message, sev); + diag.source = 'ok-script'; + const list = byFile.get(item.file) || []; + list.push(diag); + byFile.set(item.file, list); + } + for (const [file, diags] of byFile) { + characterDiagnostics.set(vscode.Uri.file(file), diags); + } + } catch { + /* 诊断失败不阻塞主流程 */ + } }; updateCharacterDataContext(); const characterDataWatcher = vscode.workspace.createFileSystemWatcher('**/assets/data/{characters.json,character_skills}'); @@ -394,30 +443,20 @@ export function activate(context: vscode.ExtensionContext): void { { language: 'jsonc', scheme: 'file' }, jsonInlay, ), - vscode.window.registerWebviewViewProvider( - TemplateGalleryViewProvider.viewType, - new TemplateGalleryViewProvider(context.extensionUri, features, thumbDir), - ), - vscode.window.registerWebviewViewProvider( - ConsoleViewProvider.viewType, - okConsole, - ), - vscode.window.registerWebviewViewProvider( - TemplateAssetViewProvider.viewType, - new TemplateAssetViewProvider(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore), - ), - vscode.window.registerWebviewViewProvider( - TempScreenshotViewProvider.viewType, - new TempScreenshotViewProvider(tempScreenshotStore, templateAssetData, tempThumbDir, context.extensionUri), - ), + runTreeView, + runTree, + paramsView, + resourcesView, + vscode.window.registerWebviewViewProvider(ParamsViewProvider.viewType, paramsView), + vscode.window.registerWebviewViewProvider(ResourcesViewProvider.viewType, resourcesView), vscode.commands.registerCommand('okScriptToolkit.showTemplates', () => { - void vscode.commands.executeCommand(`${TemplateGalleryViewProvider.viewType}.focus`); + void vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); }), vscode.commands.registerCommand('okScriptToolkit.openTemplatesEditor', () => { TemplateGalleryPanel.show(features, thumbDir, context.extensionUri); }), vscode.commands.registerCommand('okScriptToolkit.showTaskLauncher', () => { - void vscode.commands.executeCommand(`${ConsoleViewProvider.viewType}.focus`); + void vscode.commands.executeCommand(`${RunTreeDataProvider.viewId}.focus`); }), vscode.commands.registerCommand('okScriptToolkit.openCharacterManager', () => { CharacterManagerPanel.show(characterManagerDependencies); @@ -433,13 +472,42 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.registerCommand('okScriptToolkit.console.connectGame', () => gameConnect.connect()), vscode.commands.registerCommand('okScriptToolkit.console.disconnectGame', () => gameConnect.disconnect()), vscode.commands.registerCommand('okScriptToolkit.openTemplateAssets', () => { - TemplateAssetPanel.show(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore); + void vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); }), vscode.commands.registerCommand('okScriptToolkit.screenshotToTemplate', () => { TemplateAssetPanel.showScreenshot(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore); }), vscode.commands.registerCommand('okScriptToolkit.showTempScreenshots', () => { - void vscode.commands.executeCommand(`${TempScreenshotViewProvider.viewType}.focus`); + void vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); + }), + vscode.commands.registerCommand('okScriptToolkit.captureToInbox', () => resourcesView.captureToInbox()), + vscode.commands.registerCommand('okScriptToolkit.publishResources', () => + TemplateAssetPanel.publish(templateAssetData, thumbDir, context.extensionUri, tempScreenshotStore)), + vscode.commands.registerCommand('okScriptToolkit.annotateImage', (target?: vscode.Uri | string) => + vscode.commands.executeCommand('okScriptToolkit.openAnnotationEditor', target)), + vscode.commands.registerCommand('okScriptToolkit.insertResourceReference', async (expression?: string) => { + const text = expression || await vscode.window.showInputBox({ prompt: tr('Resource expression (e.g. fL.btn_claim)') }); + if (text) insertIntoPythonEditor(text); + }), + vscode.commands.registerCommand('okScriptToolkit.revealResource', async (name?: string) => { + void vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); + if (name) void vscode.window.showInformationMessage(tr('Locate “{name}” in Resources', { name })); + }), + vscode.commands.registerCommand('okScriptToolkit.pickProject', async () => { + const picked = await vscode.window.showOpenDialog({ + canSelectFiles: false, canSelectFolders: true, canSelectMany: false, + openLabel: tr('Use as ok-script project'), + }); + if (picked?.[0]) { + await vscode.workspace.getConfiguration('okScriptToolkit').update('okScriptProjectPath', picked[0].fsPath, vscode.ConfigurationTarget.Workspace); + } + }), + vscode.commands.registerCommand('okScriptToolkit.run.openParams', (node?: RunTreeNode) => paramsView.openForNode(node)), + vscode.commands.registerCommand('okScriptToolkit.run.runTaskInline', async (node?: RunTreeNode) => { + if (node?.taskKey) await okConsole.enqueueByKey(node.taskKey); + }), + vscode.commands.registerCommand('okScriptToolkit.run.toggleTrigger', async (node?: RunTreeNode) => { + if (node?.taskKey) await okConsole.setTriggerEnabledByKey(node.taskKey, !node.checked); }), vscode.commands.registerCommand('okScriptToolkit.openAnnotationEditor', async (target?: vscode.Uri | string) => { // 资源管理器右键 / 带参数调用时直接打开;否则让用户从标注管理的图片里选一张。 diff --git a/src/run/resourcesView.ts b/src/run/resourcesView.ts new file mode 100644 index 00000000..dfa0f678 --- /dev/null +++ b/src/run/resourcesView.ts @@ -0,0 +1,234 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { FeatureData } from '../featureData'; +import { injectWebviewLocalization, tr } from '../localization'; +import { AnnotationPanel } from '../annotationPanel'; +import { TemplateAssetData } from '../templateAssetData'; +import { TemplateAssetPanel } from '../templateAssetPanel'; +import { TempScreenshotStore } from '../tempScreenshotStore'; +import { applySharedAssets, errorPage, getNonce } from '../webviewHtml'; +import { countUnpublished, diffPublishSets, fingerprintBox } from './publishDiff'; +import { primaryFeatureAlias } from '../templatePanel'; +import { templatesDirectory } from '../projectConfig'; + +type LibraryMode = 'draft' | 'published'; + +/** Unified Resources view: inbox (temp shots) + draft/published library. */ +export class ResourcesViewProvider implements vscode.WebviewViewProvider, vscode.Disposable { + static readonly viewType = 'okScriptToolkit.resources'; + + private view?: vscode.WebviewView; + private mode: LibraryMode = 'draft'; + private readonly subs: vscode.Disposable[] = []; + + constructor( + private readonly extensionUri: vscode.Uri, + private readonly assets: TemplateAssetData, + private readonly features: FeatureData, + private readonly tempStore: TempScreenshotStore, + private readonly thumbDir: string, + ) { + this.subs.push(tempStore.onChange(() => void this.pushInbox())); + } + + resolveWebviewView(view: vscode.WebviewView): void { + this.view = view; + view.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this.extensionUri, 'media', 'resources'), + vscode.Uri.joinPath(this.extensionUri, 'media', 'shared'), + vscode.Uri.file(this.tempStore.directory), + vscode.Uri.file(this.thumbDir), + vscode.Uri.file(this.assets.root || ''), + ].filter((u) => u.fsPath), + }; + view.webview.html = this.buildHtml(view.webview); + view.webview.onDidReceiveMessage(async (msg) => { + switch (msg?.type) { + case 'ready': + await this.pushInbox(); + await this.pushLibrary(); + break; + case 'setMode': + this.mode = msg.mode === 'published' ? 'published' : 'draft'; + await this.pushLibrary(); + break; + case 'annotateInbox': + if (msg.id) await this.annotateInbox(String(msg.id)); + break; + case 'deleteInbox': + if (msg.id) this.tempStore.remove(String(msg.id)); + break; + case 'openAnnotation': + await this.openAnnotation(msg.imagePath || msg.name); + break; + case 'insert': + if (msg.expression) { + await vscode.commands.executeCommand('okScriptToolkit.insertResourceReference', String(msg.expression)); + } + break; + case 'copy': + if (msg.expression) await vscode.env.clipboard.writeText(String(msg.expression)); + break; + case 'publish': + await TemplateAssetPanel.publish(this.assets, this.thumbDir, this.extensionUri, this.tempStore); + await this.pushLibrary(); + break; + default: + break; + } + }); + } + + async captureToInbox(): Promise { + const { captureGameWindow } = await import('../screenshotCapture'); + const out = this.tempStore.newFilePath(); + const result = await captureGameWindow(out); + if (!result.ok) { + void vscode.window.showErrorMessage(result.error || tr('Screenshot failed')); + return; + } + this.tempStore.register(out); + void vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); + await this.pushInbox(); + } + + private async annotateInbox(id: string): Promise { + const shot = this.tempStore.get(id); + if (!shot) return; + // Reuse temp→assets flow used by the old temp panel when possible. + const destDir = path.isAbsolute(templatesDirectory(this.assets.root)) + ? templatesDirectory(this.assets.root) + : path.join(this.assets.root, templatesDirectory(this.assets.root)); + try { + fs.mkdirSync(destDir, { recursive: true }); + } catch { /* ignore */ } + const dest = path.join(destDir, path.basename(shot.filePath)); + try { + fs.copyFileSync(shot.filePath, dest); + } catch (e) { + void vscode.window.showErrorMessage(tr('Failed to add screenshot to drafts: {error}', { + error: e instanceof Error ? e.message : String(e), + })); + return; + } + this.assets.load(); + AnnotationPanel.show(this.extensionUri, this.assets, this.thumbDir, dest, this.assets.listImages(), () => { + void this.pushLibrary(); + }); + } + + private async openAnnotation(imagePath: string | undefined): Promise { + if (!imagePath) return; + this.assets.load(); + const images = this.assets.listImages(); + const resolved = images.find((image) => path.resolve(image) === path.resolve(imagePath)) + || images.find((image) => path.basename(image) === path.basename(imagePath)); + if (!resolved) { + void vscode.window.showWarningMessage(tr('This image is not in Annotation Management yet. Add it there first.')); + return; + } + AnnotationPanel.show(this.extensionUri, this.assets, this.thumbDir, resolved, images, () => { + void this.pushLibrary(); + }); + } + + private async pushInbox(): Promise { + const webview = this.view?.webview; + if (!webview) return; + const items = this.tempStore.list().map((shot) => ({ + id: shot.id, + name: shot.name, + url: webview.asWebviewUri(vscode.Uri.file(shot.filePath)).toString(true), + })); + void webview.postMessage({ type: 'inbox', items }); + if (this.view) { + this.view.badge = items.length + ? { value: items.length, tooltip: tr('Inbox screenshots') } + : undefined; + } + } + + private async pushLibrary(): Promise { + const webview = this.view?.webview; + if (!webview) return; + this.assets.load(); + this.features.refresh(true); + const alias = primaryFeatureAlias(); + const draftImages = this.assets.listImages(); + const draftItems = draftImages.map((imagePath) => { + const name = path.basename(imagePath, path.extname(imagePath)); + const anns = this.assets.getAnnotationsForImage(imagePath, true); + const first = anns[0]; + return { + name, + imagePath, + kind: 'template' as const, + expression: `${alias}.${name}`, + width: first?.bbox?.[2], + height: first?.bbox?.[3], + draftFingerprint: first?.bbox ? fingerprintBox({ bbox: first.bbox as number[] }) : undefined, + }; + }); + const published = [...this.features.all()].map((ft) => ({ + name: ft.name, + imagePath: ft.imagePath, + kind: 'template' as const, + expression: `${alias}.${ft.name}`, + width: ft.width, + height: ft.height, + publishedFingerprint: fingerprintBox({ bbox: ft.bbox, width: ft.width, height: ft.height }), + })); + const diffs = diffPublishSets( + draftItems.map((d) => ({ name: d.name, draftFingerprint: d.draftFingerprint })), + published.map((p) => ({ name: p.name, publishedFingerprint: p.publishedFingerprint })), + ); + const statuses = Object.fromEntries(diffs.map((d) => [d.name, d.status])); + const unpublished = countUnpublished(diffs); + const items = this.mode === 'draft' ? draftItems : published; + void webview.postMessage({ + type: 'library', + mode: this.mode, + items, + statuses, + unpublished, + }); + // Thumbs: prefer annotated crop when present + const thumbItems: { name: string; url: string }[] = []; + for (const it of items.slice(0, 40)) { + const src = it.imagePath; + if (src && fs.existsSync(src)) { + thumbItems.push({ name: it.name, url: webview.asWebviewUri(vscode.Uri.file(src)).toString(true) }); + } + } + if (thumbItems.length) void webview.postMessage({ type: 'thumbs', items: thumbItems }); + } + + private buildHtml(webview: vscode.Webview): string { + const nonce = getNonce(); + const htmlPath = path.join(this.extensionUri.fsPath, 'media', 'resources', 'index.html'); + let html = ''; + try { + html = fs.readFileSync(htmlPath, 'utf-8'); + } catch (e) { + return errorPage(tr('Error'), tr('Unable to read view file: {error}', { + error: e instanceof Error ? e.message : String(e), + })); + } + const resource = (name: string) => webview.asWebviewUri( + vscode.Uri.joinPath(this.extensionUri, 'media', 'resources', name), + ).toString(true); + return injectWebviewLocalization(applySharedAssets(webview, this.extensionUri, html + .split('__CSP_NONCE__').join(nonce) + .split('__CSP_SOURCE__').join(webview.cspSource) + .split('__STYLE_URI__').join(resource('resources.css')) + .split('__APP_SCRIPT_URI__').join(resource('app.js')), + )); + } + + dispose(): void { + for (const s of this.subs) s.dispose(); + } +} From c2077b922dc17b17df0b78723d2aab70c7317f4c Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 01:00:01 +0800 Subject: [PATCH 18/29] feat(annotation): source image strip + selection inspector shell --- media/annotationPanel/app.js | 33 ++++++++++++++++++++++++++++++++ media/annotationPanel/index.html | 12 ++++++++++++ media/annotationPanel/style.css | 31 ++++++++++++++++++++++++++++++ src/annotationPanel.ts | 1 + 4 files changed, 77 insertions(+) diff --git a/media/annotationPanel/app.js b/media/annotationPanel/app.js index efcbb8c1..32ebafc5 100644 --- a/media/annotationPanel/app.js +++ b/media/annotationPanel/app.js @@ -1167,6 +1167,7 @@ } else { img = null; imgData = null; canvas.style.display = 'none'; emptyMsg.style.display = 'flex'; syncAnnotationList(); } + renderImageStrip(msg); document.getElementById('navInfo').textContent = `${msg.filename} (${msg.currentIndex + 1}/${msg.totalImages})`; document.getElementById('prevBtn').disabled = msg.currentIndex <= 0; document.getElementById('nextBtn').disabled = msg.currentIndex >= msg.totalImages - 1; @@ -1175,6 +1176,38 @@ window.addEventListener('resize', resize); resize(); + + function renderImageStrip(msg) { + const strip = document.getElementById('imageStrip'); + if (!strip) return; + const names = msg.imageNames || []; + strip.replaceChildren(); + names.forEach((name, index) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'image-strip__item' + (index === msg.currentIndex ? ' is-active' : ''); + btn.title = name; + btn.textContent = String(index + 1); + btn.addEventListener('click', () => vscode.postMessage({ type: 'navigate', index })); + strip.appendChild(btn); + }); + } + + function updateInspector(ann) { + const box = document.getElementById('annotationInspector'); + if (!box) return; + if (!ann) { box.hidden = true; return; } + box.hidden = false; + document.getElementById('inspectorTitle').textContent = t('inspectorSelected') || 'Selected'; + document.getElementById('inspectorName').value = ann.category || ann.categoryName || ''; + document.getElementById('inspectorX').value = Math.round(ann.x ?? ann.bbox?.[0] ?? 0); + document.getElementById('inspectorY').value = Math.round(ann.y ?? ann.bbox?.[1] ?? 0); + document.getElementById('inspectorW').value = Math.round(ann.w ?? ann.bbox?.[2] ?? 0); + document.getElementById('inspectorH').value = Math.round(ann.h ?? ann.bbox?.[3] ?? 0); + const expr = document.getElementById('inspectorExpr'); + if (expr) expr.textContent = ann.category ? ('fL.' + ann.category) : ''; + } + updateStaticText(); vscode.postMessage({ type:'ready' }); })(); \ No newline at end of file diff --git a/media/annotationPanel/index.html b/media/annotationPanel/index.html index 8588f0b2..9d6f5a97 100644 --- a/media/annotationPanel/index.html +++ b/media/annotationPanel/index.html @@ -36,6 +36,7 @@
+
@@ -53,6 +54,17 @@
+
diff --git a/media/annotationPanel/style.css b/media/annotationPanel/style.css index 452b0fcc..4d030128 100644 --- a/media/annotationPanel/style.css +++ b/media/annotationPanel/style.css @@ -209,3 +209,34 @@ canvas { justify-content: center; color: var(--text-muted); } + +/* P1: source image strip + selection inspector */ +.editor-body { display: flex; } +.image-strip { + flex: 0 0 72px; + display: flex; + flex-direction: column; + gap: 6px; + overflow-y: auto; + padding: var(--space-xs); + border-right: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + background: var(--bg-container); +} +.image-strip__item { + width: 56px; height: 40px; border: 1px solid var(--border); border-radius: var(--radius-sm); + background: var(--bg-row); cursor: pointer; overflow: hidden; padding: 0; +} +.image-strip__item.is-active { border-color: var(--border-strong); box-shadow: 0 0 0 1px var(--border-strong); } +.image-strip__item img { width: 100%; height: 100%; object-fit: cover; display: block; } +.annotation-inspector { + margin-top: var(--space-sm); + padding-top: var(--space-sm); + border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent); + display: grid; gap: 6px; +} +.inspector-row { display: grid; gap: 2px; font-size: var(--font-xs); color: var(--muted); } +.inspector-row input { width: 100%; } +.inspector-xywh { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } +.inspector-xywh label { display: grid; gap: 2px; font-size: var(--font-2xs); color: var(--muted); } +.inspector-xywh input { width: 100%; min-width: 0; } +.inspector-expr { font-family: var(--font-mono); font-size: var(--font-2xs); color: var(--muted); word-break: break-all; } diff --git a/src/annotationPanel.ts b/src/annotationPanel.ts index 205e0fe4..998c8a1a 100644 --- a/src/annotationPanel.ts +++ b/src/annotationPanel.ts @@ -417,6 +417,7 @@ class AnnotationController { currentIndex: this._imageList.indexOf(imagePath), totalImages: this._imageList.length, filename: path.basename(imagePath), + imageNames: this._imageList.map((p) => path.basename(p)), }); } From b10d2e56e24ff18a2ce63e90370fc4809163152b Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 01:00:01 +0800 Subject: [PATCH 19/29] docs: README and parity notes for the P1 information architecture --- README.en.md | 9 +++++---- README.md | 28 ++++++++++++++-------------- docs/interaction-parity-audit.en.md | 3 +++ docs/interaction-parity-audit.md | 3 +++ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/README.en.md b/README.en.md index 88bce059..97c0f79f 100644 --- a/README.en.md +++ b/README.en.md @@ -8,7 +8,7 @@ **Bring ok-script's language keys, OCR fixes, templates, skills and task data straight into your VS Code workflow.** -Language key completion · OCR fix hints · Resource preview & annotation · Console · Character skill management +Language key completion · OCR fix hints · Run / Parameters / Resources (inbox→draft→published) · Character skill management [![VS Code Marketplace](https://img.shields.io/badge/VS%20Code%20Marketplace-ok--script%20Toolkit-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=AliceJump.ok-script-toolkit) [![JetBrains Marketplace](https://img.shields.io/badge/JetBrains%20Marketplace-ok--script%20Toolkit-000000?logo=jetbrains&logoColor=white)](https://plugins.jetbrains.com/plugin/34091-ok-script-toolkit) @@ -23,7 +23,7 @@ Language key completion · OCR fix hints · Resource preview & annotation · Con --- -A VS Code extension that brings ok-script language keys, OCR fixes, templates, skill effects, and task data directly into your Python development workflow. It also includes built-in Resource Preview, Annotation Management, Console, and Character & Skill Manager panels. +A VS Code extension that brings ok-script language keys, OCR fixes, templates, skill effects, and task data directly into your Python development workflow. It also includes a **Run** tree, **Parameters** inspector, unified **Resources** workbench (inbox / draft / published), and Character & Skill Manager. > [!TIP] > The demo images at the beginning of each feature section below are clickable — if the GIFs don't load, click the links to view them. @@ -53,7 +53,7 @@ See [feature and documentation parity](docs/feature-parity.en.md) for current ho | [Code development assistance](#code-development-assistance) | Complete and explain `self.lang`, OCR regex, and skill effect IDs in the editor | | [Template management](#template-management) | Browse templates in a grid, click to insert `fL.`, with thumbnail preview | | [Temp screenshots](#temp-screenshots) | 10-slot scratch area in the activity bar — box-select to copy normalized coordinates | -| [Console](#console-task-running) | Auto-generate parameter forms from `config.py` and run tasks, without writing to the target project's config | +| [Run](#run-task-tree) | Auto-generate parameter forms from `config.py` and run tasks, without writing to the target project's config | | [Character skill management](#character-skill-management) | Visual management and diagnostics for characters / skills / effects / enhancement groups | | [Multi-language support](#multi-language-support) | UI supports 6 languages; data hints follow the target project | @@ -121,7 +121,7 @@ Details: - Note: Cross-Webview drag-and-drop is not available in VS Code (each webview is an iframe with a different origin, `dataTransfer` is blocked), so the **Add to Annotation Management** button is the reliable entry point for import. - The **annotation editor** also supports this mode: click "Coords (C)" in the toolbar or press `C` to enter box-select mode, copying the same `x,y,tox,toy` normalized coordinates without creating an annotation box or modifying COCO. The resulting box also has handles for fine-tuning, with coordinates re-copied after each adjustment; clicking outside the box in non-interactive areas clears it. Override with `copyCoords` in `okScriptToolkit.annotationKeybindings`. -### Console (Task Running) +### Run (Task Tree)

Console: parameter form and run log @@ -290,6 +290,7 @@ Command categories display as "ok-script 工具箱" / "ok-script Toolkit" depend | Command | Shortcut | Description | |---|---|---| +| `ok-script Toolkit: Capture to Inbox` | `Ctrl+Alt+S` | | `ok-script Toolkit: Open Resource Preview` | `Ctrl+Alt+T` (macOS `Cmd+Alt+T`, requires Python editor focus) | Focus the Resource Preview view in the activity bar | | `ok-script Toolkit: Open Resource Preview in Editor` | — | Open a large grid view in the editor area | | `ok-script Toolkit: Open Console` | — | Focus the Console view in the activity bar | diff --git a/README.md b/README.md index 99b676ea..8adce23f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **把 ok-script 的语言、OCR、模板、技能和任务数据,直接搬进 VS Code 的开发流程。** -语言键补全 · OCR 修正提示 · 资源预览与标注 · 控制台 · 角色技能管理 +语言键补全 · OCR 修正提示 · 运行 / 参数 / 资源(收件箱→草稿→已发布)· 角色技能管理 [![VS Code Marketplace](https://img.shields.io/badge/VS%20Code%20Marketplace-ok--script%20Toolkit-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=AliceJump.ok-script-toolkit) [![JetBrains Marketplace](https://img.shields.io/badge/JetBrains%20Marketplace-ok--script%20Toolkit-000000?logo=jetbrains&logoColor=white)](https://plugins.jetbrains.com/plugin/34091-ok-script-toolkit) @@ -23,7 +23,7 @@ --- -VS Code 扩展,为 ok-script 项目的 Python 开发提供语言键、OCR 修正、模板和技能效果的数据提示,同时内置资源预览、标注管理、控制台和角色技能管理面板,让 ok-script 的语言、OCR、模板、技能和任务数据直接进入开发流程。 +VS Code 扩展,为 ok-script 项目的 Python 开发提供语言键、OCR 修正、模板和技能效果的数据提示,同时内置**运行**(原生任务树)、**参数**检查器、**资源**工作台(收件箱 / 草稿 / 已发布)和角色技能管理,让 ok-script 的语言、OCR、模板、技能和任务数据直接进入开发流程。 > [!TIP] > 下面每个功能章节开头的演示图都是可点的——如果动图没加载出来,直接点开链接看。 @@ -52,8 +52,8 @@ PyCharm / IntelliJ IDEA 用户请装 JetBrains 版:[ok-script Toolkit for JetB |---|---| | [代码开发辅助](#代码开发辅助) | 在编辑器内补全和解释 `self.lang`、OCR 正则与技能效果 ID | | [模板管理](#模板管理) | 网格浏览模板,单击插入 `fL.<名称>`,支持缩略图预览 | -| [临时截图](#临时截图) | 活动栏里的 10 张暂存区,框选即可复制归一化坐标 | -| [控制台](#控制台任务运行) | 从 `config.py` 自动生成参数表单并运行任务,全程不写目标项目配置 | +| [资源](#资源收件箱草稿已发布) | 单一「资源」视图:收件箱(原临时截图)+ 草稿 / 已发布 | +| [运行](#运行任务树) | 原生 TreeView:游戏 / 执行器 / 触发与一次性任务;参数在「参数」视图 | | [角色技能管理](#角色技能管理) | 角色 / 技能 / 效果 / 强化组的可视化管理与诊断 | | [多语言支持](#多语言支持) | 界面支持 6 种语言,数据提示跟随目标项目 | @@ -98,13 +98,13 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) 框资源管理复用标注原图与 COCO 编辑器,支持编辑框、从模板生成包围框,并显式发布归一化位置表。框画廊提供裁剪预览、来源定位及 `self.pos` 引用的插入、复制、补全和 Hover。工作文件与运行时文件分开,游戏中的加载由业务项目负责。详见 [框资源设计](docs/box-resources.md)。 -### 临时截图 +### 资源(收件箱 → 草稿 → 已发布)

临时截图:轮播与框选复制归一化坐标

-**ok-script 临时截图** 是活动栏上的**独立容器**(有自己的图标),里面是一个最多 10 张的暂存区(超出自动淘汰最早一张),用于快速取素材: +活动栏只保留一个 **ok-script** 容器。其中的 **资源** 视图上部是**收件箱**(原「临时截图」,最多 10 张,超出淘汰最早一张),下部是**草稿 / 已发布**切换: | 能力 | 说明 | |---|---| @@ -121,7 +121,7 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) - 注:VS Code 的跨 Webview 拖拽不可用(各 webview 是不同 origin 的 iframe,`dataTransfer` 被屏蔽),所以「添加到标注管理」按钮是导入的可靠入口。 - **标注编辑器**同样支持该模式:工具栏「坐标 (C)」或按 `C` 键后框选,直接复制同样的 `x,y,tox,toy` 归一化坐标,不会创建标注框、不改动 COCO。得到的框同样带手柄可继续微调,每次调整结束重新复制;点击图片非交互部分清除。快捷键可用 `okScriptToolkit.annotationKeybindings` 的 `copyCoords` 覆盖。 -### 控制台(任务运行) +### 运行(任务树)

控制台:参数表单与运行日志 @@ -139,9 +139,9 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) - **不污染目标项目配置**:执行器运行时,ok 框架对 `configs/` 与截图的读写全部改道到工作区的沙箱目录 `.vscode/ok-script-toolkit/`,**目标项目的配置文件与截图全程保持原样**(`devices.json` 做桥接拷贝以复用游戏连接)。调试时可以放心地改参数试跑,不会弄脏项目。 - 执行器可随时暂停/恢复(全局挂起轮询与任务),也可以「停止当前任务」而不关闭执行器;运行日志输出到专属输出频道。 - **全局配置接管**:目标项目的全局配置组(ok 框架 GlobalConfig 的可见分组)由探针一并解析,在控制台「配置」分段以参数快照呈现——修改即保存、可一键恢复默认;改动经 `gparams` 即时推送给运行中的执行器,执行器启动时以 `OK_TOOLKIT_GCONFIG` 注入全量快照。同样只写沙箱、不碰目标项目配置。 -- **控制台视图**:任务运行与游戏连接统一在「控制台」的四个分段(任务 / 游戏 / 配置 / 账号)里管理;顶栏健康度条实时反映执行器状态,空闲时详情面板显示运行中心(当前任务 / 执行队列 / 触发轮询),任务卡悬停弹出只读摘要。侧边栏较窄时,分段导航会收成图标条(≤640px)或顶部分段条(≤480px)。 -- **状态栏与命令**:状态栏左侧显示执行器状态(空闲 / 启动中 / 运行中的任务 + 排队数 / 已暂停),点击打开控制台;控制台视图标题栏和命令面板提供「运行任务… / 启动 / 暂停 / 继续 / 停止当前任务 / 关闭执行器 / 查看日志 / 连接游戏」等命令,按执行器状态显隐。运行中 + 排队的任务数显示在视图徽标上。 -- **错误就地显示**:项目路径缺失、Python 不存在、执行器异常退出等错误显示在控制台横幅里,并带「设置项目路径 / 设置 Python 路径 / 查看日志 / 重试」等下一步按钮;控制台不可见时才退回通知(同样带按钮)。 +- **运行视图**:原生 TreeView,展示游戏连接、执行器、触发任务(勾选=启用)、一次性任务(按组)、全局配置与账号;行内可运行 / 打开参数 / 停止。 +- **参数视图**:跟随「运行」里的选中项,编辑沙箱覆盖(自动保存并在执行器运行时推送)。 +- **状态栏与命令**:状态栏左侧显示执行器状态,点击打开「运行」;标题栏和命令面板提供启动 / 暂停 / 停止 / 日志 / 连接游戏等,按状态显隐。运行中 + 排队数显示为视图徽标。 - **角色技能管理入口**在「配置」分段;未检测到角色数据(`characterMasterFile` / `characterSkillsDirectory` 都不存在)时隐藏,命令面板里的同名命令也一并隐藏。 ### 角色技能管理 @@ -162,7 +162,7 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) ### 多语言支持 - 扩展界面(通知、输出频道、hover、资源预览、标注管理、控制台和角色技能管理面板)支持简体中文、繁体中文、英文、日文、韩文和西班牙文,默认跟随 VS Code 显示语言。 -- 侧边栏分为 **ok-script 工具**(控制台)、**ok-script 资源**(资源预览 + 标注管理)和 **ok-script 临时截图** 三个独立活动栏容器。界面图标统一使用 VS Code codicons。 +- 侧边栏只有一个 **ok-script** 活动栏容器,内含 **运行**(TreeView)、**参数**(检查器)、**资源**(收件箱 + 草稿/已发布)。界面图标统一使用 VS Code codicons。 - 代码提示中的语言数据始终使用目标项目自身的 locale 和原始协议值,不受插件界面语言影响。 > [!NOTE] @@ -302,9 +302,9 @@ self.wait_click_ocr(match=self.lang.zip_line_mixin.k_2f4f4a2f, ...) | 命令 | 快捷键 | 说明 | |---|---|---| -| `ok-script 工具箱: 打开资源预览` | `Ctrl+Alt+T`(macOS `Cmd+Alt+T`,需聚焦 Python 编辑器) | 聚焦活动栏中的资源预览视图 | -| `ok-script 工具箱: 在编辑器中打开资源预览` | — | 在编辑器区打开大窗口网格视图 | -| `ok-script 工具箱: 打开控制台` | — | 聚焦活动栏中的控制台视图 | +| `ok-script 工具箱: 打开资源预览` | `Ctrl+Alt+T`(macOS `Cmd+Alt+T`,需聚焦 Python 编辑器) | 聚焦「资源」视图 | +| `ok-script 工具箱: 截到收件箱` | `Ctrl+Alt+S`(macOS `Cmd+Alt+S`) | 截图进入资源收件箱(原「截图并打开标注管理」仍保留为另一条命令) | +| `ok-script 工具箱: 打开控制台` | — | 聚焦「运行」视图 | | `ok-script 工具箱: 控制台:运行任务…` | — | 从已加载的一次性任务里选一个入队(执行器未启动时会先启动) | | `ok-script 工具箱: 控制台:启动执行器 / 暂停执行器 / 继续执行器 / 停止当前任务 / 关闭执行器` | — | 与控制台按钮同一套逻辑;按执行器状态在命令面板和视图标题栏里显隐 | | `ok-script 工具箱: 控制台:刷新任务 / 查看日志 / 连接游戏 / 断开游戏` | — | 重新解析任务列表、打开输出频道、连接或断开游戏窗口 | diff --git a/docs/interaction-parity-audit.en.md b/docs/interaction-parity-audit.en.md index 1caa7c1f..3a3b9cdc 100644 --- a/docs/interaction-parity-audit.en.md +++ b/docs/interaction-parity-audit.en.md @@ -8,6 +8,9 @@ This report compares the pages actually registered in the baseline source, cover This is a source audit, without interactive or screenshot verification in either IDE. “No corresponding entry in the current page” does not mean that no implementation exists elsewhere in the plugin. Theme, scaling, focus, and keyboard behavior still need real IDE verification. Shared Python code, formats, or passing tests do not establish interaction parity. + + +> P1 (2026-10-11): VS Code now uses a single activity-bar container with Run TreeView / Parameters / Resources. JetBrains toolWindow merge and Tree parity remain pending (plan.md §11). ## 1. Registered entry points and organization | ID | Item | VS Code | JetBrains | User-visible difference | diff --git a/docs/interaction-parity-audit.md b/docs/interaction-parity-audit.md index 2e5a240a..541bec6e 100644 --- a/docs/interaction-parity-audit.md +++ b/docs/interaction-parity-audit.md @@ -8,6 +8,9 @@ 这是源码核对,未启动两个 IDE 做交互或截图验收。源码明确没有的入口写为“当前页面无对应入口”;不能据此推断整个插件没有任何实现。主题、缩放、焦点与键盘的实际表现仍需真实 IDE 验证。共享 Python、数据格式或测试通过,也不代表交互一致。 + + +> P1(2026-10-11):VS Code 已合并为单一活动栏「ok-script」+ 运行 TreeView / 参数 / 资源工作台。JetBrains 端 toolWindow 合并与 Tree 对齐仍待同步(见 plan.md §11)。 ## 1. 实际入口与页面组织 | 编号 | 对照项 | VS Code | JetBrains | 用户可感知的差异 | From 59ee840533cc3768d855c3870762eef425d4c6d4 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:09:03 +0800 Subject: [PATCH 20/29] feat(hover): trusted command links on template hovers (reveal / find references) MarkdownString.isTrusted uses an enabledCommands allowlist with only the two extension commands; link targets are encoded so names cannot close the markdown link. Snapshot test in scripts/test_hover_links.js. --- scripts/test_hover_links.js | 12 ++++++++++++ src/hoverLinks.ts | 22 ++++++++++++++++++++++ src/providers.ts | 8 +++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 scripts/test_hover_links.js create mode 100644 src/hoverLinks.ts diff --git a/scripts/test_hover_links.js b/scripts/test_hover_links.js new file mode 100644 index 00000000..63d44867 --- /dev/null +++ b/scripts/test_hover_links.js @@ -0,0 +1,12 @@ +const assert = require('assert'); +const path = require('path'); +const { featureHoverLinks, HOVER_ENABLED_COMMANDS } = require(path.join(__dirname, '..', 'out', 'hoverLinks')); +const md = featureHoverLinks('btn_claim', { reveal: '在资源中定位', refs: '查找引用' }); +assert.strictEqual(md, + '[$(search) 在资源中定位](command:okScriptToolkit.revealResource?%5B%22btn_claim%22%5D) · ' + + '[$(references) 查找引用](command:okScriptToolkit.findTemplateReferences?%5B%22btn_claim%22%5D)'); +// 链接里出现的命令必须都在白名单内 +for (const m of md.matchAll(/command:([\w.]+)/g)) assert.ok(HOVER_ENABLED_COMMANDS.includes(m[1]), m[1]); +// 名字里的特殊字符被编码,不能闭合 markdown 链接 +assert.ok(!featureHoverLinks('a)b', { reveal: 'r', refs: 'f' }).includes('a)b')); +console.log('ok hover links snapshot'); diff --git a/src/hoverLinks.ts b/src/hoverLinks.ts new file mode 100644 index 00000000..042bb90a --- /dev/null +++ b/src/hoverLinks.ts @@ -0,0 +1,22 @@ +/** + * 悬停里的命令链接(纯函数,不依赖 vscode,便于快照测试)。 + * MarkdownString 只放行 HOVER_ENABLED_COMMANDS 里的本插件命令(isTrusted.enabledCommands)。 + */ +export const HOVER_ENABLED_COMMANDS = [ + 'okScriptToolkit.revealResource', + 'okScriptToolkit.findTemplateReferences', +] as const; + +function commandUri(command: string, args: unknown[]): string { + // encodeURIComponent 不编码 ( ) ,它们会提前闭合 markdown 链接 + const query = encodeURIComponent(JSON.stringify(args)).replace(/\(/g, '%28').replace(/\)/g, '%29'); + return `command:${command}?${query}`; +} + +/** 模板悬停底部的链接行:在资源中定位 · 查找引用 */ +export function featureHoverLinks(name: string, labels: { reveal: string; refs: string }): string { + return [ + `[$(search) ${labels.reveal}](${commandUri('okScriptToolkit.revealResource', [name])})`, + `[$(references) ${labels.refs}](${commandUri('okScriptToolkit.findTemplateReferences', [name])})`, + ].join(' · '); +} diff --git a/src/providers.ts b/src/providers.ts index 71806261..02a8176e 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { featureHoverLinks, HOVER_ENABLED_COMMANDS } from './hoverLinks'; import { LangData, LangEntry, @@ -434,7 +435,12 @@ export class LangHoverProvider implements vscode.HoverProvider { for (const mf of findFeatureMatches(line)) { if (position.character >= mf.start && position.character <= mf.end) { const ft = this.features.entry(mf.name); - if (ft) return new vscode.Hover(formatFeature(ft)); + if (ft) { + const md = formatFeature(ft); + md.appendMarkdown('\n\n' + featureHoverLinks(ft.name, { reveal: tr('Reveal in Resources'), refs: tr('Find References') })); + md.isTrusted = { enabledCommands: [...HOVER_ENABLED_COMMANDS] }; + return new vscode.Hover(md); + } } } // OCR 函数的 match 正则:hover 显示 ocr.po 中的修正映射 From 136a653a234b4daf551132fcf3804b0b897b00b6 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:09:03 +0800 Subject: [PATCH 21/29] feat(resources): find template references, reveal in Resources, image explorer menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - okScriptToolkit.findTemplateReferences: fL.xxx / self.pos.xxx / "key" in workspace Python files → QuickPick → jump (pure finder + test) - revealResource now filters the Resources view to the name - Explorer '用标注编辑器打开' only on .png/.jpg/.jpeg/.webp --- l10n/bundle.l10n.es.json | 8 ++++-- l10n/bundle.l10n.ja.json | 8 ++++-- l10n/bundle.l10n.json | 8 ++++-- l10n/bundle.l10n.ko.json | 8 ++++-- l10n/bundle.l10n.zh-cn.json | 8 ++++-- l10n/bundle.l10n.zh-tw.json | 8 ++++-- media/resources/app.js | 7 +++++ package.json | 30 ++++++++++++++++++++-- package.nls.es.json | 3 ++- package.nls.ja.json | 3 ++- package.nls.json | 3 ++- package.nls.ko.json | 3 ++- package.nls.zh-cn.json | 3 ++- package.nls.zh-tw.json | 3 ++- scripts/test_template_references.js | 19 ++++++++++++++ src/extension.ts | 40 +++++++++++++++++++++++++++-- src/run/resourcesView.ts | 6 +++++ src/templateReferences.ts | 37 ++++++++++++++++++++++++++ 18 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 scripts/test_template_references.js create mode 100644 src/templateReferences.ts diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 251cb530..83ae89ad 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -298,6 +298,10 @@ "Open Parameters": "Abrir parámetros", "paramsSandboxHint": "Solo en el sandbox · se envía al ejecutor si está activo", "Resource expression (e.g. fL.btn_claim)": "Expresión de recurso (p. ej. fL.btn_claim)", - "Locate “{name}” in Resources": "Localizar “{name}” en Recursos", - "Use as ok-script project": "Usar como proyecto ok-script" + "Use as ok-script project": "Usar como proyecto ok-script", + "Reveal in Resources": "Mostrar en Recursos", + "Find References": "Buscar referencias", + "Select a template to find references": "Elige una plantilla", + "No references to “{name}” found in Python files.": "No hay referencias a “{name}” en archivos Python.", + "{count} reference(s) to “{name}”": "{count} referencia(s) a “{name}”" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 20e2ad80..e819fcfb 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -298,6 +298,10 @@ "Open Parameters": "パラメータを開く", "paramsSandboxHint": "サンドボックスにのみ保存 · 実行中はエグゼキュータへ送出", "Resource expression (e.g. fL.btn_claim)": "リソース式(例: fL.btn_claim)", - "Locate “{name}” in Resources": "「リソース」で “{name}” を表示", - "Use as ok-script project": "ok-script プロジェクトとして使う" + "Use as ok-script project": "ok-script プロジェクトとして使う", + "Reveal in Resources": "リソースで表示", + "Find References": "参照を検索", + "Select a template to find references": "参照を検索するテンプレートを選択", + "No references to “{name}” found in Python files.": "Python ファイルに “{name}” の参照はありません。", + "{count} reference(s) to “{name}”": "“{name}” の参照 {count} 件" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index ae11e42c..f03c6c7e 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -298,6 +298,10 @@ "Open Parameters": "Open Parameters", "paramsSandboxHint": "Saved to the sandbox only · pushed to the executor when running", "Resource expression (e.g. fL.btn_claim)": "Resource expression (e.g. fL.btn_claim)", - "Locate “{name}” in Resources": "Locate “{name}” in Resources", - "Use as ok-script project": "Use as ok-script project" + "Use as ok-script project": "Use as ok-script project", + "Reveal in Resources": "Reveal in Resources", + "Find References": "Find References", + "Select a template to find references": "Select a template to find references", + "No references to “{name}” found in Python files.": "No references to “{name}” found in Python files.", + "{count} reference(s) to “{name}”": "{count} reference(s) to “{name}”" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index e0312927..5e08e96a 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -298,6 +298,10 @@ "Open Parameters": "매개 변수 열기", "paramsSandboxHint": "샌드박스에만 저장 · 실행 중이면 실행기로 전송", "Resource expression (e.g. fL.btn_claim)": "리소스 식(예: fL.btn_claim)", - "Locate “{name}” in Resources": "리소스에서 “{name}” 찾기", - "Use as ok-script project": "ok-script 프로젝트로 사용" + "Use as ok-script project": "ok-script 프로젝트로 사용", + "Reveal in Resources": "리소스에서 표시", + "Find References": "참조 찾기", + "Select a template to find references": "참조를 찾을 템플릿 선택", + "No references to “{name}” found in Python files.": "Python 파일에서 “{name}” 참조를 찾지 못했습니다.", + "{count} reference(s) to “{name}”": "“{name}” 참조 {count}개" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 5d6559ef..0abce7a8 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -298,6 +298,10 @@ "Open Parameters": "打开参数", "paramsSandboxHint": "仅写入沙箱 · 执行器运行中时会推送", "Resource expression (e.g. fL.btn_claim)": "资源表达式(例如 fL.btn_claim)", - "Locate “{name}” in Resources": "在「资源」中定位 “{name}”", - "Use as ok-script project": "用作 ok-script 项目" + "Use as ok-script project": "用作 ok-script 项目", + "Reveal in Resources": "在资源中定位", + "Find References": "查找引用", + "Select a template to find references": "选择要查找引用的模板", + "No references to “{name}” found in Python files.": "Python 文件中没有找到 “{name}” 的引用。", + "{count} reference(s) to “{name}”": "“{name}” 共 {count} 处引用" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index a6a4c7fd..ebae7dc9 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -298,6 +298,10 @@ "Open Parameters": "開啟參數", "paramsSandboxHint": "僅寫入沙箱 · 執行器執行中時會推送", "Resource expression (e.g. fL.btn_claim)": "資源運算式(例如 fL.btn_claim)", - "Locate “{name}” in Resources": "在「資源」中定位 “{name}”", - "Use as ok-script project": "用作 ok-script 專案" + "Use as ok-script project": "用作 ok-script 專案", + "Reveal in Resources": "在資源中定位", + "Find References": "尋找參考", + "Select a template to find references": "選擇要尋找參考的模板", + "No references to “{name}” found in Python files.": "Python 檔案中沒有找到 “{name}” 的參考。", + "{count} reference(s) to “{name}”": "“{name}” 共 {count} 處參考" } diff --git a/media/resources/app.js b/media/resources/app.js index 895d1c47..f0fc976a 100644 --- a/media/resources/app.js +++ b/media/resources/app.js @@ -174,6 +174,13 @@ setSeg(document.getElementById('modeTabs'), 'data-mode', state.mode); renderGrid(); } + if (msg.type === 'reveal') { + state.query = msg.name || ''; + els.search.value = state.query; + renderGrid(); + const card = els.grid.querySelector('.res-card'); + if (card) ThumbnailActions.select(card, true); + } if (msg.type === 'thumbs') { for (const it of (msg.items || [])) state.thumbs[it.name] = it.url; renderGrid(); diff --git a/package.json b/package.json index 0e8109e2..cc0757b8 100644 --- a/package.json +++ b/package.json @@ -216,6 +216,12 @@ "title": "%command.pickProject%", "category": "%command.category%", "icon": "$(folder)" + }, + { + "command": "okScriptToolkit.findTemplateReferences", + "title": "%command.findTemplateReferences%", + "category": "%command.category%", + "icon": "$(references)" } ], "walkthroughs": [ @@ -623,6 +629,10 @@ { "command": "okScriptToolkit.insertResourceReference", "when": "false" + }, + { + "command": "okScriptToolkit.findTemplateReferences", + "when": "okScriptToolkit.projectReady" } ], "view/title": [ @@ -673,6 +683,20 @@ "when": "view == okScriptToolkit.run && viewItem == run.task.running", "group": "inline@1" } + ], + "explorer/context": [ + { + "command": "okScriptToolkit.annotateImage", + "when": "resourceExtname =~ /^\\.(png|jpe?g|webp)$/i", + "group": "navigation@90" + } + ], + "editor/context": [ + { + "command": "okScriptToolkit.findTemplateReferences", + "when": "editorLangId == python && okScriptToolkit.projectReady", + "group": "navigation@90" + } ] }, "viewsWelcome": [ @@ -690,7 +714,7 @@ "verify:version": "node scripts/release/verify-version.js", "verify:l10n": "node scripts/release/verify-l10n.js", "release": "sh scripts/release.sh", - "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", + "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:hover-links && npm run test:template-references && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", "test:webview-html": "node scripts/test_webview_html.js", "test:design-system": "node scripts/test_design_system.js", "test:thumbnail-actions": "node scripts/test_thumbnail_actions.js", @@ -745,7 +769,9 @@ "test:command-contract": "node scripts/test_command_contract.js", "test:run-tree-model": "node scripts/test_run_tree_model.js", "test:publish-diff": "node scripts/test_publish_diff.js", - "test:character-diagnostics": "node scripts/test_character_diagnostics.js" + "test:character-diagnostics": "node scripts/test_character_diagnostics.js", + "test:hover-links": "node scripts/test_hover_links.js", + "test:template-references": "node scripts/test_template_references.js" }, "devDependencies": { "@types/jpeg-js": "^0.3.0", diff --git a/package.nls.es.json b/package.nls.es.json index 80b033ec..be16d189 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -91,5 +91,6 @@ "command.run.openParams": "Abrir parámetros", "command.run.runTaskInline": "Ejecutar tarea", "command.run.toggleTrigger": "Alternar disparador", - "command.pickProject": "Elegir carpeta del proyecto…" + "command.pickProject": "Elegir carpeta del proyecto…", + "command.findTemplateReferences": "Buscar referencias de plantilla…" } diff --git a/package.nls.ja.json b/package.nls.ja.json index dbbd7a49..f7b2f11e 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -91,5 +91,6 @@ "command.run.openParams": "パラメータを開く", "command.run.runTaskInline": "タスクを実行", "command.run.toggleTrigger": "トリガー切替", - "command.pickProject": "ok-script プロジェクトフォルダーを選択…" + "command.pickProject": "ok-script プロジェクトフォルダーを選択…", + "command.findTemplateReferences": "テンプレート参照を検索…" } diff --git a/package.nls.json b/package.nls.json index 41740a6c..b3bc1ef4 100644 --- a/package.nls.json +++ b/package.nls.json @@ -91,5 +91,6 @@ "command.run.openParams": "Open Parameters", "command.run.runTaskInline": "Run Task", "command.run.toggleTrigger": "Toggle Trigger", - "command.pickProject": "Choose ok-script Project Folder…" + "command.pickProject": "Choose ok-script Project Folder…", + "command.findTemplateReferences": "Find Template References…" } diff --git a/package.nls.ko.json b/package.nls.ko.json index 8d48409c..4e215990 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -91,5 +91,6 @@ "command.run.openParams": "매개 변수 열기", "command.run.runTaskInline": "작업 실행", "command.run.toggleTrigger": "트리거 전환", - "command.pickProject": "ok-script 프로젝트 폴더 선택…" + "command.pickProject": "ok-script 프로젝트 폴더 선택…", + "command.findTemplateReferences": "템플릿 참조 찾기…" } diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 89917360..35cc5cf5 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -91,5 +91,6 @@ "command.run.openParams": "打开参数", "command.run.runTaskInline": "运行任务", "command.run.toggleTrigger": "切换触发启用", - "command.pickProject": "选择 ok-script 项目目录…" + "command.pickProject": "选择 ok-script 项目目录…", + "command.findTemplateReferences": "查找模板引用…" } diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 561a40ae..dbd406e1 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -91,5 +91,6 @@ "command.run.openParams": "開啟參數", "command.run.runTaskInline": "執行任務", "command.run.toggleTrigger": "切換觸發啟用", - "command.pickProject": "選擇 ok-script 專案目錄…" + "command.pickProject": "選擇 ok-script 專案目錄…", + "command.findTemplateReferences": "尋找模板參考…" } diff --git a/scripts/test_template_references.js b/scripts/test_template_references.js new file mode 100644 index 00000000..34afdd69 --- /dev/null +++ b/scripts/test_template_references.js @@ -0,0 +1,19 @@ +const assert = require('assert'); +const path = require('path'); +const { findReferencesInText } = require(path.join(__dirname, '..', 'out', 'templateReferences')); +const src = [ + 'box = self.find_one(fL.btn_claim)', + 'self.click(Features.btn_claim)', + '# fL.btn_claim in a comment is ignored', + 'other = fL.btn_claim_all # different template', + 'self.pos.btn_claim.to_box()', + 'self.find_feature("btn_claim")', +].join('\n'); +const refs = findReferencesInText(src, 'btn_claim', ['fL', 'Features']); +assert.deepStrictEqual(refs.map((r) => [r.line, r.kind]), [ + [0, 'feature'], [1, 'feature'], [4, 'position'], [5, 'key'], +]); +assert.strictEqual(refs[0].column, 20); +assert.deepStrictEqual(findReferencesInText(src, '', ['fL']), []); +assert.strictEqual(findReferencesInText('fL.a.b', 'a.b', ['fL']).length, 1); // 点号按字面匹配 +console.log('ok template references'); diff --git a/src/extension.ts b/src/extension.ts index 28f5b391..e6efadbc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { LangCompletionProvider, LangHoverProvider, LangInlayHintsProvider, + featureAliases, } from './providers'; import { EffectCompletionProvider, EffectHoverProvider, EffectInlayHintsProvider } from './effectProvider'; import { @@ -30,6 +31,7 @@ import type { RunTreeNode } from './run/treeModel'; import { issuesToDiagnostics } from './run/characterDiagnostics'; import { configuredCharacterDataPaths, loadCharacterManagerData } from './characterData'; import { insertIntoPythonEditor } from './pythonEditor'; +import { findReferencesInText } from './templateReferences'; import { CharacterManagerPanel, detectCharacterData } from './characterPanel'; import { GameConnectService } from './toolboxConnect'; import { TemplateAssetData } from './templateAssetData'; @@ -490,8 +492,42 @@ export function activate(context: vscode.ExtensionContext): void { if (text) insertIntoPythonEditor(text); }), vscode.commands.registerCommand('okScriptToolkit.revealResource', async (name?: string) => { - void vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); - if (name) void vscode.window.showInformationMessage(tr('Locate “{name}” in Resources', { name })); + await vscode.commands.executeCommand(`${ResourcesViewProvider.viewType}.focus`); + if (typeof name === 'string' && name) resourcesView.reveal(name); + }), + vscode.commands.registerCommand('okScriptToolkit.findTemplateReferences', async (name?: string) => { + let target = typeof name === 'string' ? name : ''; + if (!target) { + const picked = await vscode.window.showQuickPick( + [...features.all()].map((ft) => ({ label: ft.name, description: `${ft.width}×${ft.height}` })), + { placeHolder: tr('Select a template to find references') }, + ); + target = picked?.label || ''; + } + if (!target) return; + const files = await vscode.workspace.findFiles('**/*.py', '**/{.venv,venv,node_modules,.git,__pycache__}/**', 2000); + const items: (vscode.QuickPickItem & { location: vscode.Location })[] = []; + for (const uri of files) { + let text = ''; + try { text = Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8'); } catch { continue; } + if (!text.includes(target)) continue; + for (const ref of findReferencesInText(text, target, featureAliases())) { + items.push({ + label: `$(${ref.kind === 'position' ? 'symbol-field' : ref.kind === 'key' ? 'symbol-string' : 'symbol-constant'}) ${ref.preview}`, + description: `${vscode.workspace.asRelativePath(uri)}:${ref.line + 1}`, + location: new vscode.Location(uri, new vscode.Range(ref.line, ref.column, ref.line, ref.column + ref.length)), + }); + } + } + if (!items.length) { + void vscode.window.showInformationMessage(tr('No references to “{name}” found in Python files.', { name: target })); + return; + } + const choice = await vscode.window.showQuickPick(items, { + placeHolder: tr('{count} reference(s) to “{name}”', { count: items.length, name: target }), + matchOnDescription: true, + }); + if (choice) await vscode.window.showTextDocument(choice.location.uri, { selection: choice.location.range }); }), vscode.commands.registerCommand('okScriptToolkit.pickProject', async () => { const picked = await vscode.window.showOpenDialog({ diff --git a/src/run/resourcesView.ts b/src/run/resourcesView.ts index dfa0f678..134881a2 100644 --- a/src/run/resourcesView.ts +++ b/src/run/resourcesView.ts @@ -82,6 +82,12 @@ export class ResourcesViewProvider implements vscode.WebviewViewProvider, vscode }); } + /** 在资源视图里定位某个资源:切到已发布并把搜索框填成名字 */ + reveal(name: string): void { + this.mode = 'published'; + void this.pushLibrary().then(() => this.view?.webview.postMessage({ type: 'reveal', name })); + } + async captureToInbox(): Promise { const { captureGameWindow } = await import('../screenshotCapture'); const out = this.tempStore.newFilePath(); diff --git a/src/templateReferences.ts b/src/templateReferences.ts new file mode 100644 index 00000000..9d77943a --- /dev/null +++ b/src/templateReferences.ts @@ -0,0 +1,37 @@ +/** + * 查找模板 / 位置资源在 Python 代码里的引用(纯函数部分)。 + * 认三种写法:<别名>.<名字>(fL.btn)、self.pos.<路径>、以及字符串键 "名字"(feature key)。 + */ +export interface TextReference { + line: number; + column: number; + length: number; + kind: 'feature' | 'position' | 'key'; + preview: string; +} + +const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export function findReferencesInText(text: string, name: string, aliases: string[]): TextReference[] { + if (!name) return []; + const n = escapeRe(name); + const alias = aliases.length ? aliases.map(escapeRe).join('|') : 'fL'; + const patterns: [TextReference['kind'], RegExp][] = [ + ['feature', new RegExp(`\\b(?:${alias})\\.${n}(?![\\w])`, 'g')], + ['position', new RegExp(`\\bself\\.pos\\.${n}(?![\\w])`, 'g')], + ['key', new RegExp(`(["'])${n}\\1`, 'g')], + ]; + const out: TextReference[] = []; + const lines = text.split(/\r?\n/); + lines.forEach((lineText, line) => { + const trimmed = lineText.trimStart(); + if (trimmed.startsWith('#')) return; + for (const [kind, re] of patterns) { + re.lastIndex = 0; + for (let m = re.exec(lineText); m; m = re.exec(lineText)) { + out.push({ line, column: m.index, length: m[0].length, kind, preview: trimmed.slice(0, 160) }); + } + } + }); + return out.sort((a, b) => a.line - b.line || a.column - b.column); +} From 4fc58242469bb871511a799a1b60742d709e318a Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:09:09 +0800 Subject: [PATCH 22/29] =?UTF-8?q?l10n:=20name=20the=20image=20context=20co?= =?UTF-8?q?mmand=20'=E7=94=A8=E6=A0=87=E6=B3=A8=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E6=89=93=E5=BC=80'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.nls.es.json | 2 +- package.nls.ja.json | 2 +- package.nls.json | 2 +- package.nls.ko.json | 2 +- package.nls.zh-cn.json | 2 +- package.nls.zh-tw.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.nls.es.json b/package.nls.es.json index be16d189..1dc00524 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -84,7 +84,7 @@ "view.resources": "Recursos", "viewsWelcome.run": "Abre una carpeta de proyecto ok-script (con src/config.py) o [elige una ruta](command:okScriptToolkit.pickProject).", "command.captureToInbox": "Capturar a la bandeja", - "command.annotateImage": "Anotar imagen…", + "command.annotateImage": "Abrir con el editor de anotaciones", "command.publishResources": "Publicar recursos…", "command.insertResourceReference": "Insertar referencia", "command.revealResource": "Mostrar recurso", diff --git a/package.nls.ja.json b/package.nls.ja.json index f7b2f11e..c5e0f525 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -84,7 +84,7 @@ "view.resources": "リソース", "viewsWelcome.run": "src/config.py のある ok-script プロジェクトフォルダーを開くか、[プロジェクトパスを選択](command:okScriptToolkit.pickProject)してください。", "command.captureToInbox": "受信箱にキャプチャ", - "command.annotateImage": "画像を注釈…", + "command.annotateImage": "注釈エディターで開く", "command.publishResources": "リソースを公開…", "command.insertResourceReference": "リソース参照を挿入", "command.revealResource": "リソースを表示", diff --git a/package.nls.json b/package.nls.json index b3bc1ef4..eec5ae71 100644 --- a/package.nls.json +++ b/package.nls.json @@ -84,7 +84,7 @@ "view.resources": "Resources", "viewsWelcome.run": "Open an ok-script project folder (with src/config.py), or [choose a project path](command:okScriptToolkit.pickProject).", "command.captureToInbox": "Capture to Inbox", - "command.annotateImage": "Annotate Image…", + "command.annotateImage": "Open with Annotation Editor", "command.publishResources": "Publish Resources…", "command.insertResourceReference": "Insert Resource Reference", "command.revealResource": "Reveal Resource", diff --git a/package.nls.ko.json b/package.nls.ko.json index 4e215990..ad783e2b 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -84,7 +84,7 @@ "view.resources": "리소스", "viewsWelcome.run": "src/config.py 가 있는 ok-script 프로젝트 폴더를 열거나 [프로젝트 경로 선택](command:okScriptToolkit.pickProject).", "command.captureToInbox": "받은 편지로 캡처", - "command.annotateImage": "이미지 주석…", + "command.annotateImage": "주석 편집기로 열기", "command.publishResources": "리소스 게시…", "command.insertResourceReference": "리소스 참조 삽입", "command.revealResource": "리소스 표시", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 35cc5cf5..d42812c6 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -84,7 +84,7 @@ "view.resources": "资源", "viewsWelcome.run": "打开含 src/config.py 的 ok-script 项目文件夹,或 [选择项目路径](command:okScriptToolkit.pickProject)。", "command.captureToInbox": "截到收件箱", - "command.annotateImage": "标注图片…", + "command.annotateImage": "用标注编辑器打开", "command.publishResources": "发布资源…", "command.insertResourceReference": "插入资源引用", "command.revealResource": "在资源中定位", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index dbd406e1..c02b154d 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -84,7 +84,7 @@ "view.resources": "資源", "viewsWelcome.run": "打開含 src/config.py 的 ok-script 專案資料夾,或 [選擇專案路徑](command:okScriptToolkit.pickProject)。", "command.captureToInbox": "截到收件匣", - "command.annotateImage": "標註圖片…", + "command.annotateImage": "用標註編輯器開啟", "command.publishResources": "發佈資源…", "command.insertResourceReference": "插入資源引用", "command.revealResource": "在資源中定位", From 8bc6f3f29f98d4e55b9042e031229c859ddf8c79 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:10:41 +0800 Subject: [PATCH 23/29] feat(publish): read-only publish preview before writing After every publish decision is collected and before anything is written, a QuickPick lists the files that will be written and the template set change (new / overwrite / removed), compared by name only since saveToAssets re-packs bboxes. Esc writes nothing; saveToAssets and position publish semantics are unchanged. --- l10n/bundle.l10n.es.json | 10 ++++- l10n/bundle.l10n.ja.json | 10 ++++- l10n/bundle.l10n.json | 10 ++++- l10n/bundle.l10n.ko.json | 10 ++++- l10n/bundle.l10n.zh-cn.json | 10 ++++- l10n/bundle.l10n.zh-tw.json | 10 ++++- package.json | 5 ++- scripts/test_publish_preview.js | 15 +++++++ src/extension.ts | 2 + src/publishPreview.ts | 63 +++++++++++++++++++++++++++ src/templateAssetPanel.ts | 76 +++++++++++++++++++++++++++++++++ 11 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 scripts/test_publish_preview.js create mode 100644 src/publishPreview.ts diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 83ae89ad..8aa60fe3 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -303,5 +303,13 @@ "Find References": "Buscar referencias", "Select a template to find references": "Elige una plantilla", "No references to “{name}” found in Python files.": "No hay referencias a “{name}” en archivos Python.", - "{count} reference(s) to “{name}”": "{count} referencia(s) a “{name}”" + "{count} reference(s) to “{name}”": "{count} referencia(s) a “{name}”", + "new": "nuevo", + "overwrite": "sobrescribir", + "removed from published": "se quitará de publicado", + "Write the files below": "Escribir los archivos siguientes", + "Files to write": "Archivos a escribir", + "Templates: {add} new · {keep} overwrite · {remove} removed": "Plantillas: {add} nuevas · {keep} sobrescritas · {remove} quitadas", + "Publish preview": "Vista previa de publicación", + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "Revisa lo que se escribirá. Enter en “Publicar” para seguir, Esc para cancelar." } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index e819fcfb..7a873e89 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -303,5 +303,13 @@ "Find References": "参照を検索", "Select a template to find references": "参照を検索するテンプレートを選択", "No references to “{name}” found in Python files.": "Python ファイルに “{name}” の参照はありません。", - "{count} reference(s) to “{name}”": "“{name}” の参照 {count} 件" + "{count} reference(s) to “{name}”": "“{name}” の参照 {count} 件", + "new": "新規", + "overwrite": "上書き", + "removed from published": "公開版から削除", + "Write the files below": "以下のファイルを書き込む", + "Files to write": "書き込むファイル", + "Templates: {add} new · {keep} overwrite · {remove} removed": "テンプレート:新規 {add} · 上書き {keep} · 削除 {remove}", + "Publish preview": "公開プレビュー", + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "書き込み内容を確認。「公開」で Enter、Esc で取消。" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index f03c6c7e..b84100d8 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -303,5 +303,13 @@ "Find References": "Find References", "Select a template to find references": "Select a template to find references", "No references to “{name}” found in Python files.": "No references to “{name}” found in Python files.", - "{count} reference(s) to “{name}”": "{count} reference(s) to “{name}”" + "{count} reference(s) to “{name}”": "{count} reference(s) to “{name}”", + "new": "new", + "overwrite": "overwrite", + "removed from published": "removed from published", + "Write the files below": "Write the files below", + "Files to write": "Files to write", + "Templates: {add} new · {keep} overwrite · {remove} removed": "Templates: {add} new · {keep} overwrite · {remove} removed", + "Publish preview": "Publish preview", + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "Review what will be written. Enter on “Publish” to continue, Esc to cancel." } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 5e08e96a..200182bb 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -303,5 +303,13 @@ "Find References": "참조 찾기", "Select a template to find references": "참조를 찾을 템플릿 선택", "No references to “{name}” found in Python files.": "Python 파일에서 “{name}” 참조를 찾지 못했습니다.", - "{count} reference(s) to “{name}”": "“{name}” 참조 {count}개" + "{count} reference(s) to “{name}”": "“{name}” 참조 {count}개", + "new": "신규", + "overwrite": "덮어쓰기", + "removed from published": "게시본에서 제거", + "Write the files below": "아래 파일 쓰기", + "Files to write": "쓸 파일", + "Templates: {add} new · {keep} overwrite · {remove} removed": "템플릿: 신규 {add} · 덮어쓰기 {keep} · 제거 {remove}", + "Publish preview": "게시 미리보기", + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "쓸 내용을 확인하세요. “게시”에서 Enter, Esc로 취소." } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 0abce7a8..a43ef9af 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -303,5 +303,13 @@ "Find References": "查找引用", "Select a template to find references": "选择要查找引用的模板", "No references to “{name}” found in Python files.": "Python 文件中没有找到 “{name}” 的引用。", - "{count} reference(s) to “{name}”": "“{name}” 共 {count} 处引用" + "{count} reference(s) to “{name}”": "“{name}” 共 {count} 处引用", + "new": "新增", + "overwrite": "覆盖", + "removed from published": "将从已发布中移除", + "Write the files below": "写入下列文件", + "Files to write": "将写入的文件", + "Templates: {add} new · {keep} overwrite · {remove} removed": "模板:新增 {add} · 覆盖 {keep} · 移除 {remove}", + "Publish preview": "发布预览", + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "确认将写入的内容。在「发布」上按 Enter 继续,Esc 取消。" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index ebae7dc9..91235989 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -303,5 +303,13 @@ "Find References": "尋找參考", "Select a template to find references": "選擇要尋找參考的模板", "No references to “{name}” found in Python files.": "Python 檔案中沒有找到 “{name}” 的參考。", - "{count} reference(s) to “{name}”": "“{name}” 共 {count} 處參考" + "{count} reference(s) to “{name}”": "“{name}” 共 {count} 處參考", + "new": "新增", + "overwrite": "覆寫", + "removed from published": "將從已發佈中移除", + "Write the files below": "寫入下列檔案", + "Files to write": "將寫入的檔案", + "Templates: {add} new · {keep} overwrite · {remove} removed": "模板:新增 {add} · 覆寫 {keep} · 移除 {remove}", + "Publish preview": "發佈預覽", + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "確認將寫入的內容。在「發佈」上按 Enter 繼續,Esc 取消。" } diff --git a/package.json b/package.json index cc0757b8..5603e242 100644 --- a/package.json +++ b/package.json @@ -714,7 +714,7 @@ "verify:version": "node scripts/release/verify-version.js", "verify:l10n": "node scripts/release/verify-l10n.js", "release": "sh scripts/release.sh", - "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:hover-links && npm run test:template-references && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", + "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:hover-links && npm run test:template-references && npm run test:publish-preview && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", "test:webview-html": "node scripts/test_webview_html.js", "test:design-system": "node scripts/test_design_system.js", "test:thumbnail-actions": "node scripts/test_thumbnail_actions.js", @@ -771,7 +771,8 @@ "test:publish-diff": "node scripts/test_publish_diff.js", "test:character-diagnostics": "node scripts/test_character_diagnostics.js", "test:hover-links": "node scripts/test_hover_links.js", - "test:template-references": "node scripts/test_template_references.js" + "test:template-references": "node scripts/test_template_references.js", + "test:publish-preview": "node scripts/test_publish_preview.js" }, "devDependencies": { "@types/jpeg-js": "^0.3.0", diff --git a/scripts/test_publish_preview.js b/scripts/test_publish_preview.js new file mode 100644 index 00000000..b009dcb2 --- /dev/null +++ b/scripts/test_publish_preview.js @@ -0,0 +1,15 @@ +const assert = require('assert'); +const path = require('path'); +const { buildPublishPreview } = require(path.join(__dirname, '..', 'out', 'publishPreview')); +const p = buildPublishPreview({ + template: { targetLabel: 'assets/', enumPath: 'src/LabelEnum.py', draftNames: ['b', 'a', 'a'], publishedNames: ['a', 'old'] }, + position: { format: 'json', files: ['assets/positions.json'], rect: true, point: false }, +}); +assert.deepStrictEqual(p.files.map((f) => f.label), [ + 'assets/coco_annotations.json', 'assets/images/', 'src/LabelEnum.py', 'assets/positions.json', +]); +assert.deepStrictEqual(p.templates.map((r) => `${r.kind}:${r.label}`), ['keep:a', 'add:b', 'remove:old']); +assert.deepStrictEqual(p.counts, { add: 1, keep: 1, remove: 1 }); +const empty = buildPublishPreview({}); +assert.deepStrictEqual(empty.files, []); +console.log('ok publish preview'); diff --git a/src/extension.ts b/src/extension.ts index e6efadbc..9f883820 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -39,6 +39,7 @@ import { TemplateAssetViewProvider, TemplateAssetPanel, repaintAllAssetGalleries, + setPublishedTemplateNamesProvider, } from './templateAssetPanel'; import { TempScreenshotStore } from './tempScreenshotStore'; import { TempScreenshotViewProvider } from './tempScreenshotPanel'; @@ -298,6 +299,7 @@ export function activate(context: vscode.ExtensionContext): void { gameConnect.overlayForwarder = (enabled) => okConsole.setOverlayEnabled(enabled); const templateAssetData = new TemplateAssetData(targetRoot); + setPublishedTemplateNamesProvider(() => [...features.all()].map((ft) => ft.name)); const tempScreenshotStore = new TempScreenshotStore( path.join(context.globalStorageUri.fsPath, 'temp-screenshots', wsHash), ); diff --git a/src/publishPreview.ts b/src/publishPreview.ts new file mode 100644 index 00000000..c37b3152 --- /dev/null +++ b/src/publishPreview.ts @@ -0,0 +1,63 @@ +/** + * 发布预览(纯函数):在真正写盘前列出「会写哪些文件、模板集合怎么变」。 + * + * 只读、只比较名字:发布会把草稿裁切 + 装箱成新的运行时 COCO(saveToAssets 整体替换), + * 草稿与已发布的 bbox 坐标系不同,比较坐标会把所有模板误报成「已修改」,所以这里不做。 + */ +export interface PublishPreviewInput { + template?: { + targetLabel: string; + enumPath?: string; + draftNames: string[]; + publishedNames: string[]; + }; + position?: { + format: 'json' | 'python'; + files: string[]; + rect: boolean; + point: boolean; + }; +} + +export type PreviewRowKind = 'file' | 'add' | 'keep' | 'remove'; + +export interface PreviewRow { + kind: PreviewRowKind; + label: string; +} + +export interface PublishPreview { + files: PreviewRow[]; + templates: PreviewRow[]; + counts: { add: number; keep: number; remove: number }; +} + +export function buildPublishPreview(input: PublishPreviewInput): PublishPreview { + const files: PreviewRow[] = []; + const templates: PreviewRow[] = []; + const counts = { add: 0, keep: 0, remove: 0 }; + if (input.template) { + const t = input.template; + const target = t.targetLabel.replace(/[\\/]+$/, ''); + files.push({ kind: 'file', label: `${target}/coco_annotations.json` }); + files.push({ kind: 'file', label: `${target}/images/` }); + if (t.enumPath) files.push({ kind: 'file', label: t.enumPath }); + const draft = [...new Set(t.draftNames)].sort((a, b) => a.localeCompare(b)); + const published = new Set(t.publishedNames); + const draftSet = new Set(draft); + for (const name of draft) { + const kind: PreviewRowKind = published.has(name) ? 'keep' : 'add'; + counts[kind] += 1; + templates.push({ kind, label: name }); + } + for (const name of [...published].sort((a, b) => a.localeCompare(b))) { + if (draftSet.has(name)) continue; + counts.remove += 1; + templates.push({ kind: 'remove', label: name }); + } + } + if (input.position) { + for (const f of input.position.files) files.push({ kind: 'file', label: f }); + } + return { files, templates, counts }; +} diff --git a/src/templateAssetPanel.ts b/src/templateAssetPanel.ts index 6e63d5c8..2798fdc4 100644 --- a/src/templateAssetPanel.ts +++ b/src/templateAssetPanel.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; +import { buildPublishPreview } from './publishPreview'; import { TemplateAssetData } from './templateAssetData'; import { AnnotationPanel } from './annotationPanel'; import { cropTemplateThumbFileAsync, THUMB_HEIGHT } from './pngCrop'; @@ -42,6 +43,12 @@ interface TemplatePublishPlan { absEnumPath?: string; } +/** 当前已发布的模板名(extension 注入 FeatureData;没注入时预览只列文件、不列增删) */ +let publishedTemplateNames: (() => string[]) | undefined; +export function setPublishedTemplateNamesProvider(provider: () => string[]): void { + publishedTemplateNames = provider; +} + type PublishResource = 'template' | 'rect' | 'point'; type PublishResourceItem = vscode.QuickPickItem & { resource: PublishResource }; @@ -358,10 +365,79 @@ class AssetGalleryController { const positionPlan = wantsPosition ? await this.preparePositionPublish(positionSelection) : undefined; if (wantsPosition && !positionPlan) return; + // 只读预览:确认前不写任何文件;取消 = 什么都不发生(与上面的「先收集决定」同一保证) + if (!(await this.confirmPublishPreview(templatePlan, positionPlan))) return; + if (positionPlan && !(await this.executePositionPublish(positionPlan))) return; if (templatePlan) await this.executeTemplatePublish(templatePlan); } + private draftTemplateNames(): string[] { + this.data.load(); + const names: string[] = []; + for (const image of this.data.listImages()) { + for (const ann of this.data.getAnnotationsForImage(image, true)) names.push(ann.categoryName); + } + return names; + } + + private async confirmPublishPreview( + templatePlan: TemplatePublishPlan | undefined, + positionPlan: PositionPublishPlan | undefined, + ): Promise { + const rel = (file: string, root: string) => path.relative(root, file).split(path.sep).join('/') || file; + const preview = buildPublishPreview({ + template: templatePlan ? { + targetLabel: templatePlan.targetLabel, + enumPath: templatePlan.absEnumPath ? rel(templatePlan.absEnumPath, templatePlan.projectRoot) : undefined, + draftNames: this.draftTemplateNames(), + publishedNames: publishedTemplateNames?.() ?? [], + } : undefined, + position: positionPlan ? { + format: positionPlan.format, + rect: positionPlan.options.rect, + point: positionPlan.options.point, + files: positionPlan.format === 'python' + ? ['ScreenRatio.py', 'PositionMap.py'].map((f) => `${positionPlan.options.pythonTargetDir.replace(/\/+$/, '')}/${f}`) + : [positionPlan.options.jsonTarget], + } : undefined, + }); + type Row = vscode.QuickPickItem & { publish?: boolean }; + const icon = { file: '$(file)', add: '$(diff-added)', keep: '$(circle-filled)', remove: '$(diff-removed)' } as const; + const kindText = { file: '', add: tr('new'), keep: tr('overwrite'), remove: tr('removed from published') } as const; + const items: Row[] = [ + { label: `$(cloud-upload) ${tr('Publish')}`, description: tr('Write the files below'), publish: true, alwaysShow: true }, + { label: tr('Files to write'), kind: vscode.QuickPickItemKind.Separator }, + ...preview.files.map((row) => ({ label: `${icon.file} ${row.label}` })), + ]; + if (preview.templates.length) { + items.push({ + label: tr('Templates: {add} new · {keep} overwrite · {remove} removed', preview.counts), + kind: vscode.QuickPickItemKind.Separator, + }); + items.push(...preview.templates.map((row) => ({ label: `${icon[row.kind]} ${row.label}`, description: kindText[row.kind] }))); + } + return new Promise((resolve) => { + const qp = vscode.window.createQuickPick(); + qp.title = tr('Publish preview'); + qp.placeholder = tr('Review what will be written. Enter on “Publish” to continue, Esc to cancel.'); + qp.items = items; + qp.activeItems = [items[0]]; + let done = false; + qp.onDidAccept(() => { + if (!qp.selectedItems[0]?.publish) return; // 只读行:不做任何事 + done = true; + resolve(true); + qp.hide(); + }); + qp.onDidHide(() => { + qp.dispose(); + if (!done) resolve(false); + }); + qp.show(); + }); + } + private async handlePublishPositions(selection: { rect: boolean; point: boolean }): Promise { const plan = await this.preparePositionPublish(selection); if (plan) await this.executePositionPublish(plan); From 59199498e09d22835e75336b7dd20b13d297199c Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:10:58 +0800 Subject: [PATCH 24/29] fix(resources): list drafts by template name, compare draft/published by name only P1 keyed draft cards by image file name, so they never matched the published fL. entries, and compared bboxes across coordinate systems (publish re-packs crops), flagging everything as modified. --- src/run/resourcesView.ts | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/run/resourcesView.ts b/src/run/resourcesView.ts index 134881a2..4fe838fe 100644 --- a/src/run/resourcesView.ts +++ b/src/run/resourcesView.ts @@ -8,7 +8,7 @@ import { TemplateAssetData } from '../templateAssetData'; import { TemplateAssetPanel } from '../templateAssetPanel'; import { TempScreenshotStore } from '../tempScreenshotStore'; import { applySharedAssets, errorPage, getNonce } from '../webviewHtml'; -import { countUnpublished, diffPublishSets, fingerprintBox } from './publishDiff'; +import { countUnpublished, diffPublishSets } from './publishDiff'; import { primaryFeatureAlias } from '../templatePanel'; import { templatesDirectory } from '../projectConfig'; @@ -164,20 +164,24 @@ export class ResourcesViewProvider implements vscode.WebviewViewProvider, vscode this.features.refresh(true); const alias = primaryFeatureAlias(); const draftImages = this.assets.listImages(); - const draftItems = draftImages.map((imagePath) => { - const name = path.basename(imagePath, path.extname(imagePath)); - const anns = this.assets.getAnnotationsForImage(imagePath, true); - const first = anns[0]; - return { - name, - imagePath, - kind: 'template' as const, - expression: `${alias}.${name}`, - width: first?.bbox?.[2], - height: first?.bbox?.[3], - draftFingerprint: first?.bbox ? fingerprintBox({ bbox: first.bbox as number[] }) : undefined, - }; - }); + // 草稿按模板名(标注类别)列出,和已发布的 fL.<名字> 对得上; + // 不比较 bbox:发布会裁切 + 装箱,草稿与运行时坐标系不同(见 publishPreview.ts) + const draftItems: { name: string; imagePath: string; kind: 'template'; expression: string; width?: number; height?: number }[] = []; + const seen = new Set(); + for (const imagePath of draftImages) { + for (const ann of this.assets.getAnnotationsForImage(imagePath, true)) { + if (!ann.categoryName || seen.has(ann.categoryName)) continue; + seen.add(ann.categoryName); + draftItems.push({ + name: ann.categoryName, + imagePath, + kind: 'template', + expression: `${alias}.${ann.categoryName}`, + width: ann.bbox?.[2] !== undefined ? Math.round(ann.bbox[2]) : undefined, + height: ann.bbox?.[3] !== undefined ? Math.round(ann.bbox[3]) : undefined, + }); + } + } const published = [...this.features.all()].map((ft) => ({ name: ft.name, imagePath: ft.imagePath, @@ -185,11 +189,10 @@ export class ResourcesViewProvider implements vscode.WebviewViewProvider, vscode expression: `${alias}.${ft.name}`, width: ft.width, height: ft.height, - publishedFingerprint: fingerprintBox({ bbox: ft.bbox, width: ft.width, height: ft.height }), })); const diffs = diffPublishSets( - draftItems.map((d) => ({ name: d.name, draftFingerprint: d.draftFingerprint })), - published.map((p) => ({ name: p.name, publishedFingerprint: p.publishedFingerprint })), + draftItems.map((d) => ({ name: d.name })), + published.map((p) => ({ name: p.name })), ); const statuses = Object.fromEntries(diffs.map((d) => [d.name, d.status])); const unpublished = countUnpublished(diffs); From ba8e8b623a36ab65821b650f02bca8a58460fb61 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:12:03 +0800 Subject: [PATCH 25/29] test: activation smoke test with a mocked vscode module; document limits @vscode/test-electron is not a dependency and needs a display, so no real Extension Host or screenshot regression; documented in docs/testing.md. --- docs/testing.en.md | 16 ++++++ docs/testing.md | 16 ++++++ package.json | 5 +- scripts/test_activation_smoke.js | 96 ++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 docs/testing.en.md create mode 100644 docs/testing.md create mode 100644 scripts/test_activation_smoke.js diff --git a/docs/testing.en.md b/docs/testing.en.md new file mode 100644 index 00000000..b51b4382 --- /dev/null +++ b/docs/testing.en.md @@ -0,0 +1,16 @@ +# Testing + +`npm test` runs entirely in Node — no VS Code, display or network: + +- Pure unit tests (`scripts/test_*.js`, against `out/`): tree model, publish diff, publish preview, template reference finder, hover link snapshot, … +- Webview tests load `media/*/index.html` and scripts in jsdom. +- Contract tests: `test_command_contract.js` (declared / registered / NLS / menus / when), `verify:l10n`, design-system audit. +- **Activation smoke test** `test_activation_smoke.js`: loads `package.json` `main` with a permissive mocked `vscode` module, calls `activate()`, and asserts every declared command is registered and every view has a provider. + +## Limitations + +- `@vscode/test-electron` is not a dependency; it downloads VS Code and needs a display (xvfb on Linux), which this sandbox and CI lack. There is no real Extension Host integration test and **no screenshot regression**. +- The mock only proves registration, not UI behaviour; webview rendering, the Python executor and real game screenshots still need manual acceptance. +- UI screenshots (`/workspace/ok-toolkit-ui-shots/`) are headless-Chrome renders of webview HTML — visual reference, not an automated baseline. + +To add real integration tests later: add `@vscode/test-electron` + `@vscode/test-cli`, run under `xvfb-run` in CI, and move the smoke assertions into `suite/extension.test.ts`. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..217b7d14 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,16 @@ +# 测试说明 + +`npm test` 全部在 Node 里跑,不需要 VS Code、显示器或网络: + +- 纯逻辑单测(`scripts/test_*.js`,读 `out/`):树模型、发布差异、发布预览、模板引用查找、悬停链接快照等。 +- Webview 测试用 jsdom 加载 `media/*/index.html` 与脚本。 +- 契约测试:`test_command_contract.js`(命令声明 / 注册 / NLS / 菜单 / when)、`verify:l10n`、设计系统审计。 +- **激活冒烟测试** `test_activation_smoke.js`:用宽松的 `vscode` 模拟模块加载 `package.json` 的 `main`,调用 `activate()`,断言每个声明的命令都已注册、每个视图都有 provider。 + +## 局限 + +- 依赖里没有 `@vscode/test-electron`;它要下载 VS Code 并需要显示器(Linux 上要 xvfb),当前沙箱和 CI 都不具备,所以没有真实 Extension Host 集成测试,也**没有截图回归**。 +- 模拟模块只验证“注册了”,不验证 UI 行为;webview 渲染、Python 执行器、真实游戏截图仍需人工验收。 +- UI 截图(`/workspace/ok-toolkit-ui-shots/`)由 headless Chrome 渲染 webview HTML 得到,是视觉参考而非自动比对基线。 + +以后要上真集成测试:加 `@vscode/test-electron` + `@vscode/test-cli`,CI 用 `xvfb-run`,把冒烟测试里的断言搬进 `suite/extension.test.ts` 即可。 diff --git a/package.json b/package.json index 5603e242..3e82fa8a 100644 --- a/package.json +++ b/package.json @@ -714,7 +714,7 @@ "verify:version": "node scripts/release/verify-version.js", "verify:l10n": "node scripts/release/verify-l10n.js", "release": "sh scripts/release.sh", - "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:hover-links && npm run test:template-references && npm run test:publish-preview && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", + "test": "npm run verify:version && npm run verify:l10n && npm run test:l10n-literals && npm run compile && npm run test:webview-l10n && npm run test:webview-html && npm run test:design-system && npm run test:command-contract && npm run test:run-tree-model && npm run test:publish-diff && npm run test:character-diagnostics && npm run test:hover-links && npm run test:template-references && npm run test:publish-preview && npm run test:activation-smoke && npm run test:thumbnail-actions && npm run test:task-launcher && npm run test:console-accountseg && npm run test:task-launcher-executor && npm run test:task-reconcile && npm run test:temp-screenshots && npm run test:annotation-coords && npm run test:annotation-clipboard && npm run test:annotation-swap && npm run test:annotation-merge && npm run test:annotation-history && npm run test:annotation-mode-cycle && npm run test:position-resource && npm run test:resource-host-contract && npm run test:position-publish-settings && npm run test:point-resource && npm run test:asset-swap-picker && npm run test:thumb-content-hash && npm run test:list-editor && npm run test:project-config && npm run test:project-config-schema && npm run test:convention-sources && npm run test:label-enum-guard && npm run test:save-to-assets-flow && npm run test:save-to-assets && npm run test:asset-pack-worker && npm run test:coco-feature-path && npm run test:box-resource && npm run test:probe-schemas && npm run test:probe-gui-group-names && npm run test:probe-qt-layer && npm run test:probe-multi-account-path && npm run test:probe-window-config && npm run test:screenshot-capture && npm run test:screenshot-worker && npm run test:background-screenshot && npm run test:global-hotkey && npm run test:run-sandbox && npm run test:startup-patches && npm run test:executor-runtime && npm run test:executor-project-config && npm run test:executor-gconfig && npm run test:project-store && npm run test:project-runtime", "test:webview-html": "node scripts/test_webview_html.js", "test:design-system": "node scripts/test_design_system.js", "test:thumbnail-actions": "node scripts/test_thumbnail_actions.js", @@ -772,7 +772,8 @@ "test:character-diagnostics": "node scripts/test_character_diagnostics.js", "test:hover-links": "node scripts/test_hover_links.js", "test:template-references": "node scripts/test_template_references.js", - "test:publish-preview": "node scripts/test_publish_preview.js" + "test:publish-preview": "node scripts/test_publish_preview.js", + "test:activation-smoke": "node scripts/test_activation_smoke.js" }, "devDependencies": { "@types/jpeg-js": "^0.3.0", diff --git a/scripts/test_activation_smoke.js b/scripts/test_activation_smoke.js new file mode 100644 index 00000000..0973061a --- /dev/null +++ b/scripts/test_activation_smoke.js @@ -0,0 +1,96 @@ +/** + * 激活冒烟测试(无 VS Code 进程):用一个宽松的 vscode 模拟模块加载 package.json main(out/entrypoint.js), + * 调用 activate(),断言 package.json 里声明的每个命令都注册了、每个视图都有 provider。 + * + * 局限:这不是 @vscode/test-electron 的真集成测试——不渲染 webview、不跑 Python、不做截图回归。 + * 依赖里没有 test-electron,且 CI/沙箱无显示器也无法下载 VS Code,见 docs/testing.md。 + */ +const assert = require('assert'); +const Module = require('module'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +const root = path.join(__dirname, '..'); +const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const registered = new Set(); +const providers = new Set(); + +// 任意属性 / 调用都返回自身的“万能”对象;then 返回 undefined 以免被当成 Promise +function loose(name = 'mock') { + const fn = function () { return loose(name + '()'); }; + const store = new Map(); + return new Proxy(fn, { + set(_t, prop, value) { store.set(prop, value); return true; }, + defineProperty(_t, prop, desc) { store.set(prop, desc.value); return true; }, + get(_t, prop) { + if (store.has(prop)) return store.get(prop); + if (prop === 'then') return undefined; + if (prop === Symbol.toPrimitive) return () => ''; + if (prop === Symbol.iterator) return function* () {}; + if (prop === 'dispose') return () => {}; + if (prop === 'length') return 0; + return loose(`${name}.${String(prop)}`); + }, + construct() { return loose(`new ${name}`); }, + }); +} +class EventEmitter { + constructor() { this.l = []; this.event = (f) => { this.l.push(f); return { dispose() {} }; }; } + fire(v) { for (const f of this.l) f(v); } + dispose() {} +} +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'okst-smoke-')); +const config = { get: (_k, d) => d, has: () => false, inspect: () => undefined, update: async () => {} }; +const vscodeMock = new Proxy({ + EventEmitter, + Disposable: class { constructor(f) { this.f = f; } dispose() { this.f && this.f(); } static from() { return { dispose() {} }; } }, + Uri: { file: (p) => ({ fsPath: p, path: p, scheme: 'file', toString: () => p }), joinPath: (u, ...s) => ({ fsPath: path.join(u.fsPath, ...s), path: path.join(u.fsPath, ...s) }), parse: (s) => ({ fsPath: s, toString: () => s }) }, + l10n: { t: (s, a) => (typeof s === 'string' ? s : s.message), bundle: undefined, uri: undefined }, + env: { language: 'en', clipboard: { writeText: async () => {} } }, + commands: { + registerCommand: (id) => { registered.add(id); return { dispose() {} }; }, + registerTextEditorCommand: (id) => { registered.add(id); return { dispose() {} }; }, + executeCommand: async () => undefined, + }, + window: new Proxy({ + registerWebviewViewProvider: (id) => { providers.add(id); return { dispose() {} }; }, + createTreeView: (id) => { providers.add(id); return loose('treeView'); }, + registerTreeDataProvider: (id) => { providers.add(id); return { dispose() {} }; }, + activeTextEditor: undefined, + visibleTextEditors: [], + }, { get: (t, p) => (p in t ? t[p] : loose(`window.${String(p)}`)) }), + workspace: new Proxy({ + workspaceFolders: [{ uri: { fsPath: tmp, path: tmp, toString: () => tmp }, name: 'smoke', index: 0 }], + getConfiguration: () => config, + textDocuments: [], + }, { get: (t, p) => (p in t ? t[p] : loose(`workspace.${String(p)}`)) }), +}, { get: (t, p) => (p in t ? t[p] : loose(`vscode.${String(p)}`)) }); + +const origLoad = Module._load; +Module._load = function (request, ...rest) { + if (request === 'vscode') return vscodeMock; + return origLoad.call(this, request, ...rest); +}; + +(async () => { + const ext = require(path.join(root, pkg.main)); + const context = { + subscriptions: [], extensionUri: { fsPath: root, path: root }, extensionPath: root, + globalState: { get: () => undefined, update: async () => {}, keys: () => [] }, + workspaceState: { get: () => undefined, update: async () => {}, keys: () => [] }, + globalStorageUri: { fsPath: tmp }, storageUri: { fsPath: tmp }, logUri: { fsPath: tmp }, + asAbsolutePath: (p) => path.join(root, p), secrets: loose('secrets'), environmentVariableCollection: loose('env'), + }; + await ext.activate(context); + const declared = pkg.contributes.commands.map((c) => c.command); + const missing = declared.filter((c) => !registered.has(c)); + assert.deepStrictEqual(missing, [], `declared but not registered: ${missing}`); + const views = Object.values(pkg.contributes.views).flat().map((v) => v.id); + const noProvider = views.filter((v) => !providers.has(v)); + assert.deepStrictEqual(noProvider, [], `views without provider: ${noProvider}`); + assert.ok(context.subscriptions.length > 0); + if (ext.deactivate) await ext.deactivate(); + console.log(`ok activation smoke (${declared.length} commands, ${views.length} views)`); + process.exit(0); +})().catch((e) => { console.error(e); process.exit(1); }); From a175201958f913b9b34968fdb869c71561d50263 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:12:03 +0800 Subject: [PATCH 26/29] docs: JetBrains parity checklist for UI P2 (docs only) --- docs/feature-parity.en.md | 11 +++++++++++ docs/feature-parity.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/feature-parity.en.md b/docs/feature-parity.en.md index 7402ad85..32428a0b 100644 --- a/docs/feature-parity.en.md +++ b/docs/feature-parity.en.md @@ -56,3 +56,14 @@ Business projects load published files. The plugin provides current declarations - Confirmed code differences are closed, and the main PR gitlink pins the companion child PR commit. Merge child PR #30 first, then confirm or update the gitlink to a commit reachable from child `main` and verify main CI before merging the main PR. Unmerged code is not released functionality. No actual IDE interaction, game screenshot or business-project runtime acceptance was performed. Full Plugin Verifier API compatibility was not run; build, structure checks and automated regression do not replace those checks. + +## UI P2 parity checklist (done in VS Code, pending in JetBrains; no JetBrains UI changes this round) + +| Item | VS Code (`ui/p2-polish`) | Suggested JetBrains approach | Status | +| --- | --- | --- | --- | +| Image context menu "Open with Annotation Editor" | `explorer/context`, only `.png/.jpg/.jpeg/.webp` | Project View `AnAction`, visibility by extension in `update()` | Pending | +| Find template references | `findTemplateReferences`: `fL.x` / `self.pos.x` / `"x"` → QuickPick → jump | `ReferencesSearch` / custom Find Usages searcher with the same regexes | Pending | +| Command links in template hover | `MarkdownString.isTrusted.enabledCommands` allowlist of two commands | Links in `DocumentationProvider` | Pending | +| Publish preview | Read-only list of files + template add/overwrite/remove (by name) before writing | Read-only list before the publish dialog; write semantics unchanged | Pending | +| Resources drafts listed by template name | Fixed: draft cards use the annotation category name, matching `fL.` | Same in the resource list | Check | +| Activation smoke test | `scripts/test_activation_smoke.js` (mocked vscode module) | `gradlew test` exists; could add an action-registration check | Optional | diff --git a/docs/feature-parity.md b/docs/feature-parity.md index d0fac54b..631473c1 100644 --- a/docs/feature-parity.md +++ b/docs/feature-parity.md @@ -56,3 +56,14 @@ - 本轮代码差异已补齐,主仓 PR 的 gitlink 指向子仓 PR 的配套提交。必须先合并子仓 PR #30,再确认或更新 gitlink 到子仓 `main` 可达的提交并核对主仓 CI,最后合并主仓;未合并代码不作为已发布功能。 没有执行真实 IDE 交互、真实游戏截图或业务项目运行验收,也未执行完整 Plugin Verifier API 兼容性检查;构建、结构和自动回归的成功不代替这些验收。 + +## UI P2 对齐清单(VS Code 已做,JetBrains 待办;本轮不改 JetBrains UI) + +| 项 | VS Code(`ui/p2-polish`) | JetBrains 建议做法 | 状态 | +| --- | --- | --- | --- | +| 图片右键「用标注编辑器打开」 | `explorer/context`,仅 `.png/.jpg/.jpeg/.webp` | Project View `AnAction`,`update()` 按扩展名显隐 | 待办 | +| 查找模板引用 | `findTemplateReferences`:`fL.x` / `self.pos.x` / `"x"` → QuickPick 跳转 | `ReferencesSearch` 或 Find Usages 自定义 searcher,复用同一正则 | 待办 | +| 模板悬停命令链接 | `MarkdownString.isTrusted.enabledCommands` 白名单两条命令 | `DocumentationProvider` 里用 `psi_element://` / 自定义 action 链接 | 待办 | +| 发布预览 | 写盘前只读列出文件 + 模板新增/覆盖/移除(按名字比较) | 发布对话框前插入只读列表;不改写入语义 | 待办 | +| 资源视图草稿按模板名列出 | 修正:草稿卡片按标注类别名,与 `fL.<名字>` 对齐 | 资源列表同样按类别名 | 核对 | +| 激活冒烟测试 | `scripts/test_activation_smoke.js`(模拟 vscode 模块) | 已有 `gradlew test`,可补 action 注册检查 | 可选 | From 2fd64dbff93173333a632ac65b6373340803cd72 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:12:25 +0800 Subject: [PATCH 27/29] docs(readme): references, hover links, explorer menu, publish preview --- README.en.md | 5 +++++ README.md | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.en.md b/README.en.md index 97c0f79f..6022c5f8 100644 --- a/README.en.md +++ b/README.en.md @@ -85,6 +85,9 @@ When editing Python code, the extension automatically detects ok-script-specific - **Template code hints**: Type `fL.` or `FeatureList.` to complete template names with size info. Hover shows thumbnail preview, dimensions, and source info. - You can also open a larger grid view in the editor area via the command **ok-script Toolkit: Open Resource Preview in Editor**. - Supports both sidebar (Resource Preview and Annotation Management views) and large editor window browsing modes. +- **Reveal / Find References**: hovering `fL.xxx` in Python shows **Reveal in Resources** and **Find References** links under the preview (only these two extension commands are trusted). **Find Template References…** (editor context menu or Command Palette) searches workspace Python files for `fL.xxx`, `self.pos.xxx` and string keys `"xxx"`, lists them in a QuickPick and jumps on Enter (comment lines skipped). +- **Explorer "Open with Annotation Editor"**: shown only on `.png/.jpg/.jpeg/.webp`. The image must already be registered in Annotation Management. +- **Publish preview**: after choosing publish options and before anything is written, a read-only list shows the files to be written and templates that are new / overwritten / removed from published (compared by name; publishing re-crops and re-packs, so coordinates are not comparable). Enter on **Publish** writes; Esc writes nothing. - **Annotation Management**: double-click / Enter / `E` opens the annotation editor, `Delete` deletes (after a confirmation). The delete button sits alone in the card's top-right corner, apart from view / swap. The **Open Annotation Editor…** command lets you pick an image directly. - **Swap annotations**: in Annotation Management, the **Swap annotations** button (codicon `arrow-swap`) at the bottom-right of a thumbnail card swaps that image's whole annotation set with another image in the same list — the fix for "annotated the wrong image / images are in the wrong order". The target is picked from thumbnails (template names are mostly numeric, so names alone are not enough). When the two images differ in size, box coordinates are **scaled proportionally** and the confirmation dialog spells out both sizes. The swap is written in one go and always asks before saving; the completion notification has an **Undo** button that restores both images' original annotations as long as neither image changed since. @@ -290,6 +293,8 @@ Command categories display as "ok-script 工具箱" / "ok-script Toolkit" depend | Command | Shortcut | Description | |---|---|---| +| `ok-script Toolkit: Find Template References…` | — | Find `fL.xxx` / `self.pos.xxx` / `"xxx"` usages in workspace Python files and jump; also in the Python editor context menu | +| `ok-script Toolkit: Open with Annotation Editor` | — | Explorer image context menu (png/jpg/jpeg/webp only); without an argument it asks for an image | | `ok-script Toolkit: Capture to Inbox` | `Ctrl+Alt+S` | | `ok-script Toolkit: Open Resource Preview` | `Ctrl+Alt+T` (macOS `Cmd+Alt+T`, requires Python editor focus) | Focus the Resource Preview view in the activity bar | | `ok-script Toolkit: Open Resource Preview in Editor` | — | Open a large grid view in the editor area | diff --git a/README.md b/README.md index 8adce23f..23b553eb 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) - 归一化坐标按图片宽高归一化到 0..1,保留 4 位小数,同样支持滚轮缩放、中键/空白拖拽平移。 - 框选完成后**框会保留并可继续调整**(带 8 向手柄、可整体拖动,交互与模板标注框一致):**创建时与每次调整结束都会重新复制一次当前框的坐标**;框本身不写入标注数据、不落盘。点击图片的非交互部分(框体与手柄之外)即清除。 - 注:VS Code 的跨 Webview 拖拽不可用(各 webview 是不同 origin 的 iframe,`dataTransfer` 被屏蔽),所以「添加到标注管理」按钮是导入的可靠入口。 +- **在资源中定位 / 查找引用**:Python 里悬停 `fL.xxx` 时,预览下方有「在资源中定位」「查找引用」两个链接(只放行这两条本插件命令)。「查找模板引用…」也可从编辑器右键或命令面板运行:在工作区 Python 文件里找 `fL.xxx`、`self.pos.xxx` 和字符串键 `"xxx"`,列在 QuickPick 里,回车跳转(跳过注释行)。 +- **资源管理器右键「用标注编辑器打开」**:只在 `.png/.jpg/.jpeg/.webp` 上出现。图片需已在标注管理里登记,否则会提示先导入。 +- **发布预览**:选完发布选项、真正写盘前,会弹出只读列表:将写入的文件,以及模板「新增 / 覆盖 / 将从已发布中移除」(按模板名比较;发布会重新裁切装箱,坐标不可比)。在「发布」上回车才写入,Esc 什么都不写。 - **标注编辑器**同样支持该模式:工具栏「坐标 (C)」或按 `C` 键后框选,直接复制同样的 `x,y,tox,toy` 归一化坐标,不会创建标注框、不改动 COCO。得到的框同样带手柄可继续微调,每次调整结束重新复制;点击图片非交互部分清除。快捷键可用 `okScriptToolkit.annotationKeybindings` 的 `copyCoords` 覆盖。 ### 运行(任务树) @@ -313,7 +316,9 @@ self.wait_click_ocr(match=self.lang.zip_line_mixin.k_2f4f4a2f, ...) | `ok-script 工具箱: 打开标注编辑器…` | — | 从标注管理的图片列表中选一张(显示标注数),直接打开 COCO 标注编辑器 | | `ok-script 工具箱: 发布资源…` | — | 打开标注管理并直接进入发布流程:选择 Template / Rect / Point 后发布 | | `ok-script 工具箱: 打开临时截图` | — | 聚焦活动栏中的临时截图视图 | -| `ok-script 工具箱: 截图并打开标注管理` | `Ctrl+Alt+S`(macOS `Cmd+Alt+S`) | 打开标注管理面板并立即截图。复用面板自己的截图动作,截图会登记进 COCO。键位可在「键盘快捷方式」里改 | +| `ok-script 工具箱: 截图并打开标注管理` | —(`Ctrl+Alt+S` 已改给「截到收件箱」) | 打开标注管理面板并立即截图。复用面板自己的截图动作,截图会登记进 COCO。键位可在「键盘快捷方式」里改 | +| `ok-script 工具箱: 查找模板引用…` | — | 在工作区 Python 文件里找 `fL.xxx` / `self.pos.xxx` / `"xxx"` 的引用并跳转;也在 Python 编辑器右键菜单里 | +| `ok-script 工具箱: 用标注编辑器打开` | — | 资源管理器图片右键(仅 png/jpg/jpeg/webp);命令面板里不带参数时先选图片 | | `ok-script 工具箱: 项目约定 vs 我的设置` | — | 列出参与取值链的设置项,显示每一项的**生效值来自哪一层**(我的设置 / 项目约定 / 内置默认)。被个人设置覆盖过的项带一个「恢复」按钮,一键回到项目约定。用于解决"我改过一次就再也看不到团队改了什么" | ## 更新后不生效 From 67b82affc065e914f46a20164a44f46de4ecd426 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Sun, 11 Oct 2026 07:15:04 +0800 Subject: [PATCH 28/29] test(publish): host contract covers the preview step and Esc-writes-nothing --- scripts/test_resource_host_contract.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/test_resource_host_contract.js b/scripts/test_resource_host_contract.js index b34814ac..11f37a25 100644 --- a/scripts/test_resource_host_contract.js +++ b/scripts/test_resource_host_contract.js @@ -79,7 +79,9 @@ function plain(value) { return JSON.parse(JSON.stringify(value)); } let templatePlan = {}; let positionPlan = {}; let positionSuccess = true; + let previewOk = true; const host = { + confirmPublishPreview: async () => { effects.push('preview'); return previewOk; }, data: templateData, prepareTemplatePublish: async () => { effects.push('configure template'); return templatePlan; }, preparePositionPublish: async picked => { @@ -95,7 +97,11 @@ function plain(value) { return JSON.parse(JSON.stringify(value)); } await publish.handlePublish.call(host); assert.deepStrictEqual(effects, expected); } - await run(['configure template', 'configure positions', 'write positions', 'write template']); + await run(['configure template', 'configure positions', 'preview', 'write positions', 'write template']); + // Esc on the read-only preview writes nothing. + previewOk = false; + await run(['configure template', 'configure positions', 'preview']); + previewOk = true; positionPlan = undefined; await run(['configure template', 'configure positions']); positionPlan = {}; @@ -103,12 +109,12 @@ function plain(value) { return JSON.parse(JSON.stringify(value)); } await run(['configure template']); templatePlan = {}; positionSuccess = false; - await run(['configure template', 'configure positions', 'write positions']); + await run(['configure template', 'configure positions', 'preview', 'write positions']); positionSuccess = true; selection = ['template']; - await run(['configure template', 'write template']); + await run(['configure template', 'preview', 'write template']); selection = ['point']; - await run(['configure positions', 'write positions']); + await run(['configure positions', 'preview', 'write positions']); selection = []; await run([]); @@ -117,7 +123,7 @@ function plain(value) { return JSON.parse(JSON.stringify(value)); } expectedResourceLabels = ['translated:Template']; rectAnnotations = []; pointItems = []; - await run(['configure template', 'write template']); + await run(['configure template', 'preview', 'write template']); templateAnnotations = []; expectedResourceLabels = []; @@ -156,7 +162,7 @@ function plain(value) { return JSON.parse(JSON.stringify(value)); } selection = ['template']; expectedResourceLabels = ['translated:Template']; const beforeMixedPick = quickPickCalls; - await run(['configure template', 'write template']); + await run(['configure template', 'preview', 'write template']); assert.strictEqual(quickPickCalls, beforeMixedPick + 1, 'valid templates remain selectable alongside empty boxes and invalid points'); rectAnnotations = [{ id: 1 }]; @@ -165,7 +171,7 @@ function plain(value) { return JSON.parse(JSON.stringify(value)); } templateData.readErrors = ['json']; selection = ['rect']; expectedResourceLabels = ['translated:Box']; - await run(['configure positions', 'write positions']); + await run(['configure positions', 'preview', 'write positions']); assert.strictEqual(quickPickCalls, beforeMixedPick + 2, 'valid boxes remain selectable alongside invalid templates and empty points'); templateData.readErrors = []; From 4bee3d5055274ff357d8bd8592ab38ba9109a652 Mon Sep 17 00:00:00 2001 From: AliceJump Date: Mon, 12 Oct 2026 15:30:29 +0800 Subject: [PATCH 29/29] fix(screenshot): unify Ctrl+Alt+S into Resources inbox after rebase on #42 Rebase ui/redesign-p0-p2 onto main (includes #42). Keep one capture flow: VS Code keybinding and Windows RegisterHotKey both land in the #41 Resources inbox (lossless BMP), not the old templates-only path. Keep #42 prewarm, GlobalScreenshotHotkey, and local review-time scripts. Update docs/keymap notes and background-screenshot tests accordingly. --- README.en.md | 4 +-- README.md | 2 +- docs/box-resources.en.md | 2 +- docs/box-resources.md | 2 +- docs/project-config.en.md | 6 ++-- docs/project-config.md | 13 ++++--- l10n/bundle.l10n.es.json | 4 ++- l10n/bundle.l10n.ja.json | 4 ++- l10n/bundle.l10n.json | 4 ++- l10n/bundle.l10n.ko.json | 4 ++- l10n/bundle.l10n.zh-cn.json | 4 ++- l10n/bundle.l10n.zh-tw.json | 4 ++- scripts/test_background_screenshot.js | 51 +++++++++++++++++++-------- src/backgroundScreenshot.ts | 40 ++++++++++++--------- src/entrypoint.ts | 9 +++-- src/extension.ts | 2 ++ src/run/resourcesView.ts | 12 ++++++- 17 files changed, 110 insertions(+), 57 deletions(-) diff --git a/README.en.md b/README.en.md index 6022c5f8..fab52f26 100644 --- a/README.en.md +++ b/README.en.md @@ -295,7 +295,7 @@ Command categories display as "ok-script 工具箱" / "ok-script Toolkit" depend |---|---|---| | `ok-script Toolkit: Find Template References…` | — | Find `fL.xxx` / `self.pos.xxx` / `"xxx"` usages in workspace Python files and jump; also in the Python editor context menu | | `ok-script Toolkit: Open with Annotation Editor` | — | Explorer image context menu (png/jpg/jpeg/webp only); without an argument it asks for an image | -| `ok-script Toolkit: Capture to Inbox` | `Ctrl+Alt+S` | +| `ok-script Toolkit: Capture to Inbox` | `Ctrl+Alt+S` (macOS `Cmd+Alt+S`; also the Windows global hotkey) | Capture into the Resources inbox (temp shots). Same destination as the system-wide RegisterHotKey path | | `ok-script Toolkit: Open Resource Preview` | `Ctrl+Alt+T` (macOS `Cmd+Alt+T`, requires Python editor focus) | Focus the Resource Preview view in the activity bar | | `ok-script Toolkit: Open Resource Preview in Editor` | — | Open a large grid view in the editor area | | `ok-script Toolkit: Open Console` | — | Focus the Console view in the activity bar | @@ -307,7 +307,7 @@ Command categories display as "ok-script 工具箱" / "ok-script Toolkit" depend | `ok-script Toolkit: Open Annotation Editor…` | — | Pick an image from Annotation Management (with annotation counts) and open the COCO annotation editor directly | | `ok-script Toolkit: Publish Resources…` | — | Open Annotation Management and go straight into publishing: choose Template / Rect / Point, then publish | | `ok-script Toolkit: Open Temp Screenshots` | — | Focus the temp screenshots view in the activity bar | -| `ok-script Toolkit: Screenshot to Annotation Management` | `Ctrl+Alt+S` (macOS `Cmd+Alt+S`) | Open Annotation Management and take a screenshot immediately. Reuses the panel's own screenshot action; the shot is registered into COCO. Rebindable in Keyboard Shortcuts | +| `ok-script Toolkit: Screenshot to Annotation Management` | — (`Ctrl+Alt+S` now goes to Capture to Inbox) | Open Annotation Management and take a screenshot immediately. Reuses the panel's own screenshot action; the shot is registered into COCO. Rebindable in Keyboard Shortcuts | | `ok-script Toolkit: Project Convention vs My Settings` | — | List the settings that take part in the precedence chain and show **which layer each effective value comes from** (my settings / project convention / built-in default). Rows you have overridden carry a Revert button that drops your override and goes back to the project convention. Exists to fix "once I changed it, I can never see what the team changed" | ## Update Not Taking Effect diff --git a/README.md b/README.md index 23b553eb..511af891 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ self.wait_click_ocr(match=self.lang.zip_line_mixin.k_2f4f4a2f, ...) | 命令 | 快捷键 | 说明 | |---|---|---| | `ok-script 工具箱: 打开资源预览` | `Ctrl+Alt+T`(macOS `Cmd+Alt+T`,需聚焦 Python 编辑器) | 聚焦「资源」视图 | -| `ok-script 工具箱: 截到收件箱` | `Ctrl+Alt+S`(macOS `Cmd+Alt+S`) | 截图进入资源收件箱(原「截图并打开标注管理」仍保留为另一条命令) | +| `ok-script 工具箱: 截到收件箱` | `Ctrl+Alt+S`(macOS `Cmd+Alt+S`;Windows 系统级热键同键) | 截图进入资源收件箱;VS Code 键位与 RegisterHotKey 全局热键共用此终点(原「截图并打开标注管理」仍保留为另一条命令) | | `ok-script 工具箱: 打开控制台` | — | 聚焦「运行」视图 | | `ok-script 工具箱: 控制台:运行任务…` | — | 从已加载的一次性任务里选一个入队(执行器未启动时会先启动) | | `ok-script 工具箱: 控制台:启动执行器 / 暂停执行器 / 继续执行器 / 停止当前任务 / 关闭执行器` | — | 与控制台按钮同一套逻辑;按执行器状态在命令面板和视图标题栏里显隐 | diff --git a/docs/box-resources.en.md b/docs/box-resources.en.md index a2485b59..da7adc79 100644 --- a/docs/box-resources.en.md +++ b/docs/box-resources.en.md @@ -55,7 +55,7 @@ Side and wide previews support Template / Rect / Point modes and name/expression Single click inserts; double click copies. Buttons expose the same actions and annotated source previews. Without a Python editor, the reference is copied with an explanation. Template previews never fill an absent runtime library with unpublished authoring data. Rect / Point hints follow current authoring data without requiring publication first. -Both hosts use `Ctrl+Alt+T` for resource previews and `Ctrl+Alt+S` for capture into annotation management; macOS uses the corresponding `Cmd+Alt` combinations. Publication belongs to annotation management, with no separate publishing chain in previews. +Both hosts use `Ctrl+Alt+T` for resource previews and `Ctrl+Alt+S` for capture into the Resources inbox (VS Code keybinding and the Windows global hotkey share this destination); macOS uses the corresponding `Cmd+Alt` combinations. Publication belongs to annotation management, with no separate publishing chain in previews. ## Unified Publication diff --git a/docs/box-resources.md b/docs/box-resources.md index 6a296b93..105ba850 100644 --- a/docs/box-resources.md +++ b/docs/box-resources.md @@ -55,7 +55,7 @@ Rect / Point 名称至少包含两个由句点分隔的 Python 标识符,例 单击插入,双击复制;按钮提供相同动作和带位置提示的原图查看。没有 Python 编辑器时复制引用并说明原因。Template 预览不会用尚未发布的工作标注填充运行时库;Rect / Point 提示跟随当前工作标注,编辑后不需要先发布才能得到代码提示。 -两端 `Ctrl+Alt+T` 打开资源预览,`Ctrl+Alt+S` 截图到标注管理;macOS 使用相应的 `Cmd+Alt` 组合。发布入口位于标注管理,资源预览不提供另一个发布链。 +两端 `Ctrl+Alt+T` 打开资源预览,`Ctrl+Alt+S` 截图到资源收件箱(VS Code 键位与 Windows 系统级热键同一终点);macOS 使用相应的 `Cmd+Alt` 组合。发布入口位于标注管理,资源预览不提供另一个发布链。 ## 统一发布 diff --git a/docs/project-config.en.md b/docs/project-config.en.md index 87b90008..8633c735 100644 --- a/docs/project-config.en.md +++ b/docs/project-config.en.md @@ -276,16 +276,16 @@ Committing these would impose one person's machine/preferences on colleagues. ### 6.4 Screenshot Shortcut (Not Project Configuration) -**Goal:** screenshot → annotation template management. Target **template asset management** (`openTemplateAssets` / `ShowTemplateAssetsAction`), whose own screenshot action saves and refreshes assets, rather than temporary screenshots (`showTempScreenshots` / `ShowTempShotsAction`). +**Goal:** one unified shortcut for screenshot → Resources inbox. Default destination is the **Resources inbox** (`captureToInbox` / temp screenshot store; annotate or promote to drafts from there), not the standalone "Screenshot to Annotation Management" panel path. `screenshotToTemplate` remains an explicit command but no longer owns the default binding. **IDE scope:** keybindings are personal. Project configuration would impose them on colleagues. VS Code and IntelliJ already have native keymap editors; do not invent another system. | Host | Approach | |---|---| -| VS Code | Add `okScriptToolkit.screenshotToTemplate` to `contributes.commands`, with a default in `contributes.keybindings` and no `when` restriction; users edit Keyboard Shortcuts | +| VS Code | `okScriptToolkit.captureToInbox` defaults to `Ctrl+Alt+S` and lands in the Resources inbox; `okScriptToolkit.screenshotToTemplate` remains available without a default binding. On Windows the RegisterHotKey global hotkey shares the inbox destination. Users can rebind in Keyboard Shortcuts | | JetBrains | Add `AnAction` and a default keybinding under `plugin.xml` actions; users edit Settings → Keymap | -Open the annotation template management panel and invoke its screenshot action. **Reuse existing `handleScreenshot`; do not add capture implementation.** +Behavior: reuse the same `captureGameWindow` implementation and write a lossless BMP into the inbox store. The system-wide hotkey intentionally avoids stealing focus from the game window. ## 7. Migration and Compatibility diff --git a/docs/project-config.md b/docs/project-config.md index 8f719031..4cd38429 100644 --- a/docs/project-config.md +++ b/docs/project-config.md @@ -349,21 +349,20 @@ import 得到(只是拿不到新标签),不会报错;而改类名是** ### 6.4 截图快捷键(新增,**不进项目配置**) -**要什么**:一个"截图 → 进标注模板管理"的快捷键。默认目标是**标注模板管理** -(`openTemplateAssets` / `ShowTemplateAssetsAction`,其面板自带截图动作: -截图落盘后刷新素材列表),而**不是**临时截图(`showTempScreenshots` / -`ShowTempShotsAction`)。 +**要什么**:一个「截图 → 进资源收件箱」的统一快捷键。默认目标是**资源收件箱** +(`captureToInbox` / 临时截图存储,随后可在资源视图里标注或送入草稿),而不是单独的 +「截图并打开标注管理」面板路径。`screenshotToTemplate` 仍保留为显式命令,但不再占用默认键位。 **为什么是 IDE 级**:键位是个人偏好。放进项目配置会强加给同事,而且 VS Code 与 IntelliJ 各有原生 keymap 编辑器,用户改键位本来就该走那里 —— 插件不该自造一套。 | 端 | 做法 | |---|---| -| VS Code | `contributes.commands` 新增 `okScriptToolkit.screenshotToTemplate`;`contributes.keybindings` 给它一个默认键位(不加 `when` 限制,任何文件都能按)。用户可在「键盘快捷方式」里改 | +| VS Code | `contributes.commands` 提供 `okScriptToolkit.captureToInbox`(默认 `Ctrl+Alt+S`,进资源收件箱)与 `okScriptToolkit.screenshotToTemplate`(无默认键位,仍可打开标注管理并截图);Windows 另有 RegisterHotKey 系统级同键,终点同为收件箱。用户可在「键盘快捷方式」里改 | | JetBrains | 新增 `AnAction`(放在 `plugin.xml` 的 `actions` 里给默认键位);用户可在「设置 → 按键映射」里改 | -行为:打开标注模板管理面板并触发它的截图动作。**不新增截图实现** —— 复用面板已有的 -`handleScreenshot`。 +行为:调用与面板相同的 `captureGameWindow` 实现,把无损 BMP 写入收件箱存储。 +系统级热键刻意不抢焦点,以免打断游戏窗口。 ## 7. 迁移与兼容 diff --git a/l10n/bundle.l10n.es.json b/l10n/bundle.l10n.es.json index 8aa60fe3..e35f1511 100644 --- a/l10n/bundle.l10n.es.json +++ b/l10n/bundle.l10n.es.json @@ -311,5 +311,7 @@ "Files to write": "Archivos a escribir", "Templates: {add} new · {keep} overwrite · {remove} removed": "Plantillas: {add} nuevas · {keep} sobrescritas · {remove} quitadas", "Publish preview": "Vista previa de publicación", - "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "Revisa lo que se escribirá. Enter en “Publicar” para seguir, Esc para cancelar." + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "Revisa lo que se escribirá. Enter en “Publicar” para seguir, Esc para cancelar.", + "Screenshot inbox is not ready.": "La bandeja de capturas aún no está lista.", + "Screenshot saved to inbox: {name}": "Captura guardada en la bandeja: {name}" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 7a873e89..8a3643e3 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -311,5 +311,7 @@ "Files to write": "書き込むファイル", "Templates: {add} new · {keep} overwrite · {remove} removed": "テンプレート:新規 {add} · 上書き {keep} · 削除 {remove}", "Publish preview": "公開プレビュー", - "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "書き込み内容を確認。「公開」で Enter、Esc で取消。" + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "書き込み内容を確認。「公開」で Enter、Esc で取消。", + "Screenshot inbox is not ready.": "スクリーンショット受信箱の準備ができていません。", + "Screenshot saved to inbox: {name}": "スクリーンショットを受信箱に保存しました: {name}" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index b84100d8..04ae8ba6 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -311,5 +311,7 @@ "Files to write": "Files to write", "Templates: {add} new · {keep} overwrite · {remove} removed": "Templates: {add} new · {keep} overwrite · {remove} removed", "Publish preview": "Publish preview", - "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "Review what will be written. Enter on “Publish” to continue, Esc to cancel." + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "Review what will be written. Enter on “Publish” to continue, Esc to cancel.", + "Screenshot inbox is not ready.": "Screenshot inbox is not ready.", + "Screenshot saved to inbox: {name}": "Screenshot saved to inbox: {name}" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index 200182bb..805c0250 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -311,5 +311,7 @@ "Files to write": "쓸 파일", "Templates: {add} new · {keep} overwrite · {remove} removed": "템플릿: 신규 {add} · 덮어쓰기 {keep} · 제거 {remove}", "Publish preview": "게시 미리보기", - "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "쓸 내용을 확인하세요. “게시”에서 Enter, Esc로 취소." + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "쓸 내용을 확인하세요. “게시”에서 Enter, Esc로 취소.", + "Screenshot inbox is not ready.": "스크린샷 받은편지함이 준비되지 않았습니다.", + "Screenshot saved to inbox: {name}": "스크린샷이 받은편지함에 저장됨: {name}" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index a43ef9af..f5cf4ad1 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -311,5 +311,7 @@ "Files to write": "将写入的文件", "Templates: {add} new · {keep} overwrite · {remove} removed": "模板:新增 {add} · 覆盖 {keep} · 移除 {remove}", "Publish preview": "发布预览", - "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "确认将写入的内容。在「发布」上按 Enter 继续,Esc 取消。" + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "确认将写入的内容。在「发布」上按 Enter 继续,Esc 取消。", + "Screenshot inbox is not ready.": "截图收件箱尚未就绪。", + "Screenshot saved to inbox: {name}": "截图已保存到收件箱: {name}" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 91235989..489bc6ef 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -311,5 +311,7 @@ "Files to write": "將寫入的檔案", "Templates: {add} new · {keep} overwrite · {remove} removed": "模板:新增 {add} · 覆寫 {keep} · 移除 {remove}", "Publish preview": "發佈預覽", - "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "確認將寫入的內容。在「發佈」上按 Enter 繼續,Esc 取消。" + "Review what will be written. Enter on “Publish” to continue, Esc to cancel.": "確認將寫入的內容。在「發佈」上按 Enter 繼續,Esc 取消。", + "Screenshot inbox is not ready.": "截圖收件匣尚未就緒。", + "Screenshot saved to inbox: {name}": "截圖已儲存到收件匣: {name}" } diff --git a/scripts/test_background_screenshot.js b/scripts/test_background_screenshot.js index 362747f1..caa8f09d 100644 --- a/scripts/test_background_screenshot.js +++ b/scripts/test_background_screenshot.js @@ -7,17 +7,30 @@ const { makeTmpDir } = require('./test-tmp'); const root = makeTmpDir('background-screenshot'); const outputs = []; const errors = []; +const infos = []; let behavior = async output => { fs.writeFileSync(output, 'frame'); return { ok: true }; }; + +const registered = []; +const inboxStore = { + newFilePath(extension = 'png') { + const name = `shot_${registered.length}.${extension}`; + return path.join(root, name); + }, + register(filePath) { + registered.push(filePath); + }, +}; + const moduleStub = { exports: {} }; const vscode = { workspace: { workspaceFolders: [{ uri: { fsPath: root } }] }, window: { - showWarningMessage() {}, + showWarningMessage(message) { errors.push(message); }, showErrorMessage(message) { errors.push(message); }, - showInformationMessage() {}, + showInformationMessage(message) { infos.push(message); }, }, }; const source = fs.readFileSync(path.join(__dirname, '../out/backgroundScreenshot.js'), 'utf8'); @@ -25,42 +38,52 @@ vm.runInNewContext(source, { exports: moduleStub.exports, require(name) { if (name === 'vscode') return vscode; - if (name === './localization') return { tr: (key, args) => `${key} ${args?.error || ''}` }; - if (name === './projectConfig') return { resolveProjectDir: () => root, templatesDirectory: () => 'templates' }; + if (name === './localization') return { + tr: (key, args) => `${key} ${args?.error || args?.name || ''}`.trim(), + }; if (name === './screenshotCapture') return { captureGameWindow(output) { outputs.push(output); return behavior(output); }, }; return require(name); }, - Date: class extends Date { constructor() { super('2026-10-12T12:00:00Z'); } }, }, { filename: 'backgroundScreenshot.js' }); -const capture = moduleStub.exports.captureTemplateScreenshotInBackground; + +const { bindInboxScreenshotStore, captureInboxScreenshotInBackground, captureTemplateScreenshotInBackground } = moduleStub.exports; (async () => { - assert.strictEqual(await capture(), true); - assert.strictEqual(await capture(), true); + assert.strictEqual(await captureInboxScreenshotInBackground(), false, 'Unbound store must fail closed'); + assert(errors.some(message => message.includes('Screenshot inbox is not ready'))); + + bindInboxScreenshotStore(inboxStore); + errors.length = 0; + + assert.strictEqual(await captureInboxScreenshotInBackground(), true); + assert.strictEqual(await captureTemplateScreenshotInBackground(), true, 'Deprecated alias must keep working'); assert.notStrictEqual(outputs[0], outputs[1], 'Captures within the same second must keep both images'); + assert.ok(outputs[0].endsWith('.bmp'), 'Inbox capture must request lossless BMP'); assert.strictEqual(fs.readFileSync(outputs[0], 'utf8'), 'frame'); assert.strictEqual(fs.readFileSync(outputs[1], 'utf8'), 'frame'); + assert.deepStrictEqual(registered, outputs.slice(0, 2)); + assert(infos.some(message => message.includes('Screenshot saved to inbox'))); let finish; behavior = () => new Promise(resolve => { finish = resolve; }); - const pending = capture(); + const pending = captureInboxScreenshotInBackground(); const requestCount = outputs.length; - assert.strictEqual(await capture(), false, 'Repeated hotkeys must not queue overlapping captures'); + assert.strictEqual(await captureInboxScreenshotInBackground(), false, 'Repeated hotkeys must not queue overlapping captures'); assert.strictEqual(outputs.length, requestCount); finish({ ok: false, reason: 'cancelled' }); assert.strictEqual(await pending, false); behavior = async () => { throw new Error('capture unavailable'); }; - assert.strictEqual(await capture(), false); + assert.strictEqual(await captureInboxScreenshotInBackground(), false); assert(errors.some(message => message.includes('capture unavailable'))); behavior = async () => ({ ok: true }); - assert.strictEqual(await capture(), true, 'A failed capture must release the busy guard'); + assert.strictEqual(await captureInboxScreenshotInBackground(), true, 'A failed capture must release the busy guard'); vscode.workspace.workspaceFolders = []; - assert.strictEqual(await capture(), false); + assert.strictEqual(await captureInboxScreenshotInBackground(), false); vscode.workspace.workspaceFolders = [{ uri: { fsPath: root } }]; - assert.strictEqual(await capture(), true, 'Missing workspace must not keep the guard locked'); + assert.strictEqual(await captureInboxScreenshotInBackground(), true, 'Missing workspace must not keep the guard locked'); console.log('test_background_screenshot: OK'); })().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/src/backgroundScreenshot.ts b/src/backgroundScreenshot.ts index 79e26196..1f76e9ad 100644 --- a/src/backgroundScreenshot.ts +++ b/src/backgroundScreenshot.ts @@ -1,21 +1,25 @@ -import * as fs from 'fs'; import * as path from 'path'; -import { randomUUID } from 'crypto'; import * as vscode from 'vscode'; import { tr } from './localization'; -import { resolveProjectDir, templatesDirectory } from './projectConfig'; import { captureGameWindow } from './screenshotCapture'; +import type { TempScreenshotStore } from './tempScreenshotStore'; let captureInProgress = false; +let inboxStore: TempScreenshotStore | undefined; + +/** Bind the Resources inbox store so the system-wide hotkey can land shots there. */ +export function bindInboxScreenshotStore(store: TempScreenshotStore): void { + inboxStore = store; +} /** - * Capture directly into the template authoring directory without revealing an IDE panel. + * Capture into the Resources inbox without revealing an IDE panel. * * The system-wide hotkey uses this path so pressing Ctrl+Alt+S while the game is focused - * does not bring VS Code to the foreground before capture. The actual window capture still - * goes through the same captureGameWindow implementation used by the normal asset panel. + * does not bring VS Code to the foreground before capture. Destination is the same temp + * screenshot store that `captureToInbox` uses (lossless BMP), not the old templates-only path. */ -export async function captureTemplateScreenshotInBackground(): Promise { +export async function captureInboxScreenshotInBackground(): Promise { if (captureInProgress) return false; captureInProgress = true; try { @@ -28,21 +32,24 @@ export async function captureTemplateScreenshotInBackground(): Promise } } +/** @deprecated Use {@link captureInboxScreenshotInBackground}; kept as an alias for older callers/tests. */ +export async function captureTemplateScreenshotInBackground(): Promise { + return captureInboxScreenshotInBackground(); +} + async function captureScreenshot(): Promise { + if (!inboxStore) { + void vscode.window.showWarningMessage(tr('Screenshot inbox is not ready.')); + return false; + } + const folder = vscode.workspace.workspaceFolders?.[0]; if (!folder) { void vscode.window.showWarningMessage(tr('No workspace folder open.')); return false; } - const projectRoot = resolveProjectDir() || folder.uri.fsPath; - const outputDir = path.join(projectRoot, templatesDirectory(projectRoot)); - fs.mkdirSync(outputDir, { recursive: true }); - - const now = new Date(); - const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; - const outputPath = path.join(outputDir, `screenshot_${ts}_${randomUUID()}.png`); - + const outputPath = inboxStore.newFilePath('bmp'); const outcome = await captureGameWindow(outputPath); if (!outcome.ok) { if (outcome.reason === 'noScript') { @@ -53,6 +60,7 @@ async function captureScreenshot(): Promise { return false; } - void vscode.window.showInformationMessage(tr('Screenshot saved: {name}', { name: path.basename(outputPath) })); + inboxStore.register(outputPath); + void vscode.window.showInformationMessage(tr('Screenshot saved to inbox: {name}', { name: path.basename(outputPath) })); return true; } diff --git a/src/entrypoint.ts b/src/entrypoint.ts index c7c2fd0a..23a0fd15 100644 --- a/src/entrypoint.ts +++ b/src/entrypoint.ts @@ -1,10 +1,9 @@ import * as vscode from 'vscode'; -import { captureTemplateScreenshotInBackground } from './backgroundScreenshot'; +import { captureInboxScreenshotInBackground } from './backgroundScreenshot'; import { activate as activateCore, deactivate as deactivateCore } from './extension'; import { GlobalScreenshotHotkey } from './globalScreenshotHotkey'; import { isSupportedProject, resolveProjectDir } from './projectConfig'; import { getProjectConfig } from './screenshotCapture'; -import { repaintAllAssetGalleries } from './templateAssetPanel'; const WALKTHROUGH_ID = 'okScriptToolkit.getStarted'; const PROJECT_READY_CONTEXT = 'okScriptToolkit.projectReady'; @@ -12,13 +11,13 @@ const PROJECT_READY_CONTEXT = 'okScriptToolkit.projectReady'; export function activate(context: vscode.ExtensionContext): void { activateCore(context); + // System-wide Ctrl+Alt+S → same Resources inbox as the VS Code keybinding. + // Capture runs in the background so the game window keeps focus. const globalScreenshotHotkey = new GlobalScreenshotHotkey( context.extensionPath, () => getProjectConfig().pythonPath, () => { - void captureTemplateScreenshotInBackground().then((captured) => { - if (captured) repaintAllAssetGalleries(); - }); + void captureInboxScreenshotInBackground(); }, ); diff --git a/src/extension.ts b/src/extension.ts index 9f883820..b3efc821 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as vscode from 'vscode'; +import { bindInboxScreenshotStore } from './backgroundScreenshot'; import { setCaptureLogger, disposeCaptureWorker } from './screenshotCapture'; import { LangData, poDirectorySetting } from './langData'; import { tr } from './localization'; @@ -304,6 +305,7 @@ export function activate(context: vscode.ExtensionContext): void { path.join(context.globalStorageUri.fsPath, 'temp-screenshots', wsHash), ); tempScreenshotStore.ensure(); + bindInboxScreenshotStore(tempScreenshotStore); const runTree = new RunTreeDataProvider(okConsole); const paramsView = new ParamsViewProvider(context.extensionUri, okConsole, runTree); const resourcesView = new ResourcesViewProvider( diff --git a/src/run/resourcesView.ts b/src/run/resourcesView.ts index 4fe838fe..bca88a5d 100644 --- a/src/run/resourcesView.ts +++ b/src/run/resourcesView.ts @@ -45,6 +45,16 @@ export class ResourcesViewProvider implements vscode.WebviewViewProvider, vscode ].filter((u) => u.fsPath), }; view.webview.html = this.buildHtml(view.webview); + void import('../screenshotCapture').then(({ prewarmScreenshotCapture }) => { + void prewarmScreenshotCapture(); + }); + view.onDidChangeVisibility(() => { + if (view.visible) { + void import('../screenshotCapture').then(({ prewarmScreenshotCapture }) => { + void prewarmScreenshotCapture(); + }); + } + }); view.webview.onDidReceiveMessage(async (msg) => { switch (msg?.type) { case 'ready': @@ -90,7 +100,7 @@ export class ResourcesViewProvider implements vscode.WebviewViewProvider, vscode async captureToInbox(): Promise { const { captureGameWindow } = await import('../screenshotCapture'); - const out = this.tempStore.newFilePath(); + const out = this.tempStore.newFilePath('bmp'); const result = await captureGameWindow(out); if (!result.ok) { void vscode.window.showErrorMessage(result.error || tr('Screenshot failed'));