From 1e6c5b1bc9a919997d7f9dd8467e143fc0d5f271 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:37:36 +0800 Subject: [PATCH 1/8] fix: show annotated source context for box previews --- jetbrains | 2 +- media/boxPanel/app.js | 21 ++++++++-- media/boxPanel/style.css | 17 ++++++++ scripts/test_box_resource.js | 18 ++++++++- scripts/test_thumb_content_hash.js | 25 ++++++++++++ src/boxPanels.ts | 17 +++++--- src/pngCrop.ts | 64 +++++++++++++++++++++--------- src/providers.ts | 4 +- 8 files changed, 136 insertions(+), 32 deletions(-) diff --git a/jetbrains b/jetbrains index bc08ca4..4798e3b 160000 --- a/jetbrains +++ b/jetbrains @@ -1 +1 @@ -Subproject commit bc08ca47aede1ad533514fae105fa0f9d7e81133 +Subproject commit 4798e3b27e9ca4bd25a545615bcd424bc66c50be diff --git a/media/boxPanel/app.js b/media/boxPanel/app.js index 2030e49..ae6471d 100644 --- a/media/boxPanel/app.js +++ b/media/boxPanel/app.js @@ -25,7 +25,7 @@ window.addEventListener('message', (event) => { const msg = event.data; const rows = document.getElementById('rows'); - // 框管理对标模板管理:每个 box path 一张 bbox 裁剪缩略图,分批到达逐张填充 + // 原图上下文裁剪与红框标记,分批到达逐张填充。 if (msg.type === 'thumbs') { (msg.items || []).forEach((item) => { const card = cards.get(item.id); @@ -38,9 +38,11 @@ while (rows.firstChild) rows.removeChild(rows.firstChild); rows.className = 'asset-grid'; (msg.rows || []).forEach((row) => { + const card = document.createElement('div'); + card.className = 'card asset-card'; const button = document.createElement('button'); button.type = 'button'; - button.className = 'card asset-card'; + button.className = 'activate-btn'; button.title = row.id; const box = document.createElement('div'); box.className = 'thumb-box'; @@ -52,7 +54,18 @@ count.className = 'asset-count'; count.textContent = row.label; button.append(box, name, count); - cards.set(row.id, button); + card.append(button); + if (row.imagePath && row.bbox) { + const open = document.createElement('button'); + open.type = 'button'; + open.className = 'open-btn'; + open.textContent = '👁'; + open.title = t('viewOriginal'); + open.setAttribute('aria-label', t('viewOriginal')); + open.onclick = () => vscode.postMessage({ type: 'open', id: row.id }); + card.append(open); + } + cards.set(row.id, card); let clickTimer = 0; button.onclick = () => { window.clearTimeout(clickTimer); @@ -62,7 +75,7 @@ window.clearTimeout(clickTimer); vscode.postMessage({ type: 'activate', id: row.id, clicks: 2 }); }; - rows.append(button); + rows.append(card); }); if (!(msg.rows || []).length) { const empty = document.createElement('div'); diff --git a/media/boxPanel/style.css b/media/boxPanel/style.css index 3888a1b..134d039 100644 --- a/media/boxPanel/style.css +++ b/media/boxPanel/style.css @@ -14,7 +14,24 @@ cursor: pointer; text-align: center; color: var(--text-primary); + position: relative; } +.activate-btn { display: block; width: 100%; padding: 0; border: none; border-radius: 0; background: transparent; color: inherit; cursor: pointer; } +.open-btn { + position: absolute; + top: 3px; + right: 3px; + width: 22px; + height: 22px; + min-height: 22px; + padding: 0; + border: var(--border-width) solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-control); + color: var(--text-primary); + line-height: 1; +} +.open-btn:hover, .open-btn:focus-visible { background: var(--bg-control-hover); border-color: var(--border-strong); } .thumb-box { height: 96px; display: flex; diff --git a/scripts/test_box_resource.js b/scripts/test_box_resource.js index ec6ebc7..76fa17b 100644 --- a/scripts/test_box_resource.js +++ b/scripts/test_box_resource.js @@ -59,6 +59,8 @@ const editorMessages = []; const galleryMessages = []; const errors = []; const infos = []; +const openedFiles = []; +const previewSources = []; const ui = { l10n: { t: text => text }, workspace: { @@ -72,6 +74,7 @@ const ui = { showWarningMessage: async (_text, _options, action) => action, }, Uri: { file: file => ({ fsPath: file }) }, + commands: { executeCommand: async (command, uri) => openedFiles.push({ command, uri }) }, }; const originalLoad = Module._load; Module._load = function(name, parent, isMain) { @@ -84,7 +87,11 @@ function loadController(file, name) { m.filename = filename; m.paths = Module._nodeModulePaths(path.dirname(filename)); m.require = request => { - if (request === './pngCrop') return { ...require('../out/imageHeader'), THUMB_HEIGHT: 84, cropTemplateThumbFileAsync: async () => undefined }; + if (request === './pngCrop') return { + ...require('../out/imageHeader'), THUMB_HEIGHT: 84, cropTemplateThumbFileAsync: async () => undefined, + annotatedImageFile: (imagePath, bbox) => { previewSources.push({ imagePath, bbox }); return path.join(folder, 'preview.png'); }, + }; + if (file === 'boxPanels' && request === './screenshotCapture') return { getProjectConfig: () => ({ projectDir: project }) }; if (request === './localization') return { tr: text => text }; return Module.prototype.require.call(m, request); }; @@ -127,6 +134,15 @@ const tick = () => new Promise(resolve => setTimeout(resolve, 30)); assert.strictEqual(raw.categories[0].name, 'screen.first'); assert(!('version' in raw) && !('boxes' in raw)); assert.strictEqual(fs.readFileSync(templates.annotationFile, 'utf8'), templateText, 'editing boxes preserves the independent template source'); + const BoxGallery = loadController('boxPanels', 'BoxGalleryViewProvider'); + const boxGallery = new BoxGallery({ fsPath: root }, folder); + boxGallery.view = { webview: {} }; + await boxGallery.onMessage({ type: 'open', id: 'screen.first' }); + assert.deepStrictEqual(previewSources.pop(), { imagePath: image, bbox: [10, 20, 30, 40] }, 'View Original uses box authoring coordinates even when template names differ'); + assert.strictEqual(openedFiles.at(-1).command, 'vscode.open'); + const openedCount = openedFiles.length; + await boxGallery.onMessage({ type: 'open', id: 'screen.runtime_only' }); + assert.strictEqual(openedFiles.length, openedCount, 'boxes without an authoring source cannot open a made-up image'); const loadsBeforeWatch = editorMessages.filter(message => message.type === 'load').length; core.notifyAnnotationDataChanged(boxesFile); await tick(); diff --git a/scripts/test_thumb_content_hash.js b/scripts/test_thumb_content_hash.js index 3f19211..017f1d5 100644 --- a/scripts/test_thumb_content_hash.js +++ b/scripts/test_thumb_content_hash.js @@ -179,6 +179,31 @@ async function main() { '注入空值时退回默认目录名(不能让来源判定变成"永远匹配不上")', ); + // Box previews share the source crop used by View Original, including context and the red marker. + const source = path.join(tmpRoot, 'box-source.png'); + const bbox = [250, 250, 20, 40]; + fs.writeFileSync(source, solidPng(800, GREEN)); + const previewUrl = pngCrop.annotatedImageToDataUrlCached(source, bbox); + assert.ok(previewUrl); + const preview = pngCrop.decodeRgba(Buffer.from(previewUrl.split(',')[1], 'base64')); + assert.deepStrictEqual([preview.width, preview.height], [382, 400], 'adds 200px of context on each side, then fits within 400px'); + const pixel = (x, y) => [...preview.rgba.subarray((y * preview.width + x) * 4, (y * preview.width + x) * 4 + 3)]; + assert.deepStrictEqual(pixel(10, 10), GREEN, 'surrounding source pixels remain visible'); + assert.deepStrictEqual(pixel(182, 182), [255, 40, 40], 'box position is marked red'); + const annotatedFile = pngCrop.annotatedImageFile(source, bbox, thumbDir); + assert.deepStrictEqual(fs.readFileSync(annotatedFile), Buffer.from(previewUrl.split(',')[1], 'base64'), 'viewing and hover use the same rendered PNG'); + assert.strictEqual(pngCrop.annotatedImageFile(source, bbox, thumbDir), annotatedFile); + const edgeUrl = pngCrop.annotatedImageToDataUrlCached(source, [0, 0, 20, 40]); + const edge = pngCrop.decodeRgba(Buffer.from(edgeUrl.split(',')[1], 'base64')); + assert.deepStrictEqual([edge.width, edge.height], [220, 240], 'context clips to the source image edge'); + for (const invalid of [[0, 0, 0, 2], [800, 0, 2, 2], [NaN, 0, 2, 2]]) { + assert.strictEqual(pngCrop.annotatedImageToDataUrlCached(source, invalid), undefined); + } + fs.writeFileSync(source, solidPng(800, BLUE)); + const changedTime = new Date(Date.now() + 10000); + fs.utimesSync(source, changedTime, changedTime); + assert.notStrictEqual(pngCrop.annotatedImageFile(source, bbox, thumbDir), annotatedFile, 'replaced source invalidates annotated image'); + fs.rmSync(tmpRoot, { recursive: true, force: true }); fs.rmSync(stubDir, { recursive: true, force: true }); console.log('test_thumb_content_hash: all assertions passed'); diff --git a/src/boxPanels.ts b/src/boxPanels.ts index 7e5851c..c44898e 100644 --- a/src/boxPanels.ts +++ b/src/boxPanels.ts @@ -11,7 +11,7 @@ import { AUTHORING_FILE_NAME, PixelBox } from './boxResourcePure'; import { probedBoxesJson } from './cocoFeaturePath'; import { injectWebviewLocalization, tr } from './localization'; import { boxesRuntimeSetting, templatesDirectory } from './projectConfig'; -import { cropTemplateThumbFileAsync, readImageSize, THUMB_HEIGHT } from './pngCrop'; +import { annotatedImageFile, readImageSize } from './pngCrop'; import { getProjectConfig } from './screenshotCapture'; import { TemplateAssetData } from './templateAssetData'; import { onAnnotationDataChanged } from './cocoAnnotationData'; @@ -81,6 +81,13 @@ export class BoxGalleryViewProvider implements vscode.WebviewViewProvider { await this.refresh(root); return; } + if (msg.type === 'open' && msg.id) { + const source = previewRectForPath(root, msg.id); + const file = source && annotatedImageFile(source.imagePath, source.bbox, this.thumbDir); + if (file) await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(file)); + else void vscode.window.showWarningMessage(tr('Failed to generate annotated image: source image could not be decoded or was missing')); + return; + } if (msg.type !== 'activate' || !msg.id) return; const text = msg.clicks === 2 ? `self.pos.${msg.id}` : `self.pos.${msg.id}.to_box()`; const editor = vscode.window.activeTextEditor; @@ -94,10 +101,10 @@ export class BoxGalleryViewProvider implements vscode.WebviewViewProvider { } /** - * 框管理对标模板管理:每个 box path 一张**bbox 裁剪后的资源缩略图**。 + * 每个 box path 展示原图中 bbox 外扩后的区域,并画红框标明位置。 * 来源是 authoring 的 Pixel bbox + 原图;运行时独有的 path 拿不到来源图, * 保持无图占位(不做 normalized → Pixel 的死转换)。裁剪、内容 hash 缓存、 - * 失败处理全部复用 `cropTemplateThumbFileAsync`,不另造预览系统。 + * 缓存、裁剪和标记与查看原图 / Hover 共用。 */ private async refresh(root: string): Promise { const view = this.view; @@ -123,7 +130,7 @@ export class BoxGalleryViewProvider implements vscode.WebviewViewProvider { const batch = rows.slice(i, i + 8).filter((row) => row.imagePath && row.bbox); const thumbs: Array<{ id: string; url: string }> = []; for (const row of batch) { - const file = await cropTemplateThumbFileAsync(row.imagePath, row.bbox!, this.thumbDir, THUMB_HEIGHT); + const file = annotatedImageFile(row.imagePath, row.bbox!, this.thumbDir); if (gen !== this.generation || !view) return; if (!file) continue; thumbs.push({ id: row.id, url: view.webview.asWebviewUri(vscode.Uri.file(file)).toString(true) }); @@ -141,7 +148,7 @@ export function openBoxEditor(extensionUri: vscode.Uri, data: TemplateAssetData, } /** - * 框路径的 Hover 预览:直接用 authoring 的 Pixel bbox 裁剪原图。 + * 框路径的预览来源:authoring 的原图与 Pixel bbox。 * 运行时独有的 path 没有来源图,不做 normalized → Pixel 的死转换。 */ export function previewRectForPath(root: string, boxPath: string): { imagePath: string; bbox: [number, number, number, number] } | undefined { diff --git a/src/pngCrop.ts b/src/pngCrop.ts index 393107f..dfc813d 100644 --- a/src/pngCrop.ts +++ b/src/pngCrop.ts @@ -985,26 +985,34 @@ function drawRectOutline( strokeRectInward(rgba, imgW, imgH, x, y, w, h, thickness, 255, 40, 40); } -export function writeAnnotatedImage( - imagePath: string, bbox: [number, number, number, number], outPath: string, +/** Shared source crop for box thumbnails, hover and source viewing. */ +export function annotatedImageToDataUrlCached( + imagePath: string, bbox: [number, number, number, number], ): string | undefined { + if (!bbox.every(Number.isFinite) || bbox[2] <= 0 || bbox[3] <= 0) return undefined; + const contentHash = imageContentHash(imagePath); + if (!contentHash) return undefined; + const key = `annotated-v3|${contentHash}|${bbox.join(',')}`; + const hit = CROP_CACHE.get(key); + if (hit) return hit.url; try { const buf = fs.readFileSync(imagePath); const { width, height, rgba } = decodeRgba(buf); - const bx = Math.max(0, Math.min(bbox[0], width - 1)); - const by = Math.max(0, Math.min(bbox[1], height - 1)); - const bw = Math.max(1, Math.min(bbox[2], width - Math.max(0, bbox[0]))); - const bh = Math.max(1, Math.min(bbox[3], height - Math.max(0, bbox[1]))); + const bx = Math.max(0, Math.round(bbox[0])); + const by = Math.max(0, Math.round(bbox[1])); + const bw = Math.min(width, Math.round(bbox[0] + bbox[2])) - bx; + const bh = Math.min(height, Math.round(bbox[1] + bbox[3])) - by; + if (bw <= 0 || bh <= 0) return undefined; const pad = 200; const cropX = Math.max(0, bx - pad), cropY = Math.max(0, by - pad); - const cropW = Math.min(width - cropX, bw + 2 * pad + Math.min(pad, bx)); - const cropH = Math.min(height - cropY, bh + 2 * pad + Math.min(pad, by)); + const cropW = Math.min(width, bx + bw + pad) - cropX; + const cropH = Math.min(height, by + bh + pad) - cropY; // 归一化:缩放到目标分辨率内,保证不同原图输出视觉效果一致 const TARGET = 400; const scale = Math.min(1, TARGET / Math.max(cropW, cropH)); - const outW = Math.round(cropW * scale); - const outH = Math.round(cropH * scale); + const outW = Math.max(1, Math.round(cropW * scale)); + const outH = Math.max(1, Math.round(cropH * scale)); // 缩放裁剪区域(最近邻) const outRgba = Buffer.alloc(outW * outH * 4); @@ -1025,13 +1033,37 @@ export function writeAnnotatedImage( const thickness = Math.max(2, Math.round(2 * scale)); drawRectOutline(outRgba, outW, outH, Math.round((bx - cropX) * scale), Math.round((by - cropY) * scale), - Math.round(bw * scale), Math.round(bh * scale), thickness); + Math.max(1, Math.round(bw * scale)), Math.max(1, Math.round(bh * scale)), thickness); + const url = `data:image/png;base64,${encodePng(outW, outH, outRgba).toString('base64')}`; + cacheSet(key, { url, imagePath, source: thumbSourceSubdir(imagePath) }); + return url; + } catch { return undefined; } +} + +export function writeAnnotatedImage( + imagePath: string, bbox: [number, number, number, number], outPath: string, +): string | undefined { + const url = annotatedImageToDataUrlCached(imagePath, bbox); + if (!url) return undefined; + try { fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, encodePng(outW, outH, outRgba)); + fs.writeFileSync(outPath, Buffer.from(url.split(',')[1], 'base64')); return outPath; } catch { return undefined; } } +/** Uses the supplied authoring source directly, without template-name lookup. */ +export function annotatedImageFile( + imagePath: string, bbox: [number, number, number, number], thumbDir: string, +): string | undefined { + const contentHash = imageContentHash(imagePath); + if (!contentHash) return undefined; + const key = crypto.createHash('sha1').update(`v3|${contentHash}|${bbox.join(',')}`).digest('hex').slice(0, 16); + const out = path.join(thumbDir, 'annotated', `a2_${key}.png`); + try { if (fs.existsSync(out) && fs.statSync(out).size > 0) return out; } catch { /* regenerate */ } + return writeAnnotatedImage(imagePath, bbox, out); +} + export function openAnnotatedImage( imagePath: string, name: string, bbox: [number, number, number, number], thumbDir: string, rootDir: string, @@ -1042,11 +1074,5 @@ export function openAnnotatedImage( const src = entry.imagePath; const srcBbox = entry.bbox; try { if (!fs.existsSync(src)) return undefined; } catch { return undefined; } - // a2 = 内容 hash 版本:原图被替换后标注图同步重绘,不会停留在旧图 - const contentHash = imageContentHash(src); - if (!contentHash) return undefined; - const key = crypto.createHash('sha1').update(`v2|${contentHash}|${srcBbox.join(',')}`).digest('hex').slice(0, 16); - const out = path.join(thumbDir, 'annotated', `a2_${key}.png`); - try { if (fs.existsSync(out) && fs.statSync(out).size > 0) return out; } catch { /* 重新生成 */ } - return writeAnnotatedImage(src, srcBbox, out); + return annotatedImageFile(src, srcBbox, thumbDir); } diff --git a/src/providers.ts b/src/providers.ts index f18b72c..a810730 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -10,7 +10,7 @@ import { import { FeatureData, FeatureTemplate } from './featureData'; import { EffectData, EffectEntry } from './effectData'; import { DEFAULT_FEATURE_ALIASES, ideSetting, labelEnumAliases, loadProjectConfig } from './projectConfig'; -import { cropTemplateToDataUrlCached } from './pngCrop'; +import { annotatedImageToDataUrlCached, cropTemplateToDataUrlCached } from './pngCrop'; import { selectedProjectLocale, tr } from './localization'; import { getProjectConfig } from './screenshotCapture'; import { posPaths, previewRectForPath } from './boxPanels'; @@ -426,7 +426,7 @@ export class LangHoverProvider implements vscode.HoverProvider { const md = new vscode.MarkdownString(undefined, true); md.appendCodeblock(`self.pos.${pos.path}.to_box()`, 'python'); if (preview) { - const img = cropTemplateToDataUrlCached(preview.imagePath, preview.bbox); + const img = annotatedImageToDataUrlCached(preview.imagePath, preview.bbox); if (img) md.appendMarkdown(`\n![box](${img})\n`); } return new vscode.Hover(md); From ccbcc5803f467228ab35ef19c7c0d63887861ea3 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:42:47 +0800 Subject: [PATCH 2/8] chore: update JetBrains thumbnail source preview button --- jetbrains | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetbrains b/jetbrains index 4798e3b..681f858 160000 --- a/jetbrains +++ b/jetbrains @@ -1 +1 @@ -Subproject commit 4798e3b27e9ca4bd25a545615bcd424bc66c50be +Subproject commit 681f858fb0e87a993a9c90a18cc47a5ec3276999 From 055458212d2f7ac09d597475863c309002379ec9 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:50:53 +0800 Subject: [PATCH 3/8] chore: update JetBrains one-click thumbnail actions --- jetbrains | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetbrains b/jetbrains index 681f858..ae164f6 160000 --- a/jetbrains +++ b/jetbrains @@ -1 +1 @@ -Subproject commit 681f858fb0e87a993a9c90a18cc47a5ec3276999 +Subproject commit ae164f657503516f6f7910734154c5c0e2129bf6 From efc4ff9f3fc15d4236c7c58131ddb555b1917457 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:11:01 +0800 Subject: [PATCH 4/8] fix: align VS Code and JetBrains thumbnail interactions --- jetbrains | 2 +- media/boxPanel/app.js | 50 ++++++++---------- media/boxPanel/index.html | 3 +- media/boxPanel/style.css | 17 +----- media/shared/controls.css | 25 +++++++++ media/shared/thumbnailActions.js | 29 +++++++++++ media/tempScreenshots/app.js | 31 ++++------- media/tempScreenshots/index.html | 3 +- media/tempScreenshots/style.css | 26 +--------- media/templateAssetPanel/app.js | 26 +++------- media/templateAssetPanel/index.html | 3 +- media/templateAssetPanel/style.css | 28 ---------- media/templatePanel/app.js | 38 +++++--------- media/templatePanel/index.html | 3 +- media/templatePanel/style.css | 23 --------- package.json | 3 +- scripts/test_asset_swap_picker.js | 19 +++---- scripts/test_box_resource.js | 19 +++++++ scripts/test_temp_screenshots.js | 1 + scripts/test_thumbnail_actions.js | 80 +++++++++++++++++++++++++++++ src/boxPanels.ts | 13 +++-- src/localization.ts | 53 +++++++++++++++---- src/pythonEditor.ts | 34 ++++++++++++ src/templateAssetPanel.ts | 6 +++ src/templatePanel.ts | 32 +----------- src/webviewHtml.ts | 3 +- 26 files changed, 322 insertions(+), 248 deletions(-) create mode 100644 media/shared/thumbnailActions.js create mode 100644 scripts/test_thumbnail_actions.js create mode 100644 src/pythonEditor.ts diff --git a/jetbrains b/jetbrains index ae164f6..4df0cee 160000 --- a/jetbrains +++ b/jetbrains @@ -1 +1 @@ -Subproject commit ae164f657503516f6f7910734154c5c0e2129bf6 +Subproject commit 4df0cee9ef460d026e4a303a4f2aa552a97d6045 diff --git a/media/boxPanel/app.js b/media/boxPanel/app.js index ae6471d..0b636aa 100644 --- a/media/boxPanel/app.js +++ b/media/boxPanel/app.js @@ -18,8 +18,8 @@ const img = document.createElement('img'); img.alt = ''; img.addEventListener('error', () => img.remove()); - box.textContent = ''; - box.append(img); + box.querySelector('.placeholder')?.remove(); + box.prepend(img); img.src = url; } window.addEventListener('message', (event) => { @@ -40,41 +40,35 @@ (msg.rows || []).forEach((row) => { const card = document.createElement('div'); card.className = 'card asset-card'; - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'activate-btn'; - button.title = row.id; + card.title = row.id; const box = document.createElement('div'); box.className = 'thumb-box'; - box.textContent = '…'; + const placeholder = document.createElement('span'); + placeholder.className = 'placeholder'; + placeholder.textContent = '…'; + box.append(placeholder); const name = document.createElement('div'); name.className = 'asset-name'; name.textContent = row.id; const count = document.createElement('div'); count.className = 'asset-count'; count.textContent = row.label; - button.append(box, name, count); - card.append(button); - if (row.imagePath && row.bbox) { - const open = document.createElement('button'); - open.type = 'button'; - open.className = 'open-btn'; - open.textContent = '👁'; - open.title = t('viewOriginal'); - open.setAttribute('aria-label', t('viewOriginal')); - open.onclick = () => vscode.postMessage({ type: 'open', id: row.id }); - card.append(open); - } + const actions = document.createElement('div'); + actions.className = 'actions thumbnail-actions'; + actions.append( + ThumbnailActions.button('+', t('insertExpression'), () => vscode.postMessage({ type: 'activate', id: row.id, clicks: 1 })), + ThumbnailActions.button('⧉', t('copyExpression'), () => vscode.postMessage({ type: 'activate', id: row.id, clicks: 2 })), + ); + const open = ThumbnailActions.button('👁', t('viewOriginal'), () => vscode.postMessage({ type: 'open', id: row.id })); + open.disabled = !(row.imagePath && row.bbox); + actions.append(open); + box.append(actions); + card.append(box, name, count); cards.set(row.id, card); - let clickTimer = 0; - button.onclick = () => { - window.clearTimeout(clickTimer); - clickTimer = window.setTimeout(() => vscode.postMessage({ type: 'activate', id: row.id, clicks: 1 }), 250); - }; - button.ondblclick = () => { - window.clearTimeout(clickTimer); - vscode.postMessage({ type: 'activate', id: row.id, clicks: 2 }); - }; + ThumbnailActions.bindClicks(card, + () => vscode.postMessage({ type: 'activate', id: row.id, clicks: 1 }), + () => vscode.postMessage({ type: 'activate', id: row.id, clicks: 2 }), + ); rows.append(card); }); if (!(msg.rows || []).length) { diff --git a/media/boxPanel/index.html b/media/boxPanel/index.html index 8e4b1ee..3a4a78a 100644 --- a/media/boxPanel/index.html +++ b/media/boxPanel/index.html @@ -16,6 +16,7 @@
- + + diff --git a/media/boxPanel/style.css b/media/boxPanel/style.css index 134d039..1487c6e 100644 --- a/media/boxPanel/style.css +++ b/media/boxPanel/style.css @@ -16,23 +16,8 @@ color: var(--text-primary); position: relative; } -.activate-btn { display: block; width: 100%; padding: 0; border: none; border-radius: 0; background: transparent; color: inherit; cursor: pointer; } -.open-btn { - position: absolute; - top: 3px; - right: 3px; - width: 22px; - height: 22px; - min-height: 22px; - padding: 0; - border: var(--border-width) solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-control); - color: var(--text-primary); - line-height: 1; -} -.open-btn:hover, .open-btn:focus-visible { background: var(--bg-control-hover); border-color: var(--border-strong); } .thumb-box { + position: relative; height: 96px; display: flex; align-items: center; diff --git a/media/shared/controls.css b/media/shared/controls.css index 391243e..59a242c 100644 --- a/media/shared/controls.css +++ b/media/shared/controls.css @@ -206,6 +206,31 @@ input[type='radio'] { accent-color: var(--border-strong); } background: var(--bg-control); } +.thumbnail-actions { + position: absolute; + bottom: 3px; + right: 3px; + display: flex; + gap: 2px; + z-index: 2; +} +.thumbnail-actions button { + width: 24px; + height: 24px; + min-height: 24px; + padding: 0; + border: var(--border-width) solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-control); + color: var(--text-primary); + font-size: var(--font-sm); + line-height: 1; +} +.thumbnail-actions button:hover, .thumbnail-actions button:focus-visible { + background: var(--bg-control-hover); + border-color: var(--border-strong); +} + /* 行级可点击区(组头 / 折叠标题行 / tab):极浅色调,不参与按钮权重 */ .seg__btn:not(.is-active), .gbar__row--link, diff --git a/media/shared/thumbnailActions.js b/media/shared/thumbnailActions.js new file mode 100644 index 0000000..2097e81 --- /dev/null +++ b/media/shared/thumbnailActions.js @@ -0,0 +1,29 @@ +(function () { + window.ThumbnailActions = { + button(icon, label, onClick) { + const button = document.createElement('button'); + button.type = 'button'; + button.textContent = icon; + button.title = label; + button.setAttribute('aria-label', label); + button.addEventListener('click', (event) => { + event.stopPropagation(); + window.clearTimeout(button.closest('.card')?._clickTimer); + onClick(); + }); + button.addEventListener('dblclick', (event) => event.stopPropagation()); + return button; + }, + bindClicks(card, onSingle, onDouble) { + card.addEventListener('click', (event) => { + window.clearTimeout(card._clickTimer); + if (event.detail >= 2) return; + card._clickTimer = window.setTimeout(() => { if (card.isConnected) onSingle(); }, 500); + }); + card.addEventListener('dblclick', () => { + window.clearTimeout(card._clickTimer); + onDouble(); + }); + }, + }; +})(); diff --git a/media/tempScreenshots/app.js b/media/tempScreenshots/app.js index 9f833fa..529fd49 100644 --- a/media/tempScreenshots/app.js +++ b/media/tempScreenshots/app.js @@ -283,32 +283,19 @@ const img = document.createElement('img'); img.src = meta.thumbUrl || meta.url; img.alt = meta.name; - card.appendChild(img); + const preview = document.createElement('div'); + preview.className = 'thumb-box'; + preview.appendChild(img); + card.appendChild(preview); const actions = document.createElement('div'); - actions.className = 'actions'; - - const sendBtn = document.createElement('button'); + actions.className = 'actions thumbnail-actions'; + const sendBtn = ThumbnailActions.button('→', t('tempSendToAssets'), () => vscode.postMessage({ type: 'sendToAssets', id: meta.id })); sendBtn.className = 'send'; - sendBtn.textContent = '→'; - sendBtn.title = t('tempSendToAssets'); - sendBtn.addEventListener('click', (e) => { - e.stopPropagation(); - vscode.postMessage({ type: 'sendToAssets', id: meta.id }); - }); - - const delBtn = document.createElement('button'); + const delBtn = ThumbnailActions.button('×', t('tempDelete'), () => vscode.postMessage({ type: 'deleteTemp', id: meta.id })); delBtn.className = 'del'; - delBtn.textContent = '×'; - delBtn.title = t('tempDelete'); - delBtn.addEventListener('click', (e) => { - e.stopPropagation(); - vscode.postMessage({ type: 'deleteTemp', id: meta.id }); - }); - - actions.appendChild(sendBtn); - actions.appendChild(delBtn); - card.appendChild(actions); + actions.append(sendBtn, delBtn); + preview.appendChild(actions); const name = document.createElement('div'); name.className = 'name'; diff --git a/media/tempScreenshots/index.html b/media/tempScreenshots/index.html index 5c4a30a..41658f1 100644 --- a/media/tempScreenshots/index.html +++ b/media/tempScreenshots/index.html @@ -56,6 +56,7 @@ - + + diff --git a/media/tempScreenshots/style.css b/media/tempScreenshots/style.css index 3f87cd7..9d2e40f 100644 --- a/media/tempScreenshots/style.css +++ b/media/tempScreenshots/style.css @@ -177,31 +177,7 @@ body { overflow: hidden; text-overflow: ellipsis; } -/* 操作按钮常驻可见:跨 webview 拖拽在 VS Code 里不可用, - 「→ 添加到标注管理」是导入的主入口,不能只在 hover 时才露出 */ -.card .actions { - position: absolute; - top: 2px; - right: 2px; - display: flex; - gap: 2px; - opacity: .85; - transition: opacity .12s ease; -} -.card:hover .actions { opacity: 1; } -.card .actions button { - width: 18px; - height: 18px; - min-height: 18px; - line-height: 1; - padding: 0; - font-size: var(--font-sm); - border: var(--border-width) solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-control); - color: var(--text-primary); -} -.card .actions button:hover { background: var(--bg-control-hover); border-color: var(--border-strong); } +.thumb-box { position: relative; } .card .actions button.send { border-color: color-mix(in srgb, var(--run) 55%, var(--border)); background: color-mix(in srgb, var(--run) 22%, var(--bg-control)); diff --git a/media/templateAssetPanel/app.js b/media/templateAssetPanel/app.js index c62cd0d..d04995b 100644 --- a/media/templateAssetPanel/app.js +++ b/media/templateAssetPanel/app.js @@ -51,24 +51,14 @@ box.appendChild(ph); const actDiv = document.createElement('div'); - actDiv.className = 'actions'; - // 交换标注:与「删除」并列,但排在左边 —— 删除是破坏性的,放最右侧不误触 - const swapBtn = document.createElement('button'); - swapBtn.textContent = '⇄'; - swapBtn.title = t('assetSwapTooltip'); - swapBtn.addEventListener('click', (e) => { - e.stopPropagation(); - openSwapPicker(meta); - }); - actDiv.appendChild(swapBtn); - const delBtn = document.createElement('button'); - delBtn.textContent = 'X'; - delBtn.title = t('assetDeleteTooltip'); - delBtn.addEventListener('click', (e) => { - e.stopPropagation(); - vscode.postMessage({ type: 'deleteImage', imagePath: meta.imagePath }); - }); - actDiv.appendChild(delBtn); + actDiv.className = 'actions thumbnail-actions'; + actDiv.append( + ThumbnailActions.button('✎', t('editAnnotations'), () => vscode.postMessage({ type: 'openAnnotation', imagePath: meta.imagePath })), + 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 })), + ); + ['edit', 'open', 'swap', 'delete'].forEach((action, index) => { actDiv.children[index].dataset.action = action; }); box.appendChild(actDiv); const m = document.createElement('div'); diff --git a/media/templateAssetPanel/index.html b/media/templateAssetPanel/index.html index 6c7dc98..0431675 100644 --- a/media/templateAssetPanel/index.html +++ b/media/templateAssetPanel/index.html @@ -35,6 +35,7 @@

- + + diff --git a/media/templateAssetPanel/style.css b/media/templateAssetPanel/style.css index c38e1ba..ae83494 100644 --- a/media/templateAssetPanel/style.css +++ b/media/templateAssetPanel/style.css @@ -69,34 +69,6 @@ body { .cats { color: var(--text-muted); font-size: var(--font-2xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .size { color: var(--text-muted); font-size: var(--font-2xs); } -/* tile 上的操作按钮组:常驻可见(默认 85%),底色/文字走 token, - 不再用 rgba(0,0,0,.6)+#fff(浅色主题下是黑块) */ -.actions { - position: absolute; - bottom: 3px; - right: 3px; - display: flex; - gap: 2px; - opacity: .85; - transition: opacity .12s ease; - z-index: 2; -} -.card:hover .actions { opacity: 1; } -.actions button { - width: 22px; - height: 22px; - min-height: 22px; - padding: 0; - border: var(--border-width) solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-control); - color: var(--text-primary); - font-size: var(--font-sm); - line-height: 1; - text-align: center; -} -.actions button:hover { background: var(--bg-control-hover); border-color: var(--border-strong); } - .empty { margin-top: var(--space-xl); text-align: center; diff --git a/media/templatePanel/app.js b/media/templatePanel/app.js index ea79e90..c446a10 100644 --- a/media/templatePanel/app.js +++ b/media/templatePanel/app.js @@ -44,21 +44,16 @@ ph.textContent = '…'; box.appendChild(ph); - // 缩略图右上角"查看原图"按钮(悬停显示,不干扰卡片点击) - const openBtn = document.createElement('button'); - openBtn.className = 'open-btn'; - openBtn.textContent = '👁'; - openBtn.title = t('viewOriginal'); - openBtn.addEventListener('click', (e) => { - e.stopPropagation(); // 不触发卡片的 click - vscode.postMessage({ - type: 'open', - imagePath: meta.imagePath, - name: meta.name, - bbox: JSON.stringify(meta.bbox), - }); - }); - box.appendChild(openBtn); + const actions = document.createElement('div'); + actions.className = 'actions thumbnail-actions'; + actions.append( + ThumbnailActions.button('+', t('insertExpression'), () => vscode.postMessage({ type: 'insert', text: meta.name })), + ThumbnailActions.button('⧉', t('copyExpression'), () => vscode.postMessage({ type: 'copy', text: meta.name })), + ThumbnailActions.button('👁', t('viewOriginal'), () => vscode.postMessage({ + type: 'open', imagePath: meta.imagePath, name: meta.name, bbox: JSON.stringify(meta.bbox), + })), + ); + box.appendChild(actions); const m = document.createElement('div'); m.className = 'meta'; @@ -75,15 +70,10 @@ card.appendChild(box); card.appendChild(m); - // 单击卡片 → 插入代码;双击卡片 → 复制 - // 使用 e.detail 区分:第二次点击(detail=2)时跳过,由 dblclick 处理 - card.addEventListener('click', (e) => { - if (e.detail >= 2) return; // 双击序列中的第二次点击,跳过(由 dblclick 处理) - vscode.postMessage({ type: 'insert', text: meta.name }); - }); - card.addEventListener('dblclick', () => { - vscode.postMessage({ type: 'copy', text: meta.name }); - }); + ThumbnailActions.bindClicks(card, + () => vscode.postMessage({ type: 'insert', text: meta.name }), + () => vscode.postMessage({ type: 'copy', text: meta.name }), + ); return card; } diff --git a/media/templatePanel/index.html b/media/templatePanel/index.html index d87e20e..0823f66 100644 --- a/media/templatePanel/index.html +++ b/media/templatePanel/index.html @@ -16,6 +16,7 @@
- + + diff --git a/media/templatePanel/style.css b/media/templatePanel/style.css index d128b71..2e236c8 100644 --- a/media/templatePanel/style.css +++ b/media/templatePanel/style.css @@ -46,29 +46,6 @@ body { } .thumb-box img { max-width: 100%; max-height: 100%; image-rendering: pixelated; } -/* 打开按钮:常驻可见(默认约 85% 不透明度)——可点击控件不能只在 hover 时现身; - 底色用控件面、文字用前景色,避免原先 rgba(0,0,0,.55)+#fff 在浅色主题成为黑块 */ -.open-btn { - position: absolute; - top: 3px; - right: 3px; - width: 22px; - height: 22px; - min-height: 22px; - padding: 0; - border: var(--border-width) solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-control); - color: var(--text-primary); - font-size: var(--font-md); - line-height: 1; - opacity: .85; - transition: opacity .12s ease, background-color .12s ease; - z-index: 2; -} -.open-btn:hover, -.open-btn:focus-visible { opacity: 1; background: var(--bg-control-hover); border-color: var(--border-strong); } - .placeholder { color: var(--text-muted); font-size: var(--font-xs); } .meta { padding: 5px 7px 6px; } diff --git a/package.json b/package.json index e026858..89a8d7e 100644 --- a/package.json +++ b/package.json @@ -347,9 +347,10 @@ "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 compile && npm run test:webview-html && npm run test:design-system && 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-swap && 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: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 compile && 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-swap && 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: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", "test:task-launcher": "node scripts/test_task_launcher_subconfigs.js", "test:console-accountseg": "node scripts/test_console_accountseg.js", "test:task-launcher-executor": "node scripts/test_task_launcher_executor.js", diff --git a/scripts/test_asset_swap_picker.js b/scripts/test_asset_swap_picker.js index 13b8791..d7dd949 100644 --- a/scripts/test_asset_swap_picker.js +++ b/scripts/test_asset_swap_picker.js @@ -56,6 +56,7 @@ html = html .replaceAll('__I18N_JSON__', JSON.stringify(dictionary)) .replaceAll('__SHARED_TOKENS_URI__', '') .replaceAll('__SHARED_CONTROLS_URI__', '') + .replace('', ``) .replace('', '') .replace('', ``); @@ -116,11 +117,11 @@ const TEMPLATES = [ const cardOf = (name) => document.querySelector(`.card[data-name="${name}"]`); check(document.querySelectorAll('.card').length === 3, '三张图各一张卡片'); check( - !!cardOf('1.png') && cardOf('1.png').querySelectorAll('.actions button').length === 2, - '卡片操作区有 2 个按钮(⇄ 交换 + X 删除)', + !!cardOf('1.png') && cardOf('1.png').querySelectorAll('.actions button').length === 4, + '卡片操作区有 4 个按钮(编辑、打开、交换、删除)', ); - const swapBtn = cardOf('1.png').querySelectorAll('.actions button')[0]; - check(swapBtn.textContent === '⇄', `第一个按钮是交换(实际 "${swapBtn.textContent}")`); + const swapBtn = cardOf('1.png').querySelector('button[data-action="swap"]'); + check(swapBtn.textContent === '⇄', `交换按钮(实际 "${swapBtn.textContent}")`); check(swapBtn.title === dictionary.assetSwapTooltip, '交换按钮的 tooltip 走文案字典'); /* ── 2. 打开选择器:排除自己、显示尺寸与标注数 ───────────────── */ @@ -190,7 +191,7 @@ const TEMPLATES = [ data: { type: 'templates', templates: [TEMPLATES[0]] }, })); await new Promise((resolve) => solo.window.setTimeout(resolve, 0)); - solo.window.document.querySelector('.card .actions button') + solo.window.document.querySelector('.card .actions button[data-action="swap"]') .dispatchEvent(new solo.window.MouseEvent('click', { bubbles: true })); await new Promise((resolve) => solo.window.setTimeout(resolve, 0)); check( @@ -215,7 +216,7 @@ const TEMPLATES = [ const flush2 = () => new Promise((resolve) => w2.setTimeout(resolve, 0)); const since = (type) => sent.slice(mark).filter((m) => m && m.type === type); const openPicker = async (name) => { - doc2.querySelector(`.card[data-name="${name}"] .actions button`) + doc2.querySelector(`.card[data-name="${name}"] .actions button[data-action="swap"]`) .dispatchEvent(new w2.MouseEvent('click', { bubbles: true })); await flush2(); }; @@ -293,7 +294,7 @@ const TEMPLATES = [ data: { type: 'templates', templates: TEMPLATES }, })); await new Promise((resolve) => broken.window.setTimeout(resolve, 0)); - broken.window.document.querySelector('.card[data-name="1.png"] .actions button') + broken.window.document.querySelector('.card[data-name="1.png"] .actions button[data-action="swap"]') .dispatchEvent(new broken.window.MouseEvent('click', { bubbles: true })); await new Promise((resolve) => broken.window.setTimeout(resolve, 0)); const brokenNames = [...broken.window.document.querySelectorAll('#swapList .swap-item')] @@ -315,7 +316,7 @@ const TEMPLATES = [ await new Promise((resolve) => w3.setTimeout(resolve, 0)); w3.dispatchEvent(new w3.MessageEvent('message', { data: { type: 'templates', templates: TEMPLATES } })); await new Promise((resolve) => w3.setTimeout(resolve, 0)); - w3.document.querySelector('.card[data-name="1.png"] .actions button') + w3.document.querySelector('.card[data-name="1.png"] .actions button[data-action="swap"]') .dispatchEvent(new w3.MouseEvent('click', { bubbles: true })); await new Promise((resolve) => w3.setTimeout(resolve, 0)); w3.dispatchEvent(new w3.MessageEvent('message', { @@ -340,7 +341,7 @@ const TEMPLATES = [ await new Promise((resolve) => w4.setTimeout(resolve, 0)); w4.dispatchEvent(new w4.MessageEvent('message', { data: { type: 'templates', templates: TEMPLATES } })); await new Promise((resolve) => w4.setTimeout(resolve, 0)); - w4.document.querySelector('.card[data-name="1.png"] .actions button') + w4.document.querySelector('.card[data-name="1.png"] .actions button[data-action="swap"]') .dispatchEvent(new w4.MouseEvent('click', { bubbles: true })); await new Promise((resolve) => w4.setTimeout(resolve, 0)); check( diff --git a/scripts/test_box_resource.js b/scripts/test_box_resource.js index 76fa17b..203c3f8 100644 --- a/scripts/test_box_resource.js +++ b/scripts/test_box_resource.js @@ -68,12 +68,15 @@ const ui = { getConfiguration: () => ({ get: (_key, fallback) => fallback, inspect: () => undefined }), onDidChangeConfiguration: () => ({ dispose() {} }), }, + env: { clipboard: { writeText: async text => { ui.copiedText = text; } } }, window: { + onDidChangeActiveTextEditor: () => ({ dispose() {} }), showErrorMessage: text => errors.push(text), showInformationMessage: text => infos.push(text), showWarningMessage: async (_text, _options, action) => action, }, Uri: { file: file => ({ fsPath: file }) }, + SnippetString: class { constructor(value) { this.value = value; } }, commands: { executeCommand: async (command, uri) => openedFiles.push({ command, uri }) }, }; const originalLoad = Module._load; @@ -140,6 +143,17 @@ const tick = () => new Promise(resolve => setTimeout(resolve, 30)); await boxGallery.onMessage({ type: 'open', id: 'screen.first' }); assert.deepStrictEqual(previewSources.pop(), { imagePath: image, bbox: [10, 20, 30, 40] }, 'View Original uses box authoring coordinates even when template names differ'); assert.strictEqual(openedFiles.at(-1).command, 'vscode.open'); + let inserts = 0; + ui.window.activeTextEditor = { document: { languageId: 'python' }, insertSnippet: async snippet => { inserts++; ui.insertedText = snippet.value; } }; + await boxGallery.onMessage({ type: 'activate', id: 'screen.first', clicks: 2 }); + assert.strictEqual(ui.copiedText, 'self.pos.screen.first'); + assert.strictEqual(inserts, 0, 'double-click copies the path without editing Python'); + await boxGallery.onMessage({ type: 'activate', id: 'screen.first', clicks: 1 }); + assert.strictEqual(inserts, 1); + assert.strictEqual(ui.insertedText, 'self.pos.screen.first.to_box()'); + ui.window.activeTextEditor = undefined; + await boxGallery.onMessage({ type: 'activate', id: 'screen.first', clicks: 1 }); + assert.strictEqual(inserts, 2, 'the card still inserts into the recent Python editor after taking focus'); const openedCount = openedFiles.length; await boxGallery.onMessage({ type: 'open', id: 'screen.runtime_only' }); assert.strictEqual(openedFiles.length, openedCount, 'boxes without an authoring source cannot open a made-up image'); @@ -154,6 +168,11 @@ const tick = () => new Promise(resolve => setTimeout(resolve, 30)); disposables.push(gallery); // No thumbnail URI conversion is needed because this fixture skips thumbnail generation. await gallery.update(); + await gallery.onMessage({ type: 'openSource', imagePath: image }); + assert.strictEqual(openedFiles.at(-1).uri.fsPath, image); + const validSourceCount = openedFiles.length; + await gallery.onMessage({ type: 'openSource', imagePath: path.join(project, 'outside.png') }); + assert.strictEqual(openedFiles.length, validSourceCount, 'source buttons only open images belonging to the gallery'); const templateEditor = new Controller({ onDidReceiveMessage: () => ({ dispose() {} }), postMessage: message => { editorMessages.push(message); return Promise.resolve(true); }, }, { fsPath: root }, templates, folder, () => true, () => {}, false); diff --git a/scripts/test_temp_screenshots.js b/scripts/test_temp_screenshots.js index 18218d0..5463466 100644 --- a/scripts/test_temp_screenshots.js +++ b/scripts/test_temp_screenshots.js @@ -42,6 +42,7 @@ html = html .replaceAll('__CSP_SOURCE__', "'self'") .replaceAll('__I18N_JSON__', JSON.stringify(dictionary)) .replace('', '') + .replace('', ``) .replace('', ``); let webviewState = {}; diff --git a/scripts/test_thumbnail_actions.js b/scripts/test_thumbnail_actions.js new file mode 100644 index 0000000..8aa1cb2 --- /dev/null +++ b/scripts/test_thumbnail_actions.js @@ -0,0 +1,80 @@ +/** Run both gallery scripts and asset cards to verify their shared interaction contract. */ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +let jsdom; +try { jsdom = require('jsdom'); } catch { + const jsdomRoot = process.env.OK_LANG_HINTS_JSDOM_ROOT || path.join(process.env.TEMP, 'ok-script-toolkit-jsdom'); + jsdom = require(path.join(jsdomRoot, 'node_modules', 'jsdom')); +} +const { JSDOM } = jsdom; +const root = path.resolve(__dirname, '..'); +const shared = fs.readFileSync(path.join(root, 'media/shared/thumbnailActions.js'), 'utf8'); + +function gallery(panel, message) { + const html = fs.readFileSync(path.join(root, 'media', panel, 'index.html'), 'utf8') + .replaceAll('__I18N_JSON__', '{}').replaceAll('__MODE__', 'gallery'); + const dom = new JSDOM(html, { runScripts: 'outside-only' }); + const w = dom.window; + const sent = []; + const timers = new Map(); + let nextTimer = 0; + 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(shared); + w.eval(fs.readFileSync(path.join(root, 'media', panel, 'app.js'), 'utf8')); + w.dispatchEvent(new w.MessageEvent('message', { data: message })); + sent.length = 0; + return { dom, w, sent, flush: () => { for (const [id, fn] of [...timers]) { timers.delete(id); fn(); } } }; +} + +for (const panel of ['templatePanel', 'boxPanel']) { + const message = panel === 'templatePanel' + ? { type: 'templates', templates: [{ name: 'button', width: 20, height: 30, bbox: [1, 2, 20, 30], imagePath: 'source.png' }] } + : { type: 'rows', rows: [{ id: 'screen.button', label: 'self.pos.screen.button.to_box()', bbox: [1, 2, 20, 30], imagePath: 'source.png' }] }; + const { dom, w, sent, flush } = gallery(panel, message); + const card = w.document.querySelector('.card'); + const buttons = [...card.querySelectorAll('.thumbnail-actions button')]; + assert.strictEqual(buttons.length, 3, `${panel} 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(panel === 'templatePanel' ? sent[0].type : sent[0].clicks, panel === 'templatePanel' ? 'insert' : 1); + sent.length = 0; + click(1); click(2); + card.dispatchEvent(new w.MouseEvent('dblclick', { bubbles: true })); + flush(); + assert.strictEqual(sent.length, 1, 'double-click never inserts before copying'); + assert.strictEqual(panel === 'templatePanel' ? sent[0].type : sent[0].clicks, panel === 'templatePanel' ? 'copy' : 2); + sent.length = 0; + click(1); 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[0].type, 'open'); + if (panel === 'boxPanel') { + w.dispatchEvent(new w.MessageEvent('message', { data: { type: 'thumbs', items: [{ id: 'screen.button', url: 'preview.png' }] } })); + assert.strictEqual(card.querySelectorAll('.thumbnail-actions button').length, 3, 'loading a box image preserves actions'); + w.dispatchEvent(new w.MessageEvent('message', { data: { type: 'rows', rows: [{ id: 'screen.runtime', label: 'to_box()' }] } })); + const open = w.document.querySelectorAll('.thumbnail-actions button')[2]; + assert(open.disabled); + sent.length = 0; open.click(); assert.strictEqual(sent.length, 0); + } + dom.window.close(); +} + +const { dom, w, sent } = gallery('templateAssetPanel', { type: 'templates', templates: [{ + name: 'source.png', imagePath: 'source.png', width: 20, height: 30, categories: [], annotations: 0, +}] }); +const card = w.document.querySelector('.card'); +assert.strictEqual(card.querySelectorAll('.thumbnail-actions button').length, 4); +card.querySelector('[data-action="edit"]').click(); +assert.deepStrictEqual(sent.pop(), { type: 'openAnnotation', imagePath: 'source.png' }); +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'); +dom.window.close(); +console.log('thumbnail action parity: buttons, single/double clicks, isolation and source-less state OK'); diff --git a/src/boxPanels.ts b/src/boxPanels.ts index c44898e..7a48e91 100644 --- a/src/boxPanels.ts +++ b/src/boxPanels.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; +import { ensureEditorTracker, insertIntoPythonEditor } from './pythonEditor'; import { AnnotationPanel } from './annotationPanel'; import { boxesForImage, @@ -59,6 +60,7 @@ export class BoxGalleryViewProvider implements vscode.WebviewViewProvider { ) {} resolveWebviewView(webviewView: vscode.WebviewView): void { + ensureEditorTracker(); this.view = webviewView; webviewView.webview.options = { enableScripts: true, @@ -89,15 +91,12 @@ export class BoxGalleryViewProvider implements vscode.WebviewViewProvider { return; } if (msg.type !== 'activate' || !msg.id) return; - const text = msg.clicks === 2 ? `self.pos.${msg.id}` : `self.pos.${msg.id}.to_box()`; - const editor = vscode.window.activeTextEditor; - if (!editor || editor.document.languageId !== 'python') { - await vscode.env.clipboard.writeText(text); + if (msg.clicks === 2) { + await vscode.env.clipboard.writeText(`self.pos.${msg.id}`); return; } - await editor.edit((builder) => { - editor.selections.forEach((selection) => builder.insert(selection.active, text)); - }); + const text = `self.pos.${msg.id}.to_box()`; + await insertIntoPythonEditor(text); } /** diff --git a/src/localization.ts b/src/localization.ts index 13806c6..abf7069 100644 --- a/src/localization.ts +++ b/src/localization.ts @@ -288,7 +288,7 @@ const ZH_CN: WebviewStrings = { overriddenTasksLabel: '有覆盖的任务', templatesSearch: '搜索模板名…', templatesTitle: '模板面板', - templatesHint: '单击=插入到代码 · 双击=复制 · 悬停缩略图点👁=查看原图', + templatesHint: '单击=插入 · 双击=复制 · 右下角按钮可直接操作', templatesCount: '{shown}/{total} 个模板', noTemplates: '未找到任何模板。', noTemplatesWithHint: '未找到任何模板。\n请确认工作区存在 assets/coco_annotations.json\n(或 ok_tasks/assets/coco_annotations.json)。', @@ -301,7 +301,7 @@ const ZH_CN: WebviewStrings = { templateSource: '来源: {path}', templateAssetsTitle: '模板素材管理', boxAssetsTitle: '框资源管理', - templateAssetsHint: '单击/双击=打开标注编辑器', + templateAssetsHint: '单击或点击编辑按钮打开标注编辑器', noTemplateAssets: '暂无模板素材。\n点击 Import 导入图片。', noTemplateAssetsHint: '暂无模板素材。', screenshotSaved: '截图已保存: {name}', @@ -401,10 +401,14 @@ const ZH_CN: WebviewStrings = { boxPathExists: '这个路径已经存在,换一个名字', generateBoxFailed: '生成框失败', publish: '发布', - assetsHint: '双击原图编辑框。发布后才会进入运行时。', + assetsHint: '单击原图或编辑按钮编辑框。发布后才会进入运行时。', galleryHint: '单击插入 to_box()。双击复制属性路径。', assetsEmpty: '没有模板原图。请先在标注管理里添加。', galleryEmpty: '还没有运行时框。请先在框资源管理里发布。', + insertExpression: "插入表达式", + copyExpression: "复制表达式", + editAnnotations: "编辑标注", + openSourceImage: "打开原图", }; const EN: WebviewStrings = { @@ -432,8 +436,8 @@ const EN: WebviewStrings = { charactersTitle: 'Character & Skill Manager', charactersTab: 'Characters & Skills', effectsTab: 'Effect Index', localesTab: 'Name Localization', issuesTab: 'Diagnostics', searchCharacters: 'Search characters, skills, effects, descriptions…', allStars: 'All Rarities', allElements: 'All Elements', allProfessions: 'All Professions', allSkillTypes: 'All Skill Types', enhancementOnly: 'Only characters with enhancements', issueOnly: 'Only characters with diagnostics', characterCount: '{shown} / {total} characters', skillsEnhancements: 'Skills / Enhancements', noCharacters: 'No matching characters', selectCharacter: 'Select a character from the left', unknownStar: 'Rarity unknown', unknownElement: 'Element unknown', unknownProfession: 'Profession unknown', unknownWeapon: 'Weapon unknown', skillsCount: '{count} skills', enhancementsCount: '{count} enhancements', openCharacterJson: 'Open Character JSON', skillsAndEnhancements: 'Skills & Enhancements', addSkill: 'Add Skill', modifySkill: 'Modify Skill', addEnhancement: 'Add Enhancement', modifyEnhancement: 'Modify Enhancement', syncedSkillLocked: 'Synced skill · ID/name/type/element/description locked', enhancedState: 'Enhanced', baseEffects: 'Base Effects', triggerEffects: 'Trigger Dependencies', outputEffects: 'Enhancement Effects', visiblePulse: 'Visible Pulse', multiplier: 'Multiplier', stagger: 'Stagger', cooldown: 'Cooldown', spiritCost: 'SP Cost', noSkills: 'No skill data for this character', searchEffects: 'Search effect ID, description, character, or skill…', allCategories: 'All Categories', allEffects: 'All Effects', usedOnly: 'Used Only', unusedOnly: 'Unused Only', unknownOnly: 'Unknown Only', addEffectCategory: 'Add Category', addEffect: 'Add Effect', effectsCount: '{shown} / {total} effects', noEffects: 'No matching effects', undefinedEffect: 'Undefined Effect', openDefinition: 'Open Definition', moreUsages: '{count} more usages', localizationMatrix: 'Character Name Localization Matrix', localizationHint: 'Blank cells are missing. Click a character to open the language source.', characterIdColumn: 'Character / ID', missing: 'Missing', searchIssues: 'Search diagnostics…', allSeverities: 'All Severities', issuesCount: '{shown} / {total} diagnostics', noIssuesMatch: 'No matching diagnostics', noIssues: 'No data issues found', openSource: 'Open Source', readingCharacterData: 'Reading characters, skills, effects, and localization data…', skillId: 'Skill ID', skillName: 'Skill Name', skillType: 'Skill Type', element: 'Element', damageMultiplier: 'Damage Multiplier', staggerValue: 'Stagger', cooldownField: 'Cooldown', spiritCostField: 'SP Cost', skillDescription: 'Skill Description', baseEffectsMulti: 'Base Effects (multi-select)', enhancementName: 'Enhancement Name', visibleMarker: 'Display Marker', triggerText: 'Trigger Condition', enhancementDescription: 'Enhancement Description', triggerEffectsMulti: 'Trigger Dependencies (multi-select)', outputEffectsMulti: 'Enhancement Effects (multi-select)', effectCategory: 'Effect Category', effectId: 'Effect ID (UPPER_SNAKE_CASE)', effectDescription: 'Effect Description', categoryName: 'Category Name', effectMultiHint: 'Hold Ctrl / Cmd to multi-select. Options come from effects.py and are grouped by category.', selectedTriggerEffects: '{count} trigger dependencies selected', noTriggerEffects: 'No trigger dependencies selected', noSelectedEffects: 'No effects selected', unknownEffect: 'Unknown effect', unknownCurrentEffect: 'Unknown effect in current data', inferredFromTriggerText: 'Inferred from trigger text', clickCopySkillId: 'Click to copy skill ID', clickCopyEffectId: 'Click to copy effect ID', openCharacterLocaleFile: 'Open character name language file', loadFailed: 'Failed to load', saveFailed: 'Failed to save', charactersMetric: 'Characters', skillsMetric: 'Skills', enhancementsMetric: 'Enhancements', effectReferencesMetric: 'Effect References', effectDefinitionsMetric: 'Effect Definitions', valueLabel: 'Value', confirmDelete: 'Delete “{name}”?\nA .bak backup is created before saving.', copied: 'Copied: {text}', - taskTitle: 'ok-script Task Launcher', noTasks: 'No tasks found.\nConfigure the project path first.', parameters: 'Parameters', collapseParameters: 'Collapse Parameters', launch: 'Launch', stop: 'Stop', pause: 'Pause', resume: 'Resume', running: 'Running…', oneTimeTask: 'One-time', triggerTask: 'Trigger', enableTrigger: 'Enable', triggerDisabled: 'Not enabled', triggerArmed: 'Armed', triggerEnqueued: 'Enqueued', triggerPolling: 'Polling', taskQueued: 'Waiting', taskRunning: 'Running', executorIdle: 'Executor stopped', executorConnecting: 'Starting executor…', executorRunning: 'Executor running · {count} trigger task(s) enqueued', executorPaused: 'Paused (polling and tasks are suspended)', startExecutor: 'Start executor', stopExecutor: 'Close executor', stopCurrent: 'Stop current task', configGroup: 'Configuration Group', commonParameters: 'Common Parameters', groupParameters: 'Group Parameters', childTaskConfig: 'Child Task Configuration', otherParameters: 'Other Parameters', launchSettings: 'Launch Settings', saved: 'Auto-saved', reset: 'Reset', noConfigParameters: 'No configurable parameters (schema not ready or task has no default_config).', schemaFailed: 'Schema collection failed: {error}', holdCtrlMulti: 'Hold Ctrl to select multiple items', currentValue: '{value} (current)', selectedOptionsHint: 'Available values: {values}', structuredJsonHint: 'JSON array supporting condition objects and action sequences.', debugOverlay: 'Debug Overlay', debugOverlayHint: 'Adopted automatically when tasks launch; can be toggled live while a task runs. When enabled, boxes drawn via draw_boxes are shown over the game window (GDI overlay).', toolboxGameSection: 'Game Connection', toolboxConnectGame: 'Connect Game', toolboxDisconnect: 'Disconnect', toolboxGameConnected: 'Connected: {title} (PID {pid})', toolboxGameNotConnected: 'Not connected. Connected window is reused first when tasks launch; the game auto-starts if not running.', availableOptions: 'Available Options', clickOptionToAdd: 'Click an option to add it.', selectedOptions: 'Selected Options', searchOptions: 'Search options…', moveUp: 'Move Up', moveDown: 'Move Down', removeItem: 'Remove', confirm: 'Confirm', addValue: 'Add value', taskPaused: 'Task paused. Click Resume to continue.', taskResumed: 'Task resumed.', stopping: 'Stopping task…', taskStopped: 'Task stopped. See Output for details.', taskCompleted: 'Task completed. See Output for details.', taskFailed: 'Task ended with an error. See Output for details.', toolboxOpenCharacterManager: 'Open Character & Skill Manager', templatesSearch: 'Search template names…', templatesTitle: 'Template Gallery', templatesHint: 'Click=insert · Double-click=copy · Hover thumbnail and click 👁 to view source', 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', thumbnailStatsWithFailures: '{loaded} thumbnails loaded / {failed} failed', thumbnailLoadFailed: 'Thumbnail failed to load', templateSize: 'Size: {width}×{height}', templateSource: 'Source: {path}', - templateAssetsTitle: 'Template Assets', templateAssetsHint: 'Click to open annotation editor', 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.', saveToAssetsTitle: 'Save to assets', saveToAssetsSuccess: 'Saved to: {path}', saveToAssetsFailed: 'Save failed: {error}', deleteImageConfirm: "Delete '{name}'? This cannot be undone.", deleteImageSuccess: 'Deleted: {name}', deleteImageFailed: 'Delete failed: {name}', importImagesTitle: 'Import images to ok_templates', importImagesSuccess: 'Imported {count} image(s)', annotationEditor: 'Annotation Editor', drawBbox: 'Draw (R)', deleteMode: 'Delete (D)', modifyBbox: 'Modify', newBboxTitle: 'New Bounding Box', editBboxTitle: 'Edit Bounding Box', categoryRequired: 'Name required', categoryExists: "Already exists in '{file}'", categoryLabel: 'Category:', widthLabel: 'Width:', heightLabel: 'Height:', + taskTitle: 'ok-script Task Launcher', noTasks: 'No tasks found.\nConfigure the project path first.', parameters: 'Parameters', collapseParameters: 'Collapse Parameters', launch: 'Launch', stop: 'Stop', pause: 'Pause', resume: 'Resume', running: 'Running…', oneTimeTask: 'One-time', triggerTask: 'Trigger', enableTrigger: 'Enable', triggerDisabled: 'Not enabled', triggerArmed: 'Armed', triggerEnqueued: 'Enqueued', triggerPolling: 'Polling', taskQueued: 'Waiting', taskRunning: 'Running', executorIdle: 'Executor stopped', executorConnecting: 'Starting executor…', executorRunning: 'Executor running · {count} trigger task(s) enqueued', executorPaused: 'Paused (polling and tasks are suspended)', startExecutor: 'Start executor', stopExecutor: 'Close executor', stopCurrent: 'Stop current task', configGroup: 'Configuration Group', commonParameters: 'Common Parameters', groupParameters: 'Group Parameters', childTaskConfig: 'Child Task Configuration', otherParameters: 'Other Parameters', launchSettings: 'Launch Settings', saved: 'Auto-saved', reset: 'Reset', noConfigParameters: 'No configurable parameters (schema not ready or task has no default_config).', schemaFailed: 'Schema collection failed: {error}', holdCtrlMulti: 'Hold Ctrl to select multiple items', currentValue: '{value} (current)', selectedOptionsHint: 'Available values: {values}', structuredJsonHint: 'JSON array supporting condition objects and action sequences.', debugOverlay: 'Debug Overlay', debugOverlayHint: 'Adopted automatically when tasks launch; can be toggled live while a task runs. When enabled, boxes drawn via draw_boxes are shown over the game window (GDI overlay).', toolboxGameSection: 'Game Connection', toolboxConnectGame: 'Connect Game', toolboxDisconnect: 'Disconnect', toolboxGameConnected: 'Connected: {title} (PID {pid})', toolboxGameNotConnected: 'Not connected. Connected window is reused first when tasks launch; the game auto-starts if not running.', availableOptions: 'Available Options', clickOptionToAdd: 'Click an option to add it.', selectedOptions: 'Selected Options', searchOptions: 'Search options…', moveUp: 'Move Up', moveDown: 'Move Down', removeItem: 'Remove', confirm: 'Confirm', addValue: 'Add value', taskPaused: 'Task paused. Click Resume to continue.', taskResumed: 'Task resumed.', stopping: 'Stopping task…', taskStopped: 'Task stopped. See Output for details.', taskCompleted: 'Task completed. See Output for details.', taskFailed: 'Task ended with an error. See Output for details.', toolboxOpenCharacterManager: 'Open Character & Skill Manager', templatesSearch: 'Search template names…', templatesTitle: 'Template Gallery', 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', thumbnailStatsWithFailures: '{loaded} thumbnails loaded / {failed} failed', thumbnailLoadFailed: 'Thumbnail failed to load', templateSize: 'Size: {width}×{height}', templateSource: 'Source: {path}', + templateAssetsTitle: 'Template 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.', screenshotSaved: 'Screenshot saved: {name}', screenshotFailed: 'Screenshot failed: {error}', noClipboardImage: 'No image in clipboard.', saveToAssetsTitle: 'Save to assets', saveToAssetsSuccess: 'Saved to: {path}', saveToAssetsFailed: 'Save failed: {error}', deleteImageConfirm: "Delete '{name}'? This cannot be undone.", deleteImageSuccess: 'Deleted: {name}', deleteImageFailed: 'Delete failed: {name}', importImagesTitle: 'Import images to ok_templates', importImagesSuccess: 'Imported {count} image(s)', annotationEditor: 'Annotation Editor', drawBbox: 'Draw (R)', deleteMode: 'Delete (D)', modifyBbox: 'Modify', newBboxTitle: 'New Bounding Box', editBboxTitle: 'Edit Bounding Box', categoryRequired: 'Name required', categoryExists: "Already exists in '{file}'", categoryLabel: 'Category:', widthLabel: 'Width:', heightLabel: 'Height:', assetImport: 'Import', assetSave: 'Save', assetSearch: 'Search...', assetNoMatch: 'No match', assetEditTooltip: 'Edit annotations', assetDeleteTooltip: 'Delete', assetImportHint: 'Use the Import button to add images from files.', assetSwapTooltip: 'Swap annotations with another image', assetSwapTitle: 'Swap annotations', @@ -540,11 +544,15 @@ const EN: WebviewStrings = { boxPathExists: 'That path already exists. Pick another name', generateBoxFailed: 'Could not create the box', publish: 'Publish', - assetsHint: 'Double-click an image to edit boxes. Publish writes the runtime file.', + assetsHint: 'Click an image or its edit button to edit boxes. Publish writes the runtime file.', boxAssetsTitle: 'Box Assets', galleryHint: 'Click inserts to_box(). Double-click copies the attribute path.', assetsEmpty: 'No template images yet. Add them in Template Assets.', galleryEmpty: 'No runtime boxes yet. Publish them from Box Assets.', + insertExpression: "Insert expression", + copyExpression: "Copy expression", + editAnnotations: "Edit annotations", + openSourceImage: "Open source image", }; const ZH_TW: WebviewStrings = { @@ -650,14 +658,19 @@ const ZH_TW: WebviewStrings = { boxPathExists: '這個路徑已經存在,換一個名字', generateBoxFailed: '產生框失敗', publish: '發布', - assetsHint: '雙擊原圖編輯框。發布後才會進入執行時。', + assetsHint: '單擊原圖或編輯按鈕編輯框。發布後才會進入執行時。', boxAssetsTitle: '框資源管理', galleryHint: '單擊插入 to_box()。雙擊複製屬性路徑。', assetsEmpty: '沒有模板原圖。請先在標註管理裡新增。', galleryEmpty: '還沒有執行時框。請先在框資源管理裡發布。', refresh: '重新整理', open: '開啟', add: '新增', modify: '修改', delete: '刪除', cancel: '取消', save: '儲存', copy: '複製', close: '關閉', search: '搜尋', loading: '載入中…', none: '無', current: '目前', enabled: '開啟', disabled: '關閉', error: '錯誤', warning: '警告', info: '資訊', charactersTitle: '角色技能管理', charactersTab: '角色與技能', effectsTab: '效果索引', localesTab: '名稱本地化', issuesTab: '資料診斷', searchCharacters: '搜尋角色、技能、效果、描述…', allStars: '全部星級', allElements: '全部元素', allProfessions: '全部職業', allSkillTypes: '全部技能類型', enhancementOnly: '僅顯示含強化組的角色', issueOnly: '僅顯示存在診斷的角色', characterCount: '{shown} / {total} 個角色', skillsEnhancements: '技能 / 強化', noCharacters: '沒有符合的角色', selectCharacter: '從左側選擇角色', openCharacterJson: '開啟角色 JSON', skillsAndEnhancements: '技能與強化效果', addSkill: '新增技能', modifySkill: '修改技能', addEnhancement: '新增強化組', modifyEnhancement: '修改強化組', baseEffects: '基礎效果', triggerEffects: '觸發依賴效果', outputEffects: '強化產出效果', visiblePulse: '可見脈衝', searchEffects: '搜尋效果 ID、描述、角色或技能…', allCategories: '全部分類', allEffects: '全部效果', usedOnly: '僅已引用', unusedOnly: '僅未引用', unknownOnly: '僅未知', addEffectCategory: '新增分類', addEffect: '新增效果', localizationMatrix: '角色名稱多語言矩陣', localizationHint: '空白儲存格表示缺失,可點擊角色開啟語言來源檔案。', missing: '缺失', searchIssues: '搜尋診斷資訊…', allSeverities: '全部級別', openSource: '開啟來源檔案', readingCharacterData: '正在讀取角色、技能、效果與多語言資料…', unknownCurrentEffect: '目前資料中的未知效果', inferredFromTriggerText: '由觸發文字推斷', clickCopySkillId: '點擊複製技能 ID', clickCopyEffectId: '點擊複製效果 ID', openCharacterLocaleFile: '開啟角色名稱語言檔案', loadFailed: '載入失敗', saveFailed: '儲存失敗', charactersMetric: '角色', skillsMetric: '技能', enhancementsMetric: '強化組', effectReferencesMetric: '效果引用', effectDefinitionsMetric: '效果定義', valueLabel: '值', taskTitle: 'ok-script 任務啟動', noTasks: '找不到任務。\n請先在設定中配置專案路徑。', parameters: '參數', launch: '啟動', stop: '停止', pause: '暫停', resume: '恢復', running: '執行中…', toolboxOpenCharacterManager: '開啟角色技能管理面板', templatesSearch: '搜尋模板名稱…', templatesTitle: '模板面板', noTemplates: '找不到任何模板。', noTemplatesWithHint: '找不到任何模板。\n請確認工作區存在 assets/coco_annotations.json\n(或 ok_tasks/assets/coco_annotations.json)。', thumbnailStats: '已載入 {loaded} 個縮圖', thumbnailStatsWithFailures: '已載入 {loaded} 個縮圖 / {failed} 個失敗', thumbnailLoadFailed: '縮圖載入失敗', templateSize: '尺寸:{width}×{height}', templateSource: '來源:{path}', - unknownStar: '星級未定', unknownElement: '元素未定', unknownProfession: '職業未定', unknownWeapon: '武器未定', skillsCount: '{count} 個技能', enhancementsCount: '{count} 個強化組', syncedSkillLocked: '同步技能 · ID/名稱/類型/元素/描述已鎖定', enhancedState: '強化態', multiplier: '倍率', stagger: '失衡', cooldown: '冷卻', spiritCost: '技力', noSkills: '此角色尚無技能資料', effectsCount: '{shown} / {total} 個效果', noEffects: '沒有符合的效果', undefinedEffect: '未定義效果', openDefinition: '開啟定義', moreUsages: '另有 {count} 處引用', characterIdColumn: '角色 / ID', issuesCount: '{shown} / {total} 條診斷', noIssuesMatch: '沒有符合的診斷', noIssues: '未發現資料問題', skillId: '技能 ID', skillName: '技能名稱', skillType: '技能類型', element: '元素', damageMultiplier: '傷害倍率', staggerValue: '失衡值', cooldownField: '冷卻', spiritCostField: '技力消耗', skillDescription: '技能描述', baseEffectsMulti: '基礎效果(可多選)', enhancementName: '強化組名稱', visibleMarker: '顯示標記', triggerText: '觸發條件文字', enhancementDescription: '強化效果說明', triggerEffectsMulti: '觸發依賴效果(可多選)', outputEffectsMulti: '強化產出效果(可多選)', effectCategory: '效果分類', effectId: '效果 ID(大寫底線)', effectDescription: '效果描述', categoryName: '分類名稱', effectMultiHint: '按住 Ctrl / Cmd 可多選;選項來自 effects.py,並依效果分類分組。', selectedTriggerEffects: '已選擇 {count} 個觸發依賴效果', noTriggerEffects: '未選擇觸發依賴效果', noSelectedEffects: '未選擇效果', unknownEffect: '未知效果', confirmDelete: '確定要刪除「{name}」嗎?\n儲存前會自動建立 .bak 備份。', copied: '已複製:{text}', collapseParameters: '收合參數', oneTimeTask: '一次性', triggerTask: '觸發任務', enableTrigger: '啟用', triggerDisabled: '未啟用', triggerArmed: '已啟用', triggerEnqueued: '已入列', triggerPolling: '輪詢中', taskQueued: '排隊中', taskRunning: '執行中', executorIdle: '執行器未啟動', executorConnecting: '執行器啟動中…', executorRunning: '執行器執行中 · {count} 個觸發任務已入列', executorPaused: '已暫停(輪詢與任務皆暫停)', startExecutor: '啟動執行器', stopExecutor: '關閉執行器', stopCurrent: '停止目前任務', configGroup: '設定分組', commonParameters: '通用參數', groupParameters: '分組參數', childTaskConfig: '子任務設定', otherParameters: '其他參數', launchSettings: '啟動設定', saved: '已自動儲存', reset: '重設', noConfigParameters: '目前沒有可設定參數(schema 尚未就緒或任務沒有 default_config)。', schemaFailed: '此任務 schema 收集失敗,無法自動產生表單:{error}', holdCtrlMulti: '按住 Ctrl 多選', currentValue: '{value}(目前)', selectedOptionsHint: '可選值:{values}', structuredJsonHint: 'JSON 陣列;支援條件物件與動作序列。', debugOverlay: '除錯浮層', debugOverlayHint: '任務啟動時自動沿用;任務執行中可即時開關。開啟後 draw_boxes 的識別框會繪製到遊戲視窗上(GDI 浮層)。', toolboxGameSection: '遊戲連線', toolboxConnectGame: '連線遊戲', toolboxDisconnect: '中斷', toolboxGameConnected: '已連線:{title}(PID {pid})', toolboxGameNotConnected: '未連線遊戲。連線後任務啟動將優先複用該視窗;遊戲未執行時會自動啟動。', availableOptions: '可用選項', clickOptionToAdd: '點擊選項新增。', selectedOptions: '已選清單', searchOptions: '搜尋選項…', moveUp: '上移', moveDown: '下移', removeItem: '移除', confirm: '確認', addValue: '新增值', taskPaused: '任務已暫停,點擊恢復繼續執行', taskResumed: '任務已恢復執行', stopping: '正在停止任務…', taskStopped: '任務已停止,詳見輸出面板', taskCompleted: '任務完成,詳見輸出面板', taskFailed: '任務異常結束,詳見輸出面板', templatesHint: '單擊=插入 · 雙擊=複製 · 將游標移到縮圖並點擊 👁 檢視原圖', templatesCount: '{shown}/{total} 個模板', noTemplateMatch: '沒有符合「{query}」的模板。', viewOriginal: '檢視原圖(標示位置)', + unknownStar: '星級未定', unknownElement: '元素未定', unknownProfession: '職業未定', unknownWeapon: '武器未定', skillsCount: '{count} 個技能', enhancementsCount: '{count} 個強化組', syncedSkillLocked: '同步技能 · ID/名稱/類型/元素/描述已鎖定', enhancedState: '強化態', multiplier: '倍率', stagger: '失衡', cooldown: '冷卻', spiritCost: '技力', noSkills: '此角色尚無技能資料', effectsCount: '{shown} / {total} 個效果', noEffects: '沒有符合的效果', undefinedEffect: '未定義效果', openDefinition: '開啟定義', moreUsages: '另有 {count} 處引用', characterIdColumn: '角色 / ID', issuesCount: '{shown} / {total} 條診斷', noIssuesMatch: '沒有符合的診斷', noIssues: '未發現資料問題', skillId: '技能 ID', skillName: '技能名稱', skillType: '技能類型', element: '元素', damageMultiplier: '傷害倍率', staggerValue: '失衡值', cooldownField: '冷卻', spiritCostField: '技力消耗', skillDescription: '技能描述', baseEffectsMulti: '基礎效果(可多選)', enhancementName: '強化組名稱', visibleMarker: '顯示標記', triggerText: '觸發條件文字', enhancementDescription: '強化效果說明', triggerEffectsMulti: '觸發依賴效果(可多選)', outputEffectsMulti: '強化產出效果(可多選)', effectCategory: '效果分類', effectId: '效果 ID(大寫底線)', effectDescription: '效果描述', categoryName: '分類名稱', effectMultiHint: '按住 Ctrl / Cmd 可多選;選項來自 effects.py,並依效果分類分組。', selectedTriggerEffects: '已選擇 {count} 個觸發依賴效果', noTriggerEffects: '未選擇觸發依賴效果', noSelectedEffects: '未選擇效果', unknownEffect: '未知效果', confirmDelete: '確定要刪除「{name}」嗎?\n儲存前會自動建立 .bak 備份。', copied: '已複製:{text}', collapseParameters: '收合參數', oneTimeTask: '一次性', triggerTask: '觸發任務', enableTrigger: '啟用', triggerDisabled: '未啟用', triggerArmed: '已啟用', triggerEnqueued: '已入列', triggerPolling: '輪詢中', taskQueued: '排隊中', taskRunning: '執行中', executorIdle: '執行器未啟動', executorConnecting: '執行器啟動中…', executorRunning: '執行器執行中 · {count} 個觸發任務已入列', executorPaused: '已暫停(輪詢與任務皆暫停)', startExecutor: '啟動執行器', stopExecutor: '關閉執行器', stopCurrent: '停止目前任務', configGroup: '設定分組', commonParameters: '通用參數', groupParameters: '分組參數', childTaskConfig: '子任務設定', otherParameters: '其他參數', launchSettings: '啟動設定', saved: '已自動儲存', reset: '重設', noConfigParameters: '目前沒有可設定參數(schema 尚未就緒或任務沒有 default_config)。', schemaFailed: '此任務 schema 收集失敗,無法自動產生表單:{error}', holdCtrlMulti: '按住 Ctrl 多選', currentValue: '{value}(目前)', selectedOptionsHint: '可選值:{values}', structuredJsonHint: 'JSON 陣列;支援條件物件與動作序列。', debugOverlay: '除錯浮層', debugOverlayHint: '任務啟動時自動沿用;任務執行中可即時開關。開啟後 draw_boxes 的識別框會繪製到遊戲視窗上(GDI 浮層)。', toolboxGameSection: '遊戲連線', toolboxConnectGame: '連線遊戲', toolboxDisconnect: '中斷', toolboxGameConnected: '已連線:{title}(PID {pid})', toolboxGameNotConnected: '未連線遊戲。連線後任務啟動將優先複用該視窗;遊戲未執行時會自動啟動。', availableOptions: '可用選項', clickOptionToAdd: '點擊選項新增。', selectedOptions: '已選清單', searchOptions: '搜尋選項…', moveUp: '上移', moveDown: '下移', removeItem: '移除', confirm: '確認', addValue: '新增值', taskPaused: '任務已暫停,點擊恢復繼續執行', taskResumed: '任務已恢復執行', stopping: '正在停止任務…', taskStopped: '任務已停止,詳見輸出面板', taskCompleted: '任務完成,詳見輸出面板', taskFailed: '任務異常結束,詳見輸出面板', templatesHint: '單擊=插入 · 雙擊=複製 · 右下角按鈕可直接操作', templatesCount: '{shown}/{total} 個模板', noTemplateMatch: '沒有符合「{query}」的模板。', viewOriginal: '檢視原圖(標示位置)', + insertExpression: "插入表達式", + copyExpression: "複製表達式", + editAnnotations: "編輯標註", + openSourceImage: "開啟原圖", + templateAssetsHint: '單擊或點擊編輯按鈕開啟標註編輯器', }; const JA: WebviewStrings = { @@ -746,7 +759,13 @@ const JA: WebviewStrings = { assetSwapBoxes: 'アノテーション {count} 件', assetSwapNoBoxes: 'アノテーションなし', refresh: '更新', open: '開く', add: '追加', modify: '変更', delete: '削除', cancel: 'キャンセル', save: '保存', copy: 'コピー', close: '閉じる', search: '検索', loading: '読み込み中…', none: 'なし', current: '現在', enabled: 'オン', disabled: 'オフ', error: 'エラー', warning: '警告', info: '情報', charactersTitle: 'キャラクター・スキル管理', charactersTab: 'キャラクターとスキル', effectsTab: '効果インデックス', localesTab: '名前のローカライズ', issuesTab: 'データ診断', searchCharacters: 'キャラクター、スキル、効果、説明を検索…', addSkill: 'スキルを追加', modifySkill: 'スキルを変更', addEnhancement: '強化グループを追加', modifyEnhancement: '強化グループを変更', addEffectCategory: 'カテゴリを追加', addEffect: '効果を追加', charactersMetric: 'キャラクター', skillsMetric: 'スキル', enhancementsMetric: '強化', effectReferencesMetric: '効果参照', effectDefinitionsMetric: '効果定義', valueLabel: '値', loadFailed: '読み込みに失敗しました', saveFailed: '保存に失敗しました', taskTitle: 'ok-script タスク起動', launch: '起動', stop: '停止', pause: '一時停止', resume: '再開', parameters: 'パラメータ', toolboxOpenCharacterManager: 'キャラクター・スキル管理を開く', templatesTitle: 'テンプレートギャラリー', templatesSearch: 'テンプレート名を検索…', noTemplatesWithHint: 'テンプレートが見つかりません。\nワークスペースに assets/coco_annotations.json\n(または ok_tasks/assets/coco_annotations.json)があることを確認してください。', - allStars: 'すべてのレア度', allElements: 'すべての元素', allProfessions: 'すべての職業', allSkillTypes: 'すべてのスキルタイプ', enhancementOnly: '強化グループのあるキャラクターのみ', issueOnly: '診断のあるキャラクターのみ', characterCount: '{shown} / {total} キャラクター', skillsEnhancements: 'スキル / 強化', noCharacters: '一致するキャラクターがありません', selectCharacter: '左側からキャラクターを選択してください', unknownStar: 'レア度未設定', unknownElement: '元素未設定', unknownProfession: '職業未設定', unknownWeapon: '武器未設定', skillsCount: '{count} スキル', enhancementsCount: '{count} 強化グループ', openCharacterJson: 'キャラクター JSON を開く', skillsAndEnhancements: 'スキルと強化効果', syncedSkillLocked: '同期スキル · ID/名前/タイプ/元素/説明はロック済み', enhancedState: '強化状態', baseEffects: '基本効果', triggerEffects: 'トリガー依存効果', outputEffects: '強化出力効果', visiblePulse: '可視パルス', multiplier: '倍率', stagger: 'ブレイク', cooldown: 'クールダウン', spiritCost: 'SP 消費', noSkills: 'このキャラクターにはスキルデータがありません', searchEffects: '効果 ID、説明、キャラクター、スキルを検索…', allCategories: 'すべてのカテゴリ', allEffects: 'すべての効果', usedOnly: '使用中のみ', unusedOnly: '未使用のみ', unknownOnly: '不明のみ', effectsCount: '{shown} / {total} 効果', noEffects: '一致する効果がありません', undefinedEffect: '未定義の効果', openDefinition: '定義を開く', moreUsages: 'ほか {count} 箇所で参照', localizationMatrix: 'キャラクター名ローカライズ表', localizationHint: '空欄は欠落を示します。キャラクターをクリックすると言語ソースを開けます。', characterIdColumn: 'キャラクター / ID', missing: '欠落', searchIssues: '診断を検索…', allSeverities: 'すべての重要度', issuesCount: '{shown} / {total} 件の診断', noIssuesMatch: '一致する診断がありません', noIssues: 'データ問題は見つかりませんでした', openSource: 'ソースを開く', readingCharacterData: 'キャラクター、スキル、効果、ローカライズデータを読み込み中…', skillId: 'スキル ID', skillName: 'スキル名', skillType: 'スキルタイプ', element: '元素', damageMultiplier: 'ダメージ倍率', staggerValue: 'ブレイク値', cooldownField: 'クールダウン', spiritCostField: 'SP 消費', skillDescription: 'スキル説明', baseEffectsMulti: '基本効果(複数選択)', enhancementName: '強化グループ名', visibleMarker: '表示マーカー', triggerText: 'トリガー条件テキスト', enhancementDescription: '強化効果の説明', triggerEffectsMulti: 'トリガー依存効果(複数選択)', outputEffectsMulti: '強化出力効果(複数選択)', effectCategory: '効果カテゴリ', effectId: '効果 ID(大文字スネークケース)', effectDescription: '効果説明', categoryName: 'カテゴリ名', effectMultiHint: 'Ctrl / Cmd を押しながら複数選択できます。選択肢は effects.py から取得し、カテゴリ別に表示します。', selectedTriggerEffects: 'トリガー依存効果を {count} 件選択', noTriggerEffects: 'トリガー依存効果が選択されていません', noSelectedEffects: '効果が選択されていません', unknownEffect: '不明な効果', unknownCurrentEffect: '現在のデータ内の不明な効果', inferredFromTriggerText: 'トリガーテキストから推定', clickCopySkillId: 'クリックしてスキル ID をコピー', clickCopyEffectId: 'クリックして効果 ID をコピー', openCharacterLocaleFile: 'キャラクター名の言語ファイルを開く', confirmDelete: '「{name}」を削除しますか?\n保存前に .bak バックアップを自動作成します。', copied: 'コピーしました: {text}', noTasks: 'タスクが見つかりません。\n先にプロジェクトパスを設定してください。', collapseParameters: 'パラメーターを折りたたむ', running: '実行中…', oneTimeTask: '単発', triggerTask: 'トリガー', enableTrigger: '有効化', triggerDisabled: '無効', triggerArmed: '有効', triggerEnqueued: 'キュー済み', triggerPolling: 'ポーリング中', taskQueued: '待機中', taskRunning: '実行中', executorIdle: 'エグゼキュータ停止中', executorConnecting: 'エグゼキュータ起動中…', executorRunning: 'エグゼキュータ実行中 · トリガータスク {count} 件がキュー済み', executorPaused: '一時停止中(ポーリングとタスクが停止)', startExecutor: 'エグゼキュータを起動', stopExecutor: 'エグゼキュータを終了', stopCurrent: '現在のタスクを停止', configGroup: '設定グループ', commonParameters: '共通パラメーター', groupParameters: 'グループパラメーター', childTaskConfig: '子タスク設定', otherParameters: 'その他のパラメーター', launchSettings: '起動設定', saved: '自動保存済み', reset: 'リセット', noConfigParameters: '設定可能なパラメーターがありません(schema 未準備、または default_config なし)。', schemaFailed: 'このタスクの schema 収集に失敗しました: {error}', holdCtrlMulti: 'Ctrl を押しながら複数選択', currentValue: '{value}(現在)', selectedOptionsHint: '選択可能な値: {values}', structuredJsonHint: '条件オブジェクトとアクション列を含む JSON 配列。', debugOverlay: 'デバッグ オーバーレイ', debugOverlayHint: 'タスク起動時に自動で引き継ぎ、実行中でも即時に切り替えられます。有効にすると draw_boxes の枠がゲームウィンドウ上に表示されます(GDI オーバーレイ)。', toolboxGameSection: 'ゲーム接続', toolboxConnectGame: 'ゲームに接続', toolboxDisconnect: '切断', toolboxGameConnected: '接続済み:{title}(PID {pid})', toolboxGameNotConnected: '未接続です。接続するとタスク起動時にそのウィンドウを優先的に再利用します。ゲームが未起動なら自動で起動します。', availableOptions: '選択肢', clickOptionToAdd: '選択肢をクリックして追加します。', selectedOptions: '選択済みリスト', searchOptions: '選択肢を検索…', moveUp: '上へ', moveDown: '下へ', removeItem: '削除', confirm: '確認', addValue: '値を追加', taskPaused: 'タスクを一時停止しました。再開をクリックで続行します。', taskResumed: 'タスクを再開しました。', stopping: 'タスクを停止しています…', taskStopped: 'タスクを停止しました。詳細は出力を確認してください。', taskCompleted: 'タスクが完了しました。詳細は出力を確認してください。', taskFailed: 'タスクがエラーで終了しました。詳細は出力を確認してください。', templatesHint: 'クリック=挿入 · ダブルクリック=コピー · サムネイルにカーソルを合わせて 👁 をクリックすると元画像を表示', templatesCount: '{shown}/{total} テンプレート', noTemplates: 'テンプレートが見つかりません。', noTemplateMatch: '「{query}」に一致するテンプレートがありません。', viewOriginal: '位置を強調表示した元画像を表示', thumbnailStats: 'サムネイル {loaded} 件を読み込み', thumbnailStatsWithFailures: 'サムネイル {loaded} 件を読み込み / {failed} 件失敗', thumbnailLoadFailed: 'サムネイルの読み込みに失敗', templateSize: 'サイズ: {width}×{height}', templateSource: 'ソース: {path}', + allStars: 'すべてのレア度', allElements: 'すべての元素', allProfessions: 'すべての職業', allSkillTypes: 'すべてのスキルタイプ', enhancementOnly: '強化グループのあるキャラクターのみ', issueOnly: '診断のあるキャラクターのみ', characterCount: '{shown} / {total} キャラクター', skillsEnhancements: 'スキル / 強化', noCharacters: '一致するキャラクターがありません', selectCharacter: '左側からキャラクターを選択してください', unknownStar: 'レア度未設定', unknownElement: '元素未設定', unknownProfession: '職業未設定', unknownWeapon: '武器未設定', skillsCount: '{count} スキル', enhancementsCount: '{count} 強化グループ', openCharacterJson: 'キャラクター JSON を開く', skillsAndEnhancements: 'スキルと強化効果', syncedSkillLocked: '同期スキル · ID/名前/タイプ/元素/説明はロック済み', enhancedState: '強化状態', baseEffects: '基本効果', triggerEffects: 'トリガー依存効果', outputEffects: '強化出力効果', visiblePulse: '可視パルス', multiplier: '倍率', stagger: 'ブレイク', cooldown: 'クールダウン', spiritCost: 'SP 消費', noSkills: 'このキャラクターにはスキルデータがありません', searchEffects: '効果 ID、説明、キャラクター、スキルを検索…', allCategories: 'すべてのカテゴリ', allEffects: 'すべての効果', usedOnly: '使用中のみ', unusedOnly: '未使用のみ', unknownOnly: '不明のみ', effectsCount: '{shown} / {total} 効果', noEffects: '一致する効果がありません', undefinedEffect: '未定義の効果', openDefinition: '定義を開く', moreUsages: 'ほか {count} 箇所で参照', localizationMatrix: 'キャラクター名ローカライズ表', localizationHint: '空欄は欠落を示します。キャラクターをクリックすると言語ソースを開けます。', characterIdColumn: 'キャラクター / ID', missing: '欠落', searchIssues: '診断を検索…', allSeverities: 'すべての重要度', issuesCount: '{shown} / {total} 件の診断', noIssuesMatch: '一致する診断がありません', noIssues: 'データ問題は見つかりませんでした', openSource: 'ソースを開く', readingCharacterData: 'キャラクター、スキル、効果、ローカライズデータを読み込み中…', skillId: 'スキル ID', skillName: 'スキル名', skillType: 'スキルタイプ', element: '元素', damageMultiplier: 'ダメージ倍率', staggerValue: 'ブレイク値', cooldownField: 'クールダウン', spiritCostField: 'SP 消費', skillDescription: 'スキル説明', baseEffectsMulti: '基本効果(複数選択)', enhancementName: '強化グループ名', visibleMarker: '表示マーカー', triggerText: 'トリガー条件テキスト', enhancementDescription: '強化効果の説明', triggerEffectsMulti: 'トリガー依存効果(複数選択)', outputEffectsMulti: '強化出力効果(複数選択)', effectCategory: '効果カテゴリ', effectId: '効果 ID(大文字スネークケース)', effectDescription: '効果説明', categoryName: 'カテゴリ名', effectMultiHint: 'Ctrl / Cmd を押しながら複数選択できます。選択肢は effects.py から取得し、カテゴリ別に表示します。', selectedTriggerEffects: 'トリガー依存効果を {count} 件選択', noTriggerEffects: 'トリガー依存効果が選択されていません', noSelectedEffects: '効果が選択されていません', unknownEffect: '不明な効果', unknownCurrentEffect: '現在のデータ内の不明な効果', inferredFromTriggerText: 'トリガーテキストから推定', clickCopySkillId: 'クリックしてスキル ID をコピー', clickCopyEffectId: 'クリックして効果 ID をコピー', openCharacterLocaleFile: 'キャラクター名の言語ファイルを開く', confirmDelete: '「{name}」を削除しますか?\n保存前に .bak バックアップを自動作成します。', copied: 'コピーしました: {text}', noTasks: 'タスクが見つかりません。\n先にプロジェクトパスを設定してください。', collapseParameters: 'パラメーターを折りたたむ', running: '実行中…', oneTimeTask: '単発', triggerTask: 'トリガー', enableTrigger: '有効化', triggerDisabled: '無効', triggerArmed: '有効', triggerEnqueued: 'キュー済み', triggerPolling: 'ポーリング中', taskQueued: '待機中', taskRunning: '実行中', executorIdle: 'エグゼキュータ停止中', executorConnecting: 'エグゼキュータ起動中…', executorRunning: 'エグゼキュータ実行中 · トリガータスク {count} 件がキュー済み', executorPaused: '一時停止中(ポーリングとタスクが停止)', startExecutor: 'エグゼキュータを起動', stopExecutor: 'エグゼキュータを終了', stopCurrent: '現在のタスクを停止', configGroup: '設定グループ', commonParameters: '共通パラメーター', groupParameters: 'グループパラメーター', childTaskConfig: '子タスク設定', otherParameters: 'その他のパラメーター', launchSettings: '起動設定', saved: '自動保存済み', reset: 'リセット', noConfigParameters: '設定可能なパラメーターがありません(schema 未準備、または default_config なし)。', schemaFailed: 'このタスクの schema 収集に失敗しました: {error}', holdCtrlMulti: 'Ctrl を押しながら複数選択', currentValue: '{value}(現在)', selectedOptionsHint: '選択可能な値: {values}', structuredJsonHint: '条件オブジェクトとアクション列を含む JSON 配列。', debugOverlay: 'デバッグ オーバーレイ', debugOverlayHint: 'タスク起動時に自動で引き継ぎ、実行中でも即時に切り替えられます。有効にすると draw_boxes の枠がゲームウィンドウ上に表示されます(GDI オーバーレイ)。', toolboxGameSection: 'ゲーム接続', toolboxConnectGame: 'ゲームに接続', toolboxDisconnect: '切断', toolboxGameConnected: '接続済み:{title}(PID {pid})', toolboxGameNotConnected: '未接続です。接続するとタスク起動時にそのウィンドウを優先的に再利用します。ゲームが未起動なら自動で起動します。', availableOptions: '選択肢', clickOptionToAdd: '選択肢をクリックして追加します。', selectedOptions: '選択済みリスト', searchOptions: '選択肢を検索…', moveUp: '上へ', moveDown: '下へ', removeItem: '削除', confirm: '確認', addValue: '値を追加', taskPaused: 'タスクを一時停止しました。再開をクリックで続行します。', taskResumed: 'タスクを再開しました。', stopping: 'タスクを停止しています…', taskStopped: 'タスクを停止しました。詳細は出力を確認してください。', taskCompleted: 'タスクが完了しました。詳細は出力を確認してください。', taskFailed: 'タスクがエラーで終了しました。詳細は出力を確認してください。', templatesHint: 'クリック=挿入 · ダブルクリック=コピー · 右下のボタンで直接操作', templatesCount: '{shown}/{total} テンプレート', noTemplates: 'テンプレートが見つかりません。', noTemplateMatch: '「{query}」に一致するテンプレートがありません。', viewOriginal: '位置を強調表示した元画像を表示', thumbnailStats: 'サムネイル {loaded} 件を読み込み', thumbnailStatsWithFailures: 'サムネイル {loaded} 件を読み込み / {failed} 件失敗', thumbnailLoadFailed: 'サムネイルの読み込みに失敗', templateSize: 'サイズ: {width}×{height}', templateSource: 'ソース: {path}', + insertExpression: "式を挿入", + copyExpression: "式をコピー", + editAnnotations: "注釈を編集", + openSourceImage: "元画像を開く", + templateAssetsHint: '画像または編集ボタンをクリックしてアノテーションを開く', + assetsHint: '画像または編集ボタンをクリックして枠を編集。公開後に実行時ファイルへ反映されます。', }; const KO: WebviewStrings = { @@ -835,7 +854,13 @@ const KO: WebviewStrings = { assetSwapBoxes: '어노테이션 {count}개', assetSwapNoBoxes: '어노테이션 없음', refresh: '새로 고침', open: '열기', add: '추가', modify: '수정', delete: '삭제', cancel: '취소', save: '저장', copy: '복사', close: '닫기', search: '검색', loading: '불러오는 중…', none: '없음', current: '현재', enabled: '켜짐', disabled: '꺼짐', error: '오류', warning: '경고', info: '정보', charactersTitle: '캐릭터·스킬 관리', charactersTab: '캐릭터와 스킬', effectsTab: '효과 인덱스', localesTab: '이름 현지화', issuesTab: '데이터 진단', searchCharacters: '캐릭터, 스킬, 효과, 설명 검색…', addSkill: '스킬 추가', modifySkill: '스킬 수정', addEnhancement: '강화 그룹 추가', modifyEnhancement: '강화 그룹 수정', addEffectCategory: '분류 추가', addEffect: '효과 추가', charactersMetric: '캐릭터', skillsMetric: '스킬', enhancementsMetric: '강화 그룹', effectReferencesMetric: '효과 참조', effectDefinitionsMetric: '효과 정의', valueLabel: '값', loadFailed: '불러오기 실패', saveFailed: '저장 실패', taskTitle: 'ok-script 작업 실행', launch: '실행', stop: '중지', pause: '일시중지', resume: '재개', parameters: '매개변수', toolboxOpenCharacterManager: '캐릭터·스킬 관리 열기', templatesTitle: '템플릿 갤러리', templatesSearch: '템플릿 이름 검색…', noTemplatesWithHint: '템플릿을 찾을 수 없습니다.\n작업 영역에 assets/coco_annotations.json\n(또는 ok_tasks/assets/coco_annotations.json)이 있는지 확인하세요.', - allStars: '모든 희귀도', allElements: '모든 원소', allProfessions: '모든 직업', allSkillTypes: '모든 스킬 유형', enhancementOnly: '강화 그룹이 있는 캐릭터만', issueOnly: '진단이 있는 캐릭터만', characterCount: '{shown} / {total} 캐릭터', skillsEnhancements: '스킬 / 강화', noCharacters: '일치하는 캐릭터가 없습니다', selectCharacter: '왼쪽에서 캐릭터를 선택하세요', unknownStar: '희귀도 미정', unknownElement: '원소 미정', unknownProfession: '직업 미정', unknownWeapon: '무기 미정', skillsCount: '스킬 {count}개', enhancementsCount: '강화 그룹 {count}개', openCharacterJson: '캐릭터 JSON 열기', skillsAndEnhancements: '스킬 및 강화 효과', syncedSkillLocked: '동기화 스킬 · ID/이름/유형/원소/설명 잠김', enhancedState: '강화 상태', baseEffects: '기본 효과', triggerEffects: '트리거 의존 효과', outputEffects: '강화 출력 효과', visiblePulse: '표시 펄스', multiplier: '배율', stagger: '브레이크', cooldown: '재사용 대기시간', spiritCost: 'SP 소모', noSkills: '이 캐릭터에는 스킬 데이터가 없습니다', searchEffects: '효과 ID, 설명, 캐릭터 또는 스킬 검색…', allCategories: '모든 분류', allEffects: '모든 효과', usedOnly: '사용 중만', unusedOnly: '미사용만', unknownOnly: '알 수 없음만', effectsCount: '{shown} / {total} 효과', noEffects: '일치하는 효과가 없습니다', undefinedEffect: '정의되지 않은 효과', openDefinition: '정의 열기', moreUsages: '참조 {count}개 더 있음', localizationMatrix: '캐릭터 이름 현지화 매트릭스', localizationHint: '빈 셀은 누락을 의미합니다. 캐릭터를 클릭해 언어 원본을 여세요.', characterIdColumn: '캐릭터 / ID', missing: '누락', searchIssues: '진단 검색…', allSeverities: '모든 심각도', issuesCount: '{shown} / {total} 진단', noIssuesMatch: '일치하는 진단이 없습니다', noIssues: '데이터 문제가 없습니다', openSource: '소스 열기', readingCharacterData: '캐릭터, 스킬, 효과 및 현지화 데이터 읽는 중…', skillId: '스킬 ID', skillName: '스킬 이름', skillType: '스킬 유형', element: '원소', damageMultiplier: '피해 배율', staggerValue: '브레이크 값', cooldownField: '재사용 대기시간', spiritCostField: 'SP 소모', skillDescription: '스킬 설명', baseEffectsMulti: '기본 효과(다중 선택)', enhancementName: '강화 그룹 이름', visibleMarker: '표시 마커', triggerText: '트리거 조건 텍스트', enhancementDescription: '강화 효과 설명', triggerEffectsMulti: '트리거 의존 효과(다중 선택)', outputEffectsMulti: '강화 출력 효과(다중 선택)', effectCategory: '효과 분류', effectId: '효과 ID(대문자 스네이크 표기)', effectDescription: '효과 설명', categoryName: '분류 이름', effectMultiHint: 'Ctrl / Cmd를 누른 채 다중 선택할 수 있습니다. 선택지는 effects.py에서 가져와 분류별로 표시합니다.', selectedTriggerEffects: '트리거 의존 효과 {count}개 선택됨', noTriggerEffects: '트리거 의존 효과를 선택하지 않았습니다', noSelectedEffects: '효과를 선택하지 않았습니다', unknownEffect: '알 수 없는 효과', unknownCurrentEffect: '현재 데이터의 알 수 없는 효과', inferredFromTriggerText: '트리거 텍스트에서 추론', clickCopySkillId: '클릭하여 스킬 ID 복사', clickCopyEffectId: '클릭하여 효과 ID 복사', openCharacterLocaleFile: '캐릭터 이름 언어 파일 열기', confirmDelete: '“{name}”을(를) 삭제하시겠습니까?\n저장 전에 .bak 백업을 자동 생성합니다.', copied: '복사됨: {text}', noTasks: '작업을 찾을 수 없습니다.\n먼저 프로젝트 경로를 설정하세요.', collapseParameters: '매개변수 접기', running: '실행 중…', oneTimeTask: '일회성', triggerTask: '트리거', enableTrigger: '사용', triggerDisabled: '사용 안 함', triggerArmed: '사용 설정됨', triggerEnqueued: '대기열 등록됨', triggerPolling: '폴링 중', taskQueued: '대기 중', taskRunning: '실행 중', executorIdle: '실행기 중지됨', executorConnecting: '실행기 시작 중…', executorRunning: '실행기 실행 중 · 트리거 작업 {count}개 대기열 등록됨', executorPaused: '일시중지됨(폴링과 작업 모두 중단)', startExecutor: '실행기 시작', stopExecutor: '실행기 닫기', stopCurrent: '현재 작업 중지', configGroup: '설정 그룹', commonParameters: '공통 매개변수', groupParameters: '그룹 매개변수', childTaskConfig: '하위 작업 설정', otherParameters: '기타 매개변수', launchSettings: '실행 설정', saved: '자동 저장됨', reset: '초기화', noConfigParameters: '설정 가능한 매개변수가 없습니다(schema 미준비 또는 default_config 없음).', schemaFailed: '이 작업의 schema 수집 실패: {error}', holdCtrlMulti: 'Ctrl을 누른 채 다중 선택', currentValue: '{value}(현재)', selectedOptionsHint: '선택 가능 값: {values}', structuredJsonHint: '조건 객체와 작업 시퀀스를 지원하는 JSON 배열입니다.', debugOverlay: '디버그 오버레이', debugOverlayHint: '작업 시작 시 자동으로 적용되며, 실행 중에도 즉시 전환할 수 있습니다. 활성화하면 draw_boxes로 그려진 박스가 게임 창 위에 표시됩니다(GDI 오버레이).', toolboxGameSection: '게임 연결', toolboxConnectGame: '게임 연결', toolboxDisconnect: '연결 해제', toolboxGameConnected: '연결됨: {title}(PID {pid})', toolboxGameNotConnected: '연결되지 않았습니다. 연결하면 작업 시작 시 해당 창을 우선 재사용합니다. 게임이 실행 중이 아니면 자동으로 시작합니다.', availableOptions: '사용 가능한 옵션', clickOptionToAdd: '옵션을 클릭해 추가하세요.', selectedOptions: '선택된 목록', searchOptions: '옵션 검색…', moveUp: '위로', moveDown: '아래로', removeItem: '제거', confirm: '확인', addValue: '값 추가', taskPaused: '작업이 일시중지되었습니다. 재개를 클릭해 계속하세요.', taskResumed: '작업이 재개되었습니다.', stopping: '작업을 중지하는 중…', taskStopped: '작업이 중지되었습니다. 자세한 내용은 출력을 확인하세요.', taskCompleted: '작업이 완료되었습니다. 자세한 내용은 출력을 확인하세요.', taskFailed: '작업이 오류로 종료되었습니다. 자세한 내용은 출력을 확인하세요.', templatesHint: '클릭=삽입 · 두 번 클릭=복사 · 미리 보기 위에서 👁을 클릭해 원본 보기', templatesCount: '{shown}/{total} 템플릿', noTemplates: '템플릿을 찾을 수 없습니다.', noTemplateMatch: '“{query}”와 일치하는 템플릿이 없습니다.', viewOriginal: '위치를 강조 표시한 원본 이미지 보기', thumbnailStats: '미리 보기 {loaded}개 불러옴', thumbnailStatsWithFailures: '미리 보기 {loaded}개 불러옴 / {failed}개 실패', thumbnailLoadFailed: '미리 보기 불러오기 실패', templateSize: '크기: {width}×{height}', templateSource: '소스: {path}', + allStars: '모든 희귀도', allElements: '모든 원소', allProfessions: '모든 직업', allSkillTypes: '모든 스킬 유형', enhancementOnly: '강화 그룹이 있는 캐릭터만', issueOnly: '진단이 있는 캐릭터만', characterCount: '{shown} / {total} 캐릭터', skillsEnhancements: '스킬 / 강화', noCharacters: '일치하는 캐릭터가 없습니다', selectCharacter: '왼쪽에서 캐릭터를 선택하세요', unknownStar: '희귀도 미정', unknownElement: '원소 미정', unknownProfession: '직업 미정', unknownWeapon: '무기 미정', skillsCount: '스킬 {count}개', enhancementsCount: '강화 그룹 {count}개', openCharacterJson: '캐릭터 JSON 열기', skillsAndEnhancements: '스킬 및 강화 효과', syncedSkillLocked: '동기화 스킬 · ID/이름/유형/원소/설명 잠김', enhancedState: '강화 상태', baseEffects: '기본 효과', triggerEffects: '트리거 의존 효과', outputEffects: '강화 출력 효과', visiblePulse: '표시 펄스', multiplier: '배율', stagger: '브레이크', cooldown: '재사용 대기시간', spiritCost: 'SP 소모', noSkills: '이 캐릭터에는 스킬 데이터가 없습니다', searchEffects: '효과 ID, 설명, 캐릭터 또는 스킬 검색…', allCategories: '모든 분류', allEffects: '모든 효과', usedOnly: '사용 중만', unusedOnly: '미사용만', unknownOnly: '알 수 없음만', effectsCount: '{shown} / {total} 효과', noEffects: '일치하는 효과가 없습니다', undefinedEffect: '정의되지 않은 효과', openDefinition: '정의 열기', moreUsages: '참조 {count}개 더 있음', localizationMatrix: '캐릭터 이름 현지화 매트릭스', localizationHint: '빈 셀은 누락을 의미합니다. 캐릭터를 클릭해 언어 원본을 여세요.', characterIdColumn: '캐릭터 / ID', missing: '누락', searchIssues: '진단 검색…', allSeverities: '모든 심각도', issuesCount: '{shown} / {total} 진단', noIssuesMatch: '일치하는 진단이 없습니다', noIssues: '데이터 문제가 없습니다', openSource: '소스 열기', readingCharacterData: '캐릭터, 스킬, 효과 및 현지화 데이터 읽는 중…', skillId: '스킬 ID', skillName: '스킬 이름', skillType: '스킬 유형', element: '원소', damageMultiplier: '피해 배율', staggerValue: '브레이크 값', cooldownField: '재사용 대기시간', spiritCostField: 'SP 소모', skillDescription: '스킬 설명', baseEffectsMulti: '기본 효과(다중 선택)', enhancementName: '강화 그룹 이름', visibleMarker: '표시 마커', triggerText: '트리거 조건 텍스트', enhancementDescription: '강화 효과 설명', triggerEffectsMulti: '트리거 의존 효과(다중 선택)', outputEffectsMulti: '강화 출력 효과(다중 선택)', effectCategory: '효과 분류', effectId: '효과 ID(대문자 스네이크 표기)', effectDescription: '효과 설명', categoryName: '분류 이름', effectMultiHint: 'Ctrl / Cmd를 누른 채 다중 선택할 수 있습니다. 선택지는 effects.py에서 가져와 분류별로 표시합니다.', selectedTriggerEffects: '트리거 의존 효과 {count}개 선택됨', noTriggerEffects: '트리거 의존 효과를 선택하지 않았습니다', noSelectedEffects: '효과를 선택하지 않았습니다', unknownEffect: '알 수 없는 효과', unknownCurrentEffect: '현재 데이터의 알 수 없는 효과', inferredFromTriggerText: '트리거 텍스트에서 추론', clickCopySkillId: '클릭하여 스킬 ID 복사', clickCopyEffectId: '클릭하여 효과 ID 복사', openCharacterLocaleFile: '캐릭터 이름 언어 파일 열기', confirmDelete: '“{name}”을(를) 삭제하시겠습니까?\n저장 전에 .bak 백업을 자동 생성합니다.', copied: '복사됨: {text}', noTasks: '작업을 찾을 수 없습니다.\n먼저 프로젝트 경로를 설정하세요.', collapseParameters: '매개변수 접기', running: '실행 중…', oneTimeTask: '일회성', triggerTask: '트리거', enableTrigger: '사용', triggerDisabled: '사용 안 함', triggerArmed: '사용 설정됨', triggerEnqueued: '대기열 등록됨', triggerPolling: '폴링 중', taskQueued: '대기 중', taskRunning: '실행 중', executorIdle: '실행기 중지됨', executorConnecting: '실행기 시작 중…', executorRunning: '실행기 실행 중 · 트리거 작업 {count}개 대기열 등록됨', executorPaused: '일시중지됨(폴링과 작업 모두 중단)', startExecutor: '실행기 시작', stopExecutor: '실행기 닫기', stopCurrent: '현재 작업 중지', configGroup: '설정 그룹', commonParameters: '공통 매개변수', groupParameters: '그룹 매개변수', childTaskConfig: '하위 작업 설정', otherParameters: '기타 매개변수', launchSettings: '실행 설정', saved: '자동 저장됨', reset: '초기화', noConfigParameters: '설정 가능한 매개변수가 없습니다(schema 미준비 또는 default_config 없음).', schemaFailed: '이 작업의 schema 수집 실패: {error}', holdCtrlMulti: 'Ctrl을 누른 채 다중 선택', currentValue: '{value}(현재)', selectedOptionsHint: '선택 가능 값: {values}', structuredJsonHint: '조건 객체와 작업 시퀀스를 지원하는 JSON 배열입니다.', debugOverlay: '디버그 오버레이', debugOverlayHint: '작업 시작 시 자동으로 적용되며, 실행 중에도 즉시 전환할 수 있습니다. 활성화하면 draw_boxes로 그려진 박스가 게임 창 위에 표시됩니다(GDI 오버레이).', toolboxGameSection: '게임 연결', toolboxConnectGame: '게임 연결', toolboxDisconnect: '연결 해제', toolboxGameConnected: '연결됨: {title}(PID {pid})', toolboxGameNotConnected: '연결되지 않았습니다. 연결하면 작업 시작 시 해당 창을 우선 재사용합니다. 게임이 실행 중이 아니면 자동으로 시작합니다.', availableOptions: '사용 가능한 옵션', clickOptionToAdd: '옵션을 클릭해 추가하세요.', selectedOptions: '선택된 목록', searchOptions: '옵션 검색…', moveUp: '위로', moveDown: '아래로', removeItem: '제거', confirm: '확인', addValue: '값 추가', taskPaused: '작업이 일시중지되었습니다. 재개를 클릭해 계속하세요.', taskResumed: '작업이 재개되었습니다.', stopping: '작업을 중지하는 중…', taskStopped: '작업이 중지되었습니다. 자세한 내용은 출력을 확인하세요.', taskCompleted: '작업이 완료되었습니다. 자세한 내용은 출력을 확인하세요.', taskFailed: '작업이 오류로 종료되었습니다. 자세한 내용은 출력을 확인하세요.', templatesHint: '클릭=삽입 · 더블클릭=복사 · 오른쪽 아래 버튼으로 바로 실행', templatesCount: '{shown}/{total} 템플릿', noTemplates: '템플릿을 찾을 수 없습니다.', noTemplateMatch: '“{query}”와 일치하는 템플릿이 없습니다.', viewOriginal: '위치를 강조 표시한 원본 이미지 보기', thumbnailStats: '미리 보기 {loaded}개 불러옴', thumbnailStatsWithFailures: '미리 보기 {loaded}개 불러옴 / {failed}개 실패', thumbnailLoadFailed: '미리 보기 불러오기 실패', templateSize: '크기: {width}×{height}', templateSource: '소스: {path}', + insertExpression: "표현식 삽입", + copyExpression: "표현식 복사", + editAnnotations: "주석 편집", + openSourceImage: "원본 이미지 열기", + templateAssetsHint: '이미지 또는 편집 버튼을 클릭하여 주석 편집기 열기', + assetsHint: '이미지 또는 편집 버튼을 클릭하여 박스를 편집하세요. 게시하면 런타임 파일에 반영됩니다.', }; const ES: WebviewStrings = { @@ -924,7 +949,13 @@ const ES: WebviewStrings = { assetSwapBoxes: '{count} anotaciones', assetSwapNoBoxes: 'Sin anotaciones', refresh: 'Actualizar', open: 'Abrir', add: 'Añadir', modify: 'Modificar', delete: 'Eliminar', cancel: 'Cancelar', save: 'Guardar', copy: 'Copiar', close: 'Cerrar', search: 'Buscar', loading: 'Cargando…', none: 'Ninguno', current: 'Actual', enabled: 'Activado', disabled: 'Desactivado', error: 'Error', warning: 'Advertencia', info: 'Información', charactersTitle: 'Gestor de personajes y habilidades', charactersTab: 'Personajes y habilidades', effectsTab: 'Índice de efectos', localesTab: 'Localización de nombres', issuesTab: 'Diagnóstico de datos', searchCharacters: 'Buscar personajes, habilidades, efectos o descripciones…', addSkill: 'Añadir habilidad', modifySkill: 'Modificar habilidad', addEnhancement: 'Añadir mejora', modifyEnhancement: 'Modificar mejora', addEffectCategory: 'Añadir categoría', addEffect: 'Añadir efecto', charactersMetric: 'Personajes', skillsMetric: 'Habilidades', enhancementsMetric: 'Mejoras', effectReferencesMetric: 'Referencias de efectos', effectDefinitionsMetric: 'Definiciones de efectos', valueLabel: 'Valor', loadFailed: 'Error al cargar', saveFailed: 'Error al guardar', taskTitle: 'Lanzador de tareas de ok-script', launch: 'Iniciar', stop: 'Detener', pause: 'Pausar', resume: 'Reanudar', parameters: 'Parámetros', toolboxOpenCharacterManager: 'Abrir gestor de personajes y habilidades', templatesTitle: 'Galería de plantillas', templatesSearch: 'Buscar nombres de 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).', - allStars: 'Todas las rarezas', allElements: 'Todos los elementos', allProfessions: 'Todas las profesiones', allSkillTypes: 'Todos los tipos de habilidad', enhancementOnly: 'Solo personajes con mejoras', issueOnly: 'Solo personajes con diagnósticos', characterCount: '{shown} / {total} personajes', skillsEnhancements: 'Habilidades / Mejoras', noCharacters: 'No hay personajes coincidentes', selectCharacter: 'Seleccione un personaje de la izquierda', unknownStar: 'Rareza sin definir', unknownElement: 'Elemento sin definir', unknownProfession: 'Profesión sin definir', unknownWeapon: 'Arma sin definir', skillsCount: '{count} habilidades', enhancementsCount: '{count} mejoras', openCharacterJson: 'Abrir JSON del personaje', skillsAndEnhancements: 'Habilidades y mejoras', syncedSkillLocked: 'Habilidad sincronizada · ID/nombre/tipo/elemento/descripción bloqueados', enhancedState: 'Mejorada', baseEffects: 'Efectos base', triggerEffects: 'Dependencias del disparador', outputEffects: 'Efectos de la mejora', visiblePulse: 'Pulso visible', multiplier: 'Multiplicador', stagger: 'Desequilibrio', cooldown: 'Reutilización', spiritCost: 'Coste de PH', noSkills: 'Este personaje no tiene datos de habilidades', searchEffects: 'Buscar ID de efecto, descripción, personaje o habilidad…', allCategories: 'Todas las categorías', allEffects: 'Todos los efectos', usedOnly: 'Solo usados', unusedOnly: 'Solo no usados', unknownOnly: 'Solo desconocidos', effectsCount: '{shown} / {total} efectos', noEffects: 'No hay efectos coincidentes', undefinedEffect: 'Efecto no definido', openDefinition: 'Abrir definición', moreUsages: '{count} referencias más', localizationMatrix: 'Matriz de localización de nombres', localizationHint: 'Las celdas vacías indican datos ausentes. Haga clic en un personaje para abrir el archivo de idioma.', characterIdColumn: 'Personaje / ID', missing: 'Ausente', searchIssues: 'Buscar diagnósticos…', allSeverities: 'Todas las gravedades', issuesCount: '{shown} / {total} diagnósticos', noIssuesMatch: 'No hay diagnósticos coincidentes', noIssues: 'No se encontraron problemas de datos', openSource: 'Abrir origen', readingCharacterData: 'Leyendo personajes, habilidades, efectos y datos de localización…', skillId: 'ID de habilidad', skillName: 'Nombre de habilidad', skillType: 'Tipo de habilidad', element: 'Elemento', damageMultiplier: 'Multiplicador de daño', staggerValue: 'Valor de desequilibrio', cooldownField: 'Reutilización', spiritCostField: 'Coste de PH', skillDescription: 'Descripción de habilidad', baseEffectsMulti: 'Efectos base (selección múltiple)', enhancementName: 'Nombre de mejora', visibleMarker: 'Marcador de visualización', triggerText: 'Texto de condición del disparador', enhancementDescription: 'Descripción de la mejora', triggerEffectsMulti: 'Dependencias del disparador (selección múltiple)', outputEffectsMulti: 'Efectos de la mejora (selección múltiple)', effectCategory: 'Categoría de efecto', effectId: 'ID de efecto (MAYÚSCULAS_CON_GUIONES_BAJOS)', effectDescription: 'Descripción del efecto', categoryName: 'Nombre de categoría', effectMultiHint: 'Mantenga Ctrl / Cmd para seleccionar varios elementos. Las opciones proceden de effects.py y se agrupan por categoría.', selectedTriggerEffects: '{count} dependencias seleccionadas', noTriggerEffects: 'No se seleccionaron dependencias del disparador', noSelectedEffects: 'No se seleccionaron efectos', unknownEffect: 'Efecto desconocido', unknownCurrentEffect: 'Efecto desconocido en los datos actuales', inferredFromTriggerText: 'Inferido del texto del disparador', clickCopySkillId: 'Haga clic para copiar el ID de habilidad', clickCopyEffectId: 'Haga clic para copiar el ID de efecto', openCharacterLocaleFile: 'Abrir archivo de idioma del nombre del personaje', confirmDelete: '¿Eliminar «{name}»?\nSe creará automáticamente una copia .bak antes de guardar.', copied: 'Copiado: {text}', noTasks: 'No se encontraron tareas.\nConfigure primero la ruta del proyecto.', collapseParameters: 'Contraer parámetros', running: 'En ejecución…', oneTimeTask: 'Una vez', triggerTask: 'Disparador', enableTrigger: 'Activar', triggerDisabled: 'Sin activar', triggerArmed: 'Activada', triggerEnqueued: 'En cola', triggerPolling: 'Sondeando', taskQueued: 'En espera', taskRunning: 'En ejecución', executorIdle: 'Ejecutor detenido', executorConnecting: 'Iniciando el ejecutor…', executorRunning: 'Ejecutor en marcha · {count} tareas de disparador en cola', executorPaused: 'En pausa (sondeo y tareas suspendidos)', startExecutor: 'Iniciar el ejecutor', stopExecutor: 'Cerrar el ejecutor', stopCurrent: 'Detener la tarea actual', configGroup: 'Grupo de configuración', commonParameters: 'Parámetros comunes', groupParameters: 'Parámetros del grupo', childTaskConfig: 'Configuración de subtareas', otherParameters: 'Otros parámetros', launchSettings: 'Configuración de inicio', saved: 'Guardado automáticamente', reset: 'Restablecer', noConfigParameters: 'No hay parámetros configurables (schema no preparado o sin default_config).', schemaFailed: 'No se pudo recopilar el schema de esta tarea: {error}', holdCtrlMulti: 'Mantenga Ctrl para seleccionar varios elementos', currentValue: '{value} (actual)', selectedOptionsHint: 'Valores disponibles: {values}', structuredJsonHint: 'Matriz JSON que admite objetos de condición y secuencias de acciones.', debugOverlay: 'Superposición de depuración', debugOverlayHint: 'Se adopta automáticamente al iniciar tareas y puede alternarse en vivo durante la ejecución. Si se activa, los recuadros dibujados con draw_boxes se muestran sobre la ventana del juego (superposición GDI).', toolboxGameSection: 'Conexión del juego', toolboxConnectGame: 'Conectar juego', toolboxDisconnect: 'Desconectar', toolboxGameConnected: 'Conectado: {title} (PID {pid})', toolboxGameNotConnected: 'Sin conexión. La ventana conectada se reutiliza primero al iniciar tareas; el juego se inicia automáticamente si no está en ejecución.', availableOptions: 'Opciones disponibles', clickOptionToAdd: 'Haga clic en una opción para añadirla.', selectedOptions: 'Opciones seleccionadas', searchOptions: 'Buscar opciones…', moveUp: 'Subir', moveDown: 'Bajar', removeItem: 'Quitar', confirm: 'Confirmar', addValue: 'Añadir valor', taskPaused: 'Tarea pausada. Haga clic en Reanudar para continuar.', taskResumed: 'Tarea reanudada.', stopping: 'Deteniendo la tarea…', taskStopped: 'Tarea detenida. Consulte Salida para obtener detalles.', taskCompleted: 'Tarea completada. Consulte Salida para obtener detalles.', taskFailed: 'La tarea terminó con un error. Consulte Salida para obtener detalles.', templatesHint: 'Clic=insertar · Doble clic=copiar · Pase sobre la miniatura y pulse 👁 para ver el origen', templatesCount: '{shown}/{total} plantillas', noTemplates: 'No se encontraron plantillas.', noTemplateMatch: 'No hay plantillas que coincidan con «{query}».', viewOriginal: 'Ver la imagen original con la posición resaltada', thumbnailStats: '{loaded} miniaturas cargadas', thumbnailStatsWithFailures: '{loaded} miniaturas cargadas / {failed} con error', thumbnailLoadFailed: 'Error al cargar la miniatura', templateSize: 'Tamaño: {width}×{height}', templateSource: 'Origen: {path}', + allStars: 'Todas las rarezas', allElements: 'Todos los elementos', allProfessions: 'Todas las profesiones', allSkillTypes: 'Todos los tipos de habilidad', enhancementOnly: 'Solo personajes con mejoras', issueOnly: 'Solo personajes con diagnósticos', characterCount: '{shown} / {total} personajes', skillsEnhancements: 'Habilidades / Mejoras', noCharacters: 'No hay personajes coincidentes', selectCharacter: 'Seleccione un personaje de la izquierda', unknownStar: 'Rareza sin definir', unknownElement: 'Elemento sin definir', unknownProfession: 'Profesión sin definir', unknownWeapon: 'Arma sin definir', skillsCount: '{count} habilidades', enhancementsCount: '{count} mejoras', openCharacterJson: 'Abrir JSON del personaje', skillsAndEnhancements: 'Habilidades y mejoras', syncedSkillLocked: 'Habilidad sincronizada · ID/nombre/tipo/elemento/descripción bloqueados', enhancedState: 'Mejorada', baseEffects: 'Efectos base', triggerEffects: 'Dependencias del disparador', outputEffects: 'Efectos de la mejora', visiblePulse: 'Pulso visible', multiplier: 'Multiplicador', stagger: 'Desequilibrio', cooldown: 'Reutilización', spiritCost: 'Coste de PH', noSkills: 'Este personaje no tiene datos de habilidades', searchEffects: 'Buscar ID de efecto, descripción, personaje o habilidad…', allCategories: 'Todas las categorías', allEffects: 'Todos los efectos', usedOnly: 'Solo usados', unusedOnly: 'Solo no usados', unknownOnly: 'Solo desconocidos', effectsCount: '{shown} / {total} efectos', noEffects: 'No hay efectos coincidentes', undefinedEffect: 'Efecto no definido', openDefinition: 'Abrir definición', moreUsages: '{count} referencias más', localizationMatrix: 'Matriz de localización de nombres', localizationHint: 'Las celdas vacías indican datos ausentes. Haga clic en un personaje para abrir el archivo de idioma.', characterIdColumn: 'Personaje / ID', missing: 'Ausente', searchIssues: 'Buscar diagnósticos…', allSeverities: 'Todas las gravedades', issuesCount: '{shown} / {total} diagnósticos', noIssuesMatch: 'No hay diagnósticos coincidentes', noIssues: 'No se encontraron problemas de datos', openSource: 'Abrir origen', readingCharacterData: 'Leyendo personajes, habilidades, efectos y datos de localización…', skillId: 'ID de habilidad', skillName: 'Nombre de habilidad', skillType: 'Tipo de habilidad', element: 'Elemento', damageMultiplier: 'Multiplicador de daño', staggerValue: 'Valor de desequilibrio', cooldownField: 'Reutilización', spiritCostField: 'Coste de PH', skillDescription: 'Descripción de habilidad', baseEffectsMulti: 'Efectos base (selección múltiple)', enhancementName: 'Nombre de mejora', visibleMarker: 'Marcador de visualización', triggerText: 'Texto de condición del disparador', enhancementDescription: 'Descripción de la mejora', triggerEffectsMulti: 'Dependencias del disparador (selección múltiple)', outputEffectsMulti: 'Efectos de la mejora (selección múltiple)', effectCategory: 'Categoría de efecto', effectId: 'ID de efecto (MAYÚSCULAS_CON_GUIONES_BAJOS)', effectDescription: 'Descripción del efecto', categoryName: 'Nombre de categoría', effectMultiHint: 'Mantenga Ctrl / Cmd para seleccionar varios elementos. Las opciones proceden de effects.py y se agrupan por categoría.', selectedTriggerEffects: '{count} dependencias seleccionadas', noTriggerEffects: 'No se seleccionaron dependencias del disparador', noSelectedEffects: 'No se seleccionaron efectos', unknownEffect: 'Efecto desconocido', unknownCurrentEffect: 'Efecto desconocido en los datos actuales', inferredFromTriggerText: 'Inferido del texto del disparador', clickCopySkillId: 'Haga clic para copiar el ID de habilidad', clickCopyEffectId: 'Haga clic para copiar el ID de efecto', openCharacterLocaleFile: 'Abrir archivo de idioma del nombre del personaje', confirmDelete: '¿Eliminar «{name}»?\nSe creará automáticamente una copia .bak antes de guardar.', copied: 'Copiado: {text}', noTasks: 'No se encontraron tareas.\nConfigure primero la ruta del proyecto.', collapseParameters: 'Contraer parámetros', running: 'En ejecución…', oneTimeTask: 'Una vez', triggerTask: 'Disparador', enableTrigger: 'Activar', triggerDisabled: 'Sin activar', triggerArmed: 'Activada', triggerEnqueued: 'En cola', triggerPolling: 'Sondeando', taskQueued: 'En espera', taskRunning: 'En ejecución', executorIdle: 'Ejecutor detenido', executorConnecting: 'Iniciando el ejecutor…', executorRunning: 'Ejecutor en marcha · {count} tareas de disparador en cola', executorPaused: 'En pausa (sondeo y tareas suspendidos)', startExecutor: 'Iniciar el ejecutor', stopExecutor: 'Cerrar el ejecutor', stopCurrent: 'Detener la tarea actual', configGroup: 'Grupo de configuración', commonParameters: 'Parámetros comunes', groupParameters: 'Parámetros del grupo', childTaskConfig: 'Configuración de subtareas', otherParameters: 'Otros parámetros', launchSettings: 'Configuración de inicio', saved: 'Guardado automáticamente', reset: 'Restablecer', noConfigParameters: 'No hay parámetros configurables (schema no preparado o sin default_config).', schemaFailed: 'No se pudo recopilar el schema de esta tarea: {error}', holdCtrlMulti: 'Mantenga Ctrl para seleccionar varios elementos', currentValue: '{value} (actual)', selectedOptionsHint: 'Valores disponibles: {values}', structuredJsonHint: 'Matriz JSON que admite objetos de condición y secuencias de acciones.', debugOverlay: 'Superposición de depuración', debugOverlayHint: 'Se adopta automáticamente al iniciar tareas y puede alternarse en vivo durante la ejecución. Si se activa, los recuadros dibujados con draw_boxes se muestran sobre la ventana del juego (superposición GDI).', toolboxGameSection: 'Conexión del juego', toolboxConnectGame: 'Conectar juego', toolboxDisconnect: 'Desconectar', toolboxGameConnected: 'Conectado: {title} (PID {pid})', toolboxGameNotConnected: 'Sin conexión. La ventana conectada se reutiliza primero al iniciar tareas; el juego se inicia automáticamente si no está en ejecución.', availableOptions: 'Opciones disponibles', clickOptionToAdd: 'Haga clic en una opción para añadirla.', selectedOptions: 'Opciones seleccionadas', searchOptions: 'Buscar opciones…', moveUp: 'Subir', moveDown: 'Bajar', removeItem: 'Quitar', confirm: 'Confirmar', addValue: 'Añadir valor', taskPaused: 'Tarea pausada. Haga clic en Reanudar para continuar.', taskResumed: 'Tarea reanudada.', stopping: 'Deteniendo la tarea…', taskStopped: 'Tarea detenida. Consulte Salida para obtener detalles.', taskCompleted: 'Tarea completada. Consulte Salida para obtener detalles.', taskFailed: 'La tarea terminó con un error. Consulte Salida para obtener detalles.', templatesHint: 'Clic=insertar · Doble clic=copiar · Use los botones inferiores derechos para acciones directas', templatesCount: '{shown}/{total} plantillas', noTemplates: 'No se encontraron plantillas.', noTemplateMatch: 'No hay plantillas que coincidan con «{query}».', viewOriginal: 'Ver la imagen original con la posición resaltada', thumbnailStats: '{loaded} miniaturas cargadas', thumbnailStatsWithFailures: '{loaded} miniaturas cargadas / {failed} con error', thumbnailLoadFailed: 'Error al cargar la miniatura', templateSize: 'Tamaño: {width}×{height}', templateSource: 'Origen: {path}', + insertExpression: "Insertar expresión", + copyExpression: "Copiar expresión", + editAnnotations: "Editar anotaciones", + openSourceImage: "Abrir imagen original", + templateAssetsHint: 'Pulse la imagen o el botón de editar para abrir anotaciones', + assetsHint: 'Pulse la imagen o el botón de editar para editar cuadros. Publicar escribe el archivo de ejecución.', }; const DICTIONARIES: Record = { diff --git a/src/pythonEditor.ts b/src/pythonEditor.ts new file mode 100644 index 0000000..fd80f7a --- /dev/null +++ b/src/pythonEditor.ts @@ -0,0 +1,34 @@ +import * as vscode from 'vscode'; +import { tr } from './localization'; + +/* ---------------- 最近 Python 编辑器跟踪(模块级单例) ---------------- */ + +let lastPythonEditor: vscode.TextEditor | undefined; +let editorTrackerReady = false; + +export function ensureEditorTracker(): void { + if (editorTrackerReady) return; + editorTrackerReady = true; + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (editor && editor.document.languageId === 'python') lastPythonEditor = editor; + }); + const cur = vscode.window.activeTextEditor; + if (cur && cur.document.languageId === 'python') lastPythonEditor = cur; +} + +/** 把文本插入最近活动的 Python 编辑器光标处;无可用编辑器时回退为复制 */ +export async function insertIntoPythonEditor(text: string): Promise { + ensureEditorTracker(); + let editor = lastPythonEditor; + if (!editor || editor.document.isClosed) { + const act = vscode.window.activeTextEditor; + editor = act && !act.document.isClosed && act.document.languageId === 'python' ? act : undefined; + } + if (!editor) { + await vscode.env.clipboard.writeText(text); + void vscode.window.showWarningMessage(tr('No Python editor is available; copied instead: {text}', { text })); + return; + } + await editor.insertSnippet(new vscode.SnippetString(text)); +} + diff --git a/src/templateAssetPanel.ts b/src/templateAssetPanel.ts index 0200f2e..05d9d1e 100644 --- a/src/templateAssetPanel.ts +++ b/src/templateAssetPanel.ts @@ -208,6 +208,12 @@ class AssetGalleryController { } break; } + case 'openSource': { + if (msg.imagePath && this.data.listImages().includes(msg.imagePath)) { + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(msg.imagePath)); + } + break; + } case 'screenshot': { // 从剪贴板粘贴截图 await this.handleScreenshot(msg.hardForeground); diff --git a/src/templatePanel.ts b/src/templatePanel.ts index a719342..c16fb4b 100644 --- a/src/templatePanel.ts +++ b/src/templatePanel.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; +import { ensureEditorTracker, insertIntoPythonEditor } from './pythonEditor'; import { FeatureData } from './featureData'; import { cropTemplateThumbFileAsync, openAnnotatedImage, THUMB_HEIGHT } from './pngCrop'; import { featureAliases } from './providers'; @@ -24,36 +25,6 @@ export function primaryFeatureAlias(): string { return aliases.length ? aliases[0] : 'fL'; } -/* ---------------- 最近 Python 编辑器跟踪(模块级单例) ---------------- */ - -let lastPythonEditor: vscode.TextEditor | undefined; -let editorTrackerReady = false; - -function ensureEditorTracker(): void { - if (editorTrackerReady) return; - editorTrackerReady = true; - vscode.window.onDidChangeActiveTextEditor((editor) => { - if (editor && editor.document.languageId === 'python') lastPythonEditor = editor; - }); - const cur = vscode.window.activeTextEditor; - if (cur && cur.document.languageId === 'python') lastPythonEditor = cur; -} - -/** 把文本插入最近活动的 Python 编辑器光标处;无可用编辑器时回退为复制 */ -async function insertIntoPythonEditor(text: string): Promise { - let editor = lastPythonEditor; - if (!editor || editor.document.isClosed) { - const act = vscode.window.activeTextEditor; - if (act && act.document.languageId === 'python') editor = act; - } - if (!editor) { - await vscode.env.clipboard.writeText(text); - void vscode.window.showWarningMessage(tr('No Python editor is available; copied instead: {text}', { text })); - return; - } - await editor.insertSnippet(new vscode.SnippetString(text)); -} - /* ---------------- 存活控制器注册表 ---------------- */ const liveControllers = new Set(); @@ -82,6 +53,7 @@ class GalleryController { private readonly isVisible: () => boolean, private readonly extensionUri: vscode.Uri, ) { + ensureEditorTracker(); liveControllers.add(this); this.disposables.push( webview.onDidReceiveMessage((msg) => { diff --git a/src/webviewHtml.ts b/src/webviewHtml.ts index 56e7821..69c0b5a 100644 --- a/src/webviewHtml.ts +++ b/src/webviewHtml.ts @@ -58,7 +58,8 @@ export function applySharedAssets( ).toString(true); return html .split('__SHARED_TOKENS_URI__').join(uri('tokens.css')) - .split('__SHARED_CONTROLS_URI__').join(uri('controls.css')); + .split('__SHARED_CONTROLS_URI__').join(uri('controls.css')) + .split('__SHARED_THUMBNAIL_ACTIONS_URI__').join(uri('thumbnailActions.js')); } /** From a92104f8d3112edb46948b19b851a7c5f88d2f72 Mon Sep 17 00:00:00 2001 From: AliceJump <149395013+AliceJump@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:55:10 +0800 Subject: [PATCH 5/8] docs: unify developer plugin scope and bilingual documentation --- .vscodeignore | 5 +- AGENT.md | 67 ------- AGENTS.md | 70 ++++++- DEVELOPMENT.en.md | 16 +- DEVELOPMENT.md | 12 +- README.en.md | 17 +- README.md | 11 +- RELEASING.en.md | 153 +++++++++++++++ RELEASING.md | 170 +---------------- docs/box-resources.en.md | 198 +++++++++++++++++++ docs/box-resources.md | 4 + docs/config-reads.en.md | 329 ++++++++++++++++++++++++++++++++ docs/config-reads.md | 26 +-- docs/design-system.en.md | 159 +++++++++++++++ docs/design-system.md | 8 +- docs/developer-tool-scope.en.md | 55 ++++++ docs/developer-tool-scope.md | 55 ++++++ docs/feature-parity.en.md | 55 ++++++ docs/feature-parity.md | 55 ++++++ docs/project-config.en.md | 310 ++++++++++++++++++++++++++++++ docs/project-config.md | 24 +-- docs/shared-core.en.md | 30 +++ docs/shared-core.md | 2 + jetbrains | 2 +- screenshots/README.en.md | 99 ++++++++++ screenshots/README.md | 114 +---------- scripts/README-gif.en.md | 126 ++++++++++++ scripts/README-gif.md | 149 ++------------- scripts/release/verify-l10n.js | 2 +- 29 files changed, 1780 insertions(+), 543 deletions(-) delete mode 100644 AGENT.md create mode 100644 RELEASING.en.md create mode 100644 docs/box-resources.en.md create mode 100644 docs/config-reads.en.md create mode 100644 docs/design-system.en.md create mode 100644 docs/developer-tool-scope.en.md create mode 100644 docs/developer-tool-scope.md create mode 100644 docs/feature-parity.en.md create mode 100644 docs/feature-parity.md create mode 100644 docs/project-config.en.md create mode 100644 docs/shared-core.en.md create mode 100644 screenshots/README.en.md create mode 100644 scripts/README-gif.en.md diff --git a/.vscodeignore b/.vscodeignore index bfc8888..03f4411 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -16,7 +16,7 @@ tsconfig.json scripts/** jetbrains/** .gitmodules -RELEASING.md +RELEASING*.md DEVELOPMENT.md DEVELOPMENT.en.md # 仓库自身的忽略规则对运行时毫无用处,打进 VSIX 只是噪音 @@ -33,7 +33,6 @@ logs/**/*.log .vscode/tasks.json .vscode/launch.json .vscode/ok-lang-hints-*.json -AGENT.md AGENTS.md **/agent.md **/agents.md @@ -47,7 +46,7 @@ python/test_*.py **/*.bak **/*.ok-script-toolkit.tmp # 审查/修复计划类的一次性工作目录(verification-<日期> at <时间>/ 等)。 -# 这些是开发过程产物,按 AGENT.md 的打包红线不得进产物。用通配而不是逐个列名 —— +# 这些是开发过程产物,按 AGENTS.md 的打包红线不得进产物。用通配而不是逐个列名 —— # 每次审查都会新建一个带时间戳的目录,逐个列名必然漏(2026-09-20 实测漏了 107 KB)。 verification-*/ verification */* diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index 95c956b..0000000 --- a/AGENT.md +++ /dev/null @@ -1,67 +0,0 @@ -# 开发规范:资源外置与插件打包 / Development Standards: External Resources & Plugin Packaging - -## 中文 - -### 资源外置 - -* **i18n 文案必须外置**,禁止在业务代码中硬编码可翻译文本。 -* **任何代码内使用的 HTML 资源必须外置**,禁止将 HTML 模板、HTML 片段直接嵌入 Python/JS/TS 等代码中。 -* 外置资源应放在项目约定的资源目录中,并通过统一的资源加载机制读取。 -* 新增功能时,如果涉及 i18n 或 HTML,必须同步新增对应的外部资源文件,不得为了方便直接写入代码。 -* 修改现有功能时,如发现已有代码内嵌 i18n 或 HTML,应优先一并迁移到外部资源。 - -### 插件打包 - -以下属于**仅开发使用的文件**,不得进入插件最终打包产物: - -* `AGENTS.md` -* `AGENT.md` -* 其他 `agent.md` / `agents.md` 文件 -* 项目开发说明、AI Agent 指令及相关开发辅助文件 -* 测试文件及其他明确标记为开发用途的资源 - -插件打包配置必须显式忽略上述文件,避免将开发文件随插件发布。 - -### 检查要求 - -提交代码前应检查: - -1. 是否存在新增的代码内嵌 i18n 文案。 -2. 是否存在新增的代码内嵌 HTML。 -3. 新增的 HTML/i18n 是否已经外置到规定目录。 -4. 插件打包产物中不得包含 `AGENT.md`、`AGENTS.md` 等 Agent 开发文件。 -5. 修改打包忽略规则后,应验证最终插件压缩包/产物中确实不存在这些文件。 - ---- - -## English - -### External Resources - -* **i18n strings must be externalized** — hardcoding translatable text in business code is prohibited. -* **Any HTML resources used in code must be externalized** — embedding HTML templates or HTML fragments directly into Python/JS/TS code is prohibited. -* Externalized resources should be placed in the project's designated resource directories and loaded through a unified resource loading mechanism. -* When adding new features that involve i18n or HTML, corresponding external resource files must be added simultaneously — do not embed them directly in code for convenience. -* When modifying existing features, if inline i18n or HTML is found, it should be migrated to external resources as a priority. - -### Plugin Packaging - -The following are **development-only files** and must not be included in the final plugin package: - -* `AGENTS.md` -* `AGENT.md` -* Other `agent.md` / `agents.md` files -* Project development documentation, AI Agent instructions, and related development auxiliary files -* Test files and other resources explicitly marked as development-purpose - -The plugin packaging configuration must explicitly ignore the above files to prevent development files from being shipped with the plugin. - -### Checklist - -Before committing code, verify: - -1. No new inline i18n strings have been added to code. -2. No new inline HTML has been added to code. -3. New HTML/i18n has been externalized to the designated directories. -4. The plugin package does not contain `AGENT.md`, `AGENTS.md`, or other Agent development files. -5. After modifying packaging ignore rules, verify the final plugin archive/products do not contain these files. diff --git a/AGENTS.md b/AGENTS.md index 7271c6b..79aed98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,66 @@ -# 仓库协作指引 +# 仓库协作指引与开发规范 -- 开发与打包约束见 [AGENT.md](AGENT.md)。 -- 处理主仓或 `jetbrains/` 子仓的 PR review 时,先读 - [.agents/skills/ok-script-pr-review/SKILL.md](.agents/skills/ok-script-pr-review/SKILL.md)。 - 两边是独立 Git 仓库,调用 GitHub API 时须明确仓库。 +## PR review + +处理主仓或 `jetbrains/` 子仓的 PR review 时,先读 +[.agents/skills/ok-script-pr-review/SKILL.md](.agents/skills/ok-script-pr-review/SKILL.md)。 +两边是独立 Git 仓库,调用 GitHub API 时须明确仓库。 + +等待 PR review 时使用技能提供的长时间等待脚本,复用已有终端进程,并让用户能在 Codex 侧边查看输出。不要默认改成隐藏进程,也不要为等待另外创建定时任务、heartbeat 或轮询 automation,除非用户明确要求。 + +## 仓库定位与功能边界 + +本仓库是面向 **ok-script 项目开发者的 IDE 插件**,用于开发、调试和资源制作。功能取舍以缩短开发步骤、准确操作当前本地代码、方便定位问题为目标。 + +**需要承担:** + +* 准确识别本地工作区当前的项目、解释器、任务注册、参数声明和资源入口;核对功能时以本地代码为准,历史版本与远程提交仅作参考。 +* 支持任务运行、触发任务启用、暂停、停止和当前参数试验,尽量沿用项目与框架的真实启动方式。 +* 隔离调试配置与项目实际配置,明确覆盖范围;用户主动编辑模板、框、角色等项目资源时,按该操作的既有保存语义写入。 +* 提供真实的保存结果、可确认的运行状态、日志与错误入口。项目路径、执行器、任务标识、配置来源等技术信息应清楚可达。 +* 支持截图、标注、模板与区域制作、代码引用、补全、预览和来源跳转;两端保持操作含义一致,界面紧凑、清晰、易操作。 +* 插件依赖的框架 API、任务元数据、配置定位或资源格式变化,导致当前功能读错、写错或不能运行时,修正插件的检测与调用入口。 + +**不需要承担:** + +* 业务项目的参数迁移、历史值恢复、跨任务配置搬运、账号覆盖迁移或旧业务版本兼容。这些由业务项目负责;项目启动链内已有逻辑可正常执行,插件不另建一套业务迁移机制。 +* 恢复业务项目已经移除、不再需要的参数。参数删除、任务拆分、参数归属调整和内部检测点更换属于正常内容改造;能正确读取当前声明时,不据此安排额外适配。 +* 把旧调试快照中的键当作当前任务仍支持的参数,或凭字段同名、旧任务关系自动推断并搬运历史值。 +* 默认扩展普通业务软件的账号生命周期、迁移恢复向导、完整健康看板等流程;也不为凑齐 UI 状态要求每个业务项目新增应用回执或观测协议。 + +插件仍负责**自己的**持久化格式、协议和缓存兼容。无法确认运行应用或等待原因时,如实说明并提供日志入口。优先保证当前调试链可靠,再改善开发效率与视觉体验。详细依据见 [开发者插件功能范围](docs/developer-tool-scope.md)。 + +## 协作说明文档 + +* 面向大模型的指令、技能和配套说明(包括 `AGENTS.md`、`SKILL.md`)每份文档只维护单文件、单语言版本,本仓库使用中文;不混写中英文正文,也不另建 `.en.md` 等翻译副本。 +* 除 Agent 指令、技能及技能配套说明外,所有文档必须同时维护中文与英文版本,分别放在 `名称.md` 与 `名称.en.md` 中;每个文件只保留对应语言的正文,更新时同步两份内容与相应链接。 +* 文件名、代码、API 名称和必要的原始界面文字保留原文,不视为双语正文。 + +## 资源外置 + +* **i18n 文案必须外置**,禁止在业务代码中硬编码可翻译文本。 +* **任何代码内使用的 HTML 资源必须外置**,禁止将 HTML 模板、HTML 片段直接嵌入 Python/JS/TS 等代码中。 +* 外置资源应放在项目约定的资源目录中,并通过统一的资源加载机制读取。 +* 新增功能时,如果涉及 i18n 或 HTML,必须同步新增对应的外部资源文件,不得为了方便直接写入代码。 +* 修改现有功能时,如发现已有代码内嵌 i18n 或 HTML,应优先一并迁移到外部资源。 + +## 插件打包 + +以下属于**仅开发使用的文件**,不得进入插件最终打包产物: + +* `AGENTS.md` +* 其他 `agent.md` / `agents.md` 文件 +* 项目开发说明、AI Agent 指令及相关开发辅助文件 +* 测试文件及其他明确标记为开发用途的资源 + +插件打包配置必须显式忽略上述文件,避免将开发文件随插件发布。 + +## 检查要求 + +提交代码前应检查: + +1. 是否存在新增的代码内嵌 i18n 文案。 +2. 是否存在新增的代码内嵌 HTML。 +3. 新增的 HTML/i18n 是否已经外置到规定目录。 +4. 插件打包产物中不得包含 `AGENTS.md` 等 Agent 开发文件。 +5. 修改打包忽略规则后,应验证最终插件压缩包/产物中确实不存在这些文件。 diff --git a/DEVELOPMENT.en.md b/DEVELOPMENT.en.md index 0f796d4..0fb0578 100644 --- a/DEVELOPMENT.en.md +++ b/DEVELOPMENT.en.md @@ -1,4 +1,4 @@ -# 开发指南 / Development Guide +# Development Guide
@@ -6,18 +6,16 @@
-本文档面向扩展开发者,包含项目结构、本地构建安装和发布流程。 - This document is for extension developers and covers the project structure, local build/installation, and release workflow. -The shared runtime scripts, protocol, and host boundaries are documented in [Shared core](docs/shared-core.md). +The shared runtime scripts, protocol, and host boundaries are documented in [Shared core](docs/shared-core.en.md); see [feature parity](docs/feature-parity.en.md) for current capabilities and limits. --- ## Project Structure ```text -src/ VS Code extension host TypeScript source (33 modules; entry points and major ones listed) +src/ VS Code extension host TypeScript source (entry points and major modules listed, not a complete inventory) projectConfig.ts Read side of the project convention file ok-script-toolkit.json (locate + cache + personal preference) projectConfigPure.ts Pure precedence-chain logic (no vscode dependency, unit-testable): parse + precedence + winning layer conventionSources.ts Data source for the "Project Convention vs My Settings" tracing panel @@ -62,7 +60,7 @@ media/ Per-Webview HTML/CSS/JS (loaded by the host via CS python/ Helper scripts shipped with the extension: task discovery, probing & execution (parse_config_tasks.py, probe_task_schemas.py, run_executor.py), plus game window capture & config probing for the template asset panel (capture_game_window.py, probe_window_config.py) python/tests/ Development-time Python regression tests (test_probe_*.py ×5 and test_run_executor_*.py ×4, 9 in total, all wired into npm test; plus _test_tmp.py, the shared per-suite temp-dir infrastructure); - excluded from VSIX / JetBrains JAR per AGENT.md packaging rules + excluded from VSIX / JetBrains JAR per AGENTS.md packaging rules jetbrains/ The JetBrains plugin's **separate public repository** (git submodule), with its own README / CI / release flow schemas/ JSON Schema for ok-script-toolkit.json (editor completion and validation) docs/ Design documents (config-reads overview, convention-file design, global UI design system, copy-pasteable example config) @@ -118,7 +116,7 @@ The `jetbrains/` directory contains a standalone Kotlin + IntelliJ Platform plug Drag **works here** — the two tool windows share the same JVM and use a custom `DataFlavor` to pass paths; note that `JPanel` has no built-in auto-drag-out, requiring manual `exportAsDrag` in `mouseDragged`. - Follow-up parity work from main-repo v1.9.0 → v1.13.0 (Swing-side spec at - [`jetbrains/docs/design-parity.md`](https://github.com/AliceJump/ok-script-toolkit-jetbrains/blob/main/docs/design-parity.md)): + [`jetbrains/docs/design-parity.md`](https://github.com/AliceJump/ok-script-toolkit-jetbrains/blob/main/docs/design-parity.en.md)): **global config takeover** (probe parses global config groups → parameter snapshots persisted → full snapshot injected via `OK_TOOLKIT_GCONFIG` at executor launch → live push via `gparams` while running), the health bar and the idle-state **run center** (current task / execution queue / trigger polling), @@ -131,7 +129,7 @@ cd jetbrains ./gradlew test buildPlugin verifyPluginStructure verifyPluginConfiguration ``` -Use `gradlew.bat` on Windows. The generated ZIP is at `jetbrains/build/distributions/` and can be installed via **Settings / Plugins / Install Plugin from Disk...** in a JetBrains IDE. For per-feature alignment status and remaining gaps, see [`jetbrains/docs/parity-review.md`](https://github.com/AliceJump/ok-script-toolkit-jetbrains/blob/main/docs/parity-review.md) (re-verified line by line against the code on 2026-09-21, baseline v1.8.0). +Use `gradlew.bat` on Windows. The generated ZIP is at `jetbrains/build/distributions/` and can be installed via **Settings / Plugins / Install Plugin from Disk...** in a JetBrains IDE. For per-feature alignment status and remaining gaps, see [`jetbrains/docs/parity-review.md`](https://github.com/AliceJump/ok-script-toolkit-jetbrains/blob/main/docs/parity-review.en.md) (historical baseline; see [current feature parity](docs/feature-parity.en.md)). ## Installation @@ -284,4 +282,4 @@ Add each one in the GitHub repo under **Settings → Secrets and variables → A When using VS Marketplace OIDC, also add `VSCE_USE_OIDC=true` under **Actions → Variables → New repository variable**; only enable after completing Marketplace Trusted Publishing policy. -See [RELEASING.md](RELEASING.md) for full token setup, signing key generation, and step-by-step release instructions. +See [RELEASING.md](RELEASING.en.md) for full token setup, signing key generation, and step-by-step release instructions. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 18483e3..727f183 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,4 +1,4 @@ -# 开发指南 / Development Guide +# 开发指南
@@ -8,16 +8,14 @@ 本文档面向扩展开发者,包含项目结构、本地构建安装和发布流程。 -两端共同维护的运行脚本、协议和宿主边界见 [共用核心](docs/shared-core.md)。 - -This document is for extension developers and covers the project structure, local build/installation, and release workflow. +两端共同维护的运行脚本、协议和宿主边界见 [共用核心](docs/shared-core.md),当前功能和限制见 [功能对齐表](docs/feature-parity.md)。 --- ## 项目结构 ```text -src/ VS Code 扩展宿主 TypeScript 源码(33 个模块,下列为入口与主要模块) +src/ VS Code 扩展宿主 TypeScript 源码(下列为入口与主要模块,非完整清单) projectConfig.ts 项目约定文件 ok-script-toolkit.json 的读盘侧(定位 + 缓存 + 个人偏好) projectConfigPure.ts 取值链纯逻辑(不依赖 vscode,可单测):解析 + 优先级 + 来源层 conventionSources.ts 「项目约定 vs 我的设置」溯源面板的数据源 @@ -62,7 +60,7 @@ media/ 每个外置 Webview 的 HTML/CSS/JS(宿主经 C python/ 随扩展发布的辅助脚本:任务发现、探测与执行(parse_config_tasks.py、probe_task_schemas.py、run_executor.py),以及模板素材面板的游戏窗口截图与配置探测(capture_game_window.py、probe_window_config.py) python/tests/ 开发期 Python 回归测试(test_probe_*.py ×5、test_run_executor_*.py ×4, 共 9 个,npm test 全部接入;另有三端共用的测试临时目录基建 _test_tmp.py); - 按 AGENT.md 打包规范不进 VSIX / JetBrains JAR + 按 AGENTS.md 打包规范不进 VSIX / JetBrains JAR jetbrains/ JetBrains 插件的**独立公开仓库**(git submodule),有自己的 README / CI / 发版流程 schemas/ ok-script-toolkit.json 的 JSON Schema(编辑器补全与校验) docs/ 设计文档(配置读取全景、项目约定文件设计、全局 UI 设计规范、可直接复制的示例配置) @@ -130,7 +128,7 @@ cd jetbrains ./gradlew test buildPlugin verifyPluginStructure verifyPluginConfiguration ``` -Windows 使用 `gradlew.bat`。生成的 ZIP 位于 `jetbrains/build/distributions/`,可在 JetBrains IDE 的 **Settings / Plugins / Install Plugin from Disk...** 中安装。逐功能的对齐状态与剩余差异见 [`jetbrains/docs/parity-review.md`](https://github.com/AliceJump/ok-script-toolkit-jetbrains/blob/main/docs/parity-review.md)(2026-09-21 已按代码逐条重核,基线 v1.8.0)。 +Windows 使用 `gradlew.bat`。生成的 ZIP 位于 `jetbrains/build/distributions/`,可在 JetBrains IDE 的 **Settings / Plugins / Install Plugin from Disk...** 中安装。逐功能的对齐状态与剩余差异见 [`jetbrains/docs/parity-review.md`](https://github.com/AliceJump/ok-script-toolkit-jetbrains/blob/main/docs/parity-review.md)(保留历史基线;当前实现见 [功能对齐表](docs/feature-parity.md))。 ## 安装 diff --git a/README.en.md b/README.en.md index b6d2981..90b988d 100644 --- a/README.en.md +++ b/README.en.md @@ -6,11 +6,8 @@ # ok-script Toolkit -**把 ok-script 的语言、OCR、模板、技能和任务数据,直接搬进 VS Code 的开发流程。** - **Bring ok-script's language keys, OCR fixes, templates, skills and task data straight into your VS Code workflow.** -语言键补全 · OCR 修正提示 · 模板浏览 · 任务启动 · 角色技能管理 Language key completion · OCR fix hints · Template browsing · Task launching · 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) @@ -26,8 +23,6 @@ Language key completion · OCR fix hints · Template browsing · Task launching --- -VS Code 扩展,为 ok-script 项目的 Python 开发提供语言键、OCR 修正、模板和技能效果的数据提示,同时内置模板浏览、任务启动和角色技能管理面板,让 ok-script 的语言、OCR、模板、技能和任务数据直接进入开发流程。 - 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. > [!TIP] @@ -51,6 +46,8 @@ For PyCharm / IntelliJ IDEA users, install the JetBrains version: [ok-script Too ## Features +See [feature and documentation parity](docs/feature-parity.en.md) for current host implementations and remaining differences. + | Module | Description | |---|---| | [Code development assistance](#code-development-assistance) | Complete and explain `self.lang`, OCR regex, and skill effect IDs in the editor | @@ -96,6 +93,10 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) Hover over `fL.give_gift` to see the cropped template image; type `fL.` to select from the template name list. +### Box Resources + +Box resource management reuses source annotation images and the COCO editor. Edit boxes, generate enclosing boxes from templates, and explicitly publish normalized position tables. The box gallery provides cropped previews, source navigation, and insertion/copy/completion/Hover for `self.pos` references. Working and runtime files are separate; business projects own game loading. See [Box Resource Design](docs/box-resources.en.md). + ### Temp Screenshots

@@ -239,10 +240,10 @@ Settings that take part in the precedence chain ↔ the field they map to: > The **`okScriptToolkit.showConventionSources`** command ("Project Convention vs My Settings") > lists which layer each effective value comes from and lets you revert to the project convention. -See [`docs/project-config.md`](docs/project-config.md) for the full field list and design notes, +See [`docs/project-config.md`](docs/project-config.en.md) for the full field list and design notes, and [`docs/ok-script-toolkit.example.json`](docs/ok-script-toolkit.example.json) for a copy-paste example. For **what the plugin actually reads at runtime, what each setting does, and the precedence rules**, -see [`docs/config-reads.md`](docs/config-reads.md) (six read-path types, per-item purpose, the invariants, and a troubleshooting list). +see [`docs/config-reads.md`](docs/config-reads.en.md) (six read-path types, per-item purpose, the invariants, and a troubleshooting list). ### Configuration Example @@ -307,6 +308,6 @@ If you just modified the extension's `package.json` configuration declarations, **Related** -[ok-script Toolkit for JetBrains](https://github.com/AliceJump/ok-script-toolkit-jetbrains) · [Development Guide](DEVELOPMENT.en.md) · [Release Process](RELEASING.md) +[ok-script Toolkit for JetBrains](https://github.com/AliceJump/ok-script-toolkit-jetbrains) · [Development Guide](DEVELOPMENT.en.md) · [Release Process](RELEASING.en.md)

diff --git a/README.md b/README.md index 78c1bd3..3439339 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,7 @@ **把 ok-script 的语言、OCR、模板、技能和任务数据,直接搬进 VS Code 的开发流程。** -**Bring ok-script's language keys, OCR fixes, templates, skills and task data straight into your VS Code workflow.** - 语言键补全 · OCR 修正提示 · 模板浏览 · 任务启动 · 角色技能管理 -Language key completion · OCR fix hints · Template browsing · Task launching · 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) @@ -28,8 +25,6 @@ Language key completion · OCR fix hints · Template browsing · Task launching VS Code 扩展,为 ok-script 项目的 Python 开发提供语言键、OCR 修正、模板和技能效果的数据提示,同时内置模板浏览、任务启动和角色技能管理面板,让 ok-script 的语言、OCR、模板、技能和任务数据直接进入开发流程。 -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. - > [!TIP] > 下面每个功能章节开头的演示图都是可点的——如果动图没加载出来,直接点开链接看。 @@ -51,6 +46,8 @@ PyCharm / IntelliJ IDEA 用户请装 JetBrains 版:[ok-script Toolkit for JetB ## 功能 +当前两端实现及保留差异见 [功能与文档对齐表](docs/feature-parity.md)。 + | 模块 | 一句话说明 | |---|---| | [代码开发辅助](#代码开发辅助) | 在编辑器内补全和解释 `self.lang`、OCR 正则与技能效果 ID | @@ -96,6 +93,10 @@ self.wait_click_feature(feature=fL.give_gift, time_out=10) 悬停 `fL.give_gift` 可查看对应模板裁剪图;输入 `fL.` 可从模板名称列表中选择。 +### 框资源 + +框资源管理复用标注原图与 COCO 编辑器,支持编辑框、从模板生成包围框,并显式发布归一化位置表。框画廊提供裁剪预览、来源定位及 `self.pos` 引用的插入、复制、补全和 Hover。工作文件与运行时文件分开,游戏中的加载由业务项目负责。详见 [框资源设计](docs/box-resources.md)。 + ### 临时截图

diff --git a/RELEASING.en.md b/RELEASING.en.md new file mode 100644 index 0000000..cc5c1a1 --- /dev/null +++ b/RELEASING.en.md @@ -0,0 +1,153 @@ +# Dual-Platform Tag Release + +[简体中文](RELEASING.md) | [English](RELEASING.en.md) + +Releases are coordinated by the parent repository `AliceJump/ok-script-toolkit`. Regular commits and manual runs do not publish; only the first push of a matching version tag triggers a Release: + +```text +vMAJOR.MINOR.PATCH +``` + +## One-time Configuration + +All Secrets are added to the parent repo: + +**Settings → Secrets and variables → Actions → New repository secret** + +### Visual Studio Marketplace + +It is recommended to configure **Trusted Publishing/OIDC** for Visual Studio Marketplace, avoiding long-lived Secrets: + +1. Go to the Visual Studio Marketplace Publisher/extension management page. +2. Add a Trusted Publishing policy for `AliceJump.ok-script-toolkit`. +3. Fill in GitHub repo as `AliceJump/ok-script-toolkit`, workflow as `release.yml`, and environment as required by the Marketplace page (or leave empty). +4. Add `VSCE_USE_OIDC=true` in the parent repo under **Settings → Secrets and variables → Actions → Variables**. +5. The tag workflow already has `id-token: write` granted; when `VSCE_PAT` is not configured and this variable is `true`, it will execute `vsce publish --oidc`. + +If continuing with PAT for now, add Secret: `VSCE_PAT` + +1. Open the Azure DevOps Personal Access Tokens page. +2. Create a new Token, selecting **All accessible organizations** for Organization. +3. Select **Custom defined**, expand all permissions, and check only **Marketplace → Manage**. +4. Copy the Token immediately after creation and save as `VSCE_PAT`. +5. Ensure the account that created the Token is a member of Visual Studio Marketplace Publisher `AliceJump`. + +The repo already has this Secret configured as a fallback before OIDC setup is complete. Microsoft has announced global PATs will retire on 2026-12-01; migration to Trusted Publishing/OIDC should be done as soon as possible. + +### JetBrains Marketplace First Listing + +The first time requires manual plugin creation: + +1. Log in to https://plugins.jetbrains.com/author/me. +2. Select **Add new plugin**. +3. Build the ZIP locally: `cd jetbrains && ./gradlew buildPlugin`. +4. Upload `build/distributions/ok-script-toolkit-jetbrains-.zip`. +5. Confirm the Plugin XML ID is `com.alicejump.okscripttoolkit`, complete license, source code, issue tracker info, and submit for review. + +After the first successful creation, the tag workflow can upload subsequent versions via API. + +### JetBrains Release Token + +Secret: `JETBRAINS_TOKEN` + +1. Open https://plugins.jetbrains.com/author/me/tokens. +2. Select **Generate Token** and enter a name. +3. Copy the permanent Token immediately (shown only once). +4. Save as parent repo Secret `JETBRAINS_TOKEN`. + +### JetBrains Signing Keys + +Secrets: + +- `JETBRAINS_PRIVATE_KEY` +- `JETBRAINS_PRIVATE_KEY_PASSWORD` +- `JETBRAINS_CERTIFICATE_CHAIN` + +Generate using OpenSSL: + +```bash +openssl genpkey -aes-256-cbc -algorithm RSA \ + -out private_encrypted.pem -pkeyopt rsa_keygen_bits:4096 +openssl rsa -in private_encrypted.pem -out private.pem +openssl req -key private.pem -new -x509 -days 365 -out chain.crt +``` + +- `JETBRAINS_PRIVATE_KEY`: Full text of `private.pem`. +- `JETBRAINS_PRIVATE_KEY_PASSWORD`: Password set in the first command and used in the second. +- `JETBRAINS_CERTIFICATE_CHAIN`: Full text of `chain.crt`. + +GitHub Secrets support multi-line text; you can paste PEM/CRT full text directly, or Base64-encode to a single line first. Never commit private keys, certificates, or passwords. Generate new certificates and update corresponding Secrets before expiration. + +## Each Release + +> **Prefer the one-shot script**: `npm run release -- --minor` (or `sh scripts/release.sh --minor`; +> Windows can use `scripts/release.ps1`). It performs every step below automatically — syncing the +> seven version locations, verifying, committing/pushing sub-repo then parent, and finally tagging. +> Add `--dry-run` to preview first. The script only runs on the `main` branch +> (required for both the parent repo and the jetbrains submodule) and refuses +> otherwise — the release commit lands on whatever branch it runs from (hit on v1.12.0). +> If releasing manually, follow the order below exactly and +> **do not skip any step**. + +On Windows, `scripts/release.ps1` first stashes uncommitted changes separately in the +parent and JetBrains repositories, including untracked files but excluding ignored files. +The release uses committed code; automatic version increments use the version in `HEAD`. +It attempts to restore this run's stashes and the original staging state afterward, +without touching existing stashes. Conflicting stashes are retained and their commit IDs +are printed. Uncommitted version changes left by a failed release are stashed separately; +successful commits or pushes are not rolled back. PowerShell uses `-DryRun` to preview +without stashing or releasing. The Shell script still requires clean workspaces. + +Run `node scripts/test_release.js` to verify the PowerShell release flow. It requires +`git`, `node`, and `pwsh`, and uses temporary repositories and local remotes only. + +For example, releasing `0.6.0`: + +```bash +# 1. Sync all seven version locations: package.json, package-lock.json, jetbrains/gradle.properties +# and all four README version badges (missing any one causes Release validation failure) +npm run version:sync -- 0.6.0 + +# 2. Verify +npm test +cd jetbrains +./gradlew test buildPlugin verifyPluginStructure verifyPluginConfiguration +cd .. + +# 3. Commit sub-repo version changes first +cd jetbrains +git add . +git commit -m "chore(release): prepare v0.6.0" +git push origin main +cd .. + +# 4. Then commit parent repo version, README badge, and new submodule pointer +# Note: README.md must be committed together — version:sync modifies its badge, +# and verify-version.js checks it in the tag pipeline (a missing commit in 2026-09 +# caused v1.6.0 validation failure) +npm run verify:version +git add package.json package-lock.json README.md jetbrains +git commit -m "chore(release): prepare v0.6.0" +git push origin main + +# 5. The only release action: create and push a new tag +git tag -a v0.6.0 -m "Release v0.6.0" +git push origin v0.6.0 +``` + +Tag release proceeds as: + +1. Check out the parent repo and pinned JetBrains submodule commit. +2. Validate all seven version locations (`package.json`, `package-lock.json`, `jetbrains/gradle.properties`, and all four README badges) match the tag exactly. +3. Test and build VSIX. +4. Test, validate, build, and sign the JetBrains ZIP using the Secrets. +5. Create a GitHub Release with both installers attached. +6. Publish to Visual Studio Marketplace if `VSCE_PAT` is present. +7. Publish to JetBrains Marketplace if complete JetBrains Token and signing Secrets are present. + +## Failure Handling + +- Do not move or force-push a tag that has already been pushed. +- When a tag build fails, fix the code, bump the patch version (e.g., `0.6.0` → `0.6.1`), and push a new tag. +- Tags and GitHub Releases are not reusable; both Marketplaces also reject duplicate versions. +- If only Marketplace Secrets are missing, the GitHub Release is still created with both offline installers available. diff --git a/RELEASING.md b/RELEASING.md index 32de944..557b520 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,8 +1,8 @@ -# 双端标签发布 / Dual-Platform Tag Release +# 双端标签发布 -发布由父仓库 `AliceJump/ok-script-toolkit` 统一协调。普通提交和手动运行不会发布;只有首次推送匹配版本的标签会运行 Release: +[简体中文](RELEASING.md) | [English](RELEASING.en.md) -Releases are coordinated by the parent repo `AliceJump/ok-script-toolkit`. Regular commits and manual runs do not publish; only a first-time push of a matching version tag triggers a Release: +发布由父仓库 `AliceJump/ok-script-toolkit` 统一协调。普通提交和手动运行不会发布;只有首次推送匹配版本的标签会运行 Release: ```text vMAJOR.MINOR.PATCH @@ -10,15 +10,13 @@ vMAJOR.MINOR.PATCH --- -## 中文 - -### 一次性配置 +## 一次性配置 所有 Secret 都添加到父仓库: **Settings → Secrets and variables → Actions → New repository secret** -#### Visual Studio Marketplace +### Visual Studio Marketplace 推荐配置 Visual Studio Marketplace 的 **Trusted Publishing/OIDC**,无需保存长期 Secret: @@ -38,7 +36,7 @@ vMAJOR.MINOR.PATCH 目前仓库已经配置该 Secret,可作为 OIDC 配置完成前的回退。Microsoft 已宣布全局 PAT 将于 2026-12-01 退役,应尽快迁移到 Trusted Publishing/OIDC。 -#### JetBrains Marketplace 首次上架 +### JetBrains Marketplace 首次上架 第一次必须手动创建插件: @@ -50,7 +48,7 @@ vMAJOR.MINOR.PATCH 第一次创建成功之后,标签工作流才能通过 API 上传后续版本。 -#### JetBrains 发布 Token +### JetBrains 发布 Token Secret:`JETBRAINS_TOKEN` @@ -59,7 +57,7 @@ Secret:`JETBRAINS_TOKEN` 3. 立即复制只显示一次的永久 Token。 4. 保存为父仓库 Secret `JETBRAINS_TOKEN`。 -#### JetBrains 签名密钥 +### JetBrains 签名密钥 Secrets: @@ -82,7 +80,7 @@ openssl req -key private.pem -new -x509 -days 365 -out chain.crt GitHub Secret 支持多行文本,可直接粘贴 PEM/CRT 全文;也可先 Base64 编码为单行。不要提交私钥、证书或密码。证书到期前需生成新证书并更新对应 Secrets。 -### 每次发布 +## 每次发布 > **推荐用一键脚本**:`npm run release -- --minor`(或 `sh scripts/release.sh --minor`, > Windows 可用 `scripts/release.ps1`)。它会自动完成下面全部步骤——同步七处版本、 @@ -144,157 +142,9 @@ git push origin v0.6.0 6. 有 `VSCE_PAT` 时发布 Visual Studio Marketplace。 7. 有完整 JetBrains Token 和签名 Secrets 时发布 JetBrains Marketplace。 -### 失败处理 +## 失败处理 - 不要移动或强制覆盖已经推送的标签。 - 标签构建失败时,修复后提升补丁版本,例如从 `0.6.0` 改为 `0.6.1`,再推送新标签。 - 标签和 GitHub Release 都不可复用;两个 Marketplace 也拒绝重复版本。 - 如果只缺 Marketplace Secret,GitHub Release 仍会创建并提供两个离线安装包。 - ---- - -## English - -### One-time Configuration - -All Secrets are added to the parent repo: - -**Settings → Secrets and variables → Actions → New repository secret** - -#### Visual Studio Marketplace - -It is recommended to configure **Trusted Publishing/OIDC** for Visual Studio Marketplace, avoiding long-lived Secrets: - -1. Go to the Visual Studio Marketplace Publisher/extension management page. -2. Add a Trusted Publishing policy for `AliceJump.ok-script-toolkit`. -3. Fill in GitHub repo as `AliceJump/ok-script-toolkit`, workflow as `release.yml`, and environment as required by the Marketplace page (or leave empty). -4. Add `VSCE_USE_OIDC=true` in the parent repo under **Settings → Secrets and variables → Actions → Variables**. -5. The tag workflow already has `id-token: write` granted; when `VSCE_PAT` is not configured and this variable is `true`, it will execute `vsce publish --oidc`. - -If continuing with PAT for now, add Secret: `VSCE_PAT` - -1. Open the Azure DevOps Personal Access Tokens page. -2. Create a new Token, selecting **All accessible organizations** for Organization. -3. Select **Custom defined**, expand all permissions, and check only **Marketplace → Manage**. -4. Copy the Token immediately after creation and save as `VSCE_PAT`. -5. Ensure the account that created the Token is a member of Visual Studio Marketplace Publisher `AliceJump`. - -The repo already has this Secret configured as a fallback before OIDC setup is complete. Microsoft has announced global PATs will retire on 2026-12-01; migration to Trusted Publishing/OIDC should be done as soon as possible. - -#### JetBrains Marketplace First Listing - -The first time requires manual plugin creation: - -1. Log in to https://plugins.jetbrains.com/author/me. -2. Select **Add new plugin**. -3. Build the ZIP locally: `cd jetbrains && ./gradlew buildPlugin`. -4. Upload `build/distributions/ok-script-toolkit-jetbrains-.zip`. -5. Confirm the Plugin XML ID is `com.alicejump.okscripttoolkit`, complete license, source code, issue tracker info, and submit for review. - -After the first successful creation, the tag workflow can upload subsequent versions via API. - -#### JetBrains Release Token - -Secret: `JETBRAINS_TOKEN` - -1. Open https://plugins.jetbrains.com/author/me/tokens. -2. Select **Generate Token** and enter a name. -3. Copy the permanent Token immediately (shown only once). -4. Save as parent repo Secret `JETBRAINS_TOKEN`. - -#### JetBrains Signing Keys - -Secrets: - -- `JETBRAINS_PRIVATE_KEY` -- `JETBRAINS_PRIVATE_KEY_PASSWORD` -- `JETBRAINS_CERTIFICATE_CHAIN` - -Generate using OpenSSL: - -```bash -openssl genpkey -aes-256-cbc -algorithm RSA \ - -out private_encrypted.pem -pkeyopt rsa_keygen_bits:4096 -openssl rsa -in private_encrypted.pem -out private.pem -openssl req -key private.pem -new -x509 -days 365 -out chain.crt -``` - -- `JETBRAINS_PRIVATE_KEY`: Full text of `private.pem`. -- `JETBRAINS_PRIVATE_KEY_PASSWORD`: Password set in the first command and used in the second. -- `JETBRAINS_CERTIFICATE_CHAIN`: Full text of `chain.crt`. - -GitHub Secrets support multi-line text; you can paste PEM/CRT full text directly, or Base64-encode to a single line first. Never commit private keys, certificates, or passwords. Generate new certificates and update corresponding Secrets before expiration. - -### Each Release - -> **Prefer the one-shot script**: `npm run release -- --minor` (or `sh scripts/release.sh --minor`; -> Windows can use `scripts/release.ps1`). It performs every step below automatically — syncing the -> seven version locations, verifying, committing/pushing sub-repo then parent, and finally tagging. -> Add `--dry-run` to preview first. The script only runs on the `main` branch -> (required for both the parent repo and the jetbrains submodule) and refuses -> otherwise — the release commit lands on whatever branch it runs from (hit on v1.12.0). -> If releasing manually, follow the order below exactly and -> **do not skip any step**. - -On Windows, `scripts/release.ps1` first stashes uncommitted changes separately in the -parent and JetBrains repositories, including untracked files but excluding ignored files. -The release uses committed code; automatic version increments use the version in `HEAD`. -It attempts to restore this run's stashes and the original staging state afterward, -without touching existing stashes. Conflicting stashes are retained and their commit IDs -are printed. Uncommitted version changes left by a failed release are stashed separately; -successful commits or pushes are not rolled back. PowerShell uses `-DryRun` to preview -without stashing or releasing. The Shell script still requires clean workspaces. - -Run `node scripts/test_release.js` to verify the PowerShell release flow. It requires -`git`, `node`, and `pwsh`, and uses temporary repositories and local remotes only. - -For example, releasing `0.6.0`: - -```bash -# 1. Sync all seven version locations: package.json, package-lock.json, jetbrains/gradle.properties -# and all four README version badges (missing any one causes Release validation failure) -npm run version:sync -- 0.6.0 - -# 2. Verify -npm test -cd jetbrains -./gradlew test buildPlugin verifyPluginStructure verifyPluginConfiguration -cd .. - -# 3. Commit sub-repo version changes first -cd jetbrains -git add . -git commit -m "chore(release): prepare v0.6.0" -git push origin main -cd .. - -# 4. Then commit parent repo version, README badge, and new submodule pointer -# Note: README.md must be committed together — version:sync modifies its badge, -# and verify-version.js checks it in the tag pipeline (a missing commit in 2026-09 -# caused v1.6.0 validation failure) -npm run verify:version -git add package.json package-lock.json README.md jetbrains -git commit -m "chore(release): prepare v0.6.0" -git push origin main - -# 5. The only release action: create and push a new tag -git tag -a v0.6.0 -m "Release v0.6.0" -git push origin v0.6.0 -``` - -Tag release proceeds as: - -1. Check out the parent repo and pinned JetBrains submodule commit. -2. Validate all seven version locations (`package.json`, `package-lock.json`, `jetbrains/gradle.properties`, and all four README badges) match the tag exactly. -3. Test and build VSIX. -4. Test, validate, build, and sign the JetBrains ZIP using the Secrets. -5. Create a GitHub Release with both installers attached. -6. Publish to Visual Studio Marketplace if `VSCE_PAT` is present. -7. Publish to JetBrains Marketplace if complete JetBrains Token and signing Secrets are present. - -### Failure Handling - -- Do not move or force-push a tag that has already been pushed. -- When a tag build fails, fix the code, bump the patch version (e.g., `0.6.0` → `0.6.1`), and push a new tag. -- Tags and GitHub Releases are not reusable; both Marketplaces also reject duplicate versions. -- If only Marketplace Secrets are missing, the GitHub Release is still created with both offline installers available. diff --git a/docs/box-resources.en.md b/docs/box-resources.en.md new file mode 100644 index 0000000..7f61e2f --- /dev/null +++ b/docs/box-resources.en.md @@ -0,0 +1,198 @@ +# Box Resource Design + +[简体中文](box-resources.md) | [English](box-resources.en.md) + +Local status, 2026-10-01: both hosts have box editing, publication, runtime galleries, completion, and source preview entry points; see [feature parity](feature-parity.en.md). This document retains data contracts and implementation order. Section 1 describes the pre-change foundation; section 10 is not a current TODO list. Actual project `ScreenPosition` loading is outside this repository's acceptance. + +Box management follows template management, with two resources. The annotation working copy is edited by the plugin only. The runtime copy is a published position table read by box management and `self.pos` completion. The game uses it only after the business project's `ScreenPosition` loads it; current ok-framework template matching still reads only `template_matching.coco_feature_json`. Reuse existing images, canvas, and visibility handling. + +This document is an implementation reference. The real call in ok-neverness-to-everness (ok-nte) is `self.pos.screen.main_viewport.to_box()`, not `self.pos.main_viewport`; there is no `screen_pos`. + +## 1. Existing Capabilities + +The toolkit already separates two template paths. + +| | Annotation resource | Runtime resource | +|---|---|---| +| File | `/coco_annotations.json`; default directory `ok_templates` | `config.py`'s `template_matching.coco_feature_json`; otherwise probe `assets/coco_annotations.json` and `ok_tasks/assets/coco_annotations.json` | +| Readers/writers | Annotation management (asset panel) | Template gallery, completion, Hover; ok-framework template matching | +| Transfer | Explicit Export to assets; editing does not automatically write runtime files | | + +The annotation editor (`AnnotationDialog` / `media/annotationPanel`) already supports selection, dragging, eight-direction resizing, add/delete, zoom, pan, undo, copy/paste, and image navigation. Memory uses pixel `xywh`; disk uses COCO `bbox`. `NormalizedBox` only serves coordinate copying: it is neither persisted nor ok-script's `Box`. + +Previously, the editor had no annotation list or visibility controls. Hiding and deletion were the same operation. + +The template gallery reads runtime COCO only. A click inserts `fL.`. Region previews dynamically crop the source image with pixel bbox; cache keys combine image-content hash and bbox. Quick Documentation and Hover share that crop. JetBrains lookup entries show dimensions on the right; VS Code adds the image to documentation when resolving a completion entry. + +This repository has no business Point resource or position table. + +## 2. ok-nte pos / Box + +The handwritten Python position table lives in `src/scene/`. + +- `self.pos` is a `PositionMap`, created in `BaseNTETask`. +- `self.pos.screen` contains rectangles: currently `center`, `dialog_icon`, and `main_viewport`, using four screen-relative `(left, top, right, bottom)` values. +- `self.pos.panels.*` contains two-value click points, unpacked with `*self.pos.panels.esc.mail`. Calling `to_box()` on a point raises an error. +- `ScreenRatio.to_box()` calls `box_of_screen(..., hcenter=True)` to create a pixel `ok.Box` for the current captured frame. `hcenter` applies at call time and must not enter resources. +- A descriptor sets `Box.name` to `ScreenPosition.` for debugging only. Search regions are not matched by that name. +- Approximately 73 task-private `box_of_screen(...)` calls explicitly remain outside the position table; box resources do not absorb them. +- Runtime does not depend on the IDE plugin. + +## 3. Two Box Resources + +| | Templates | Boxes | +|---|---|---| +| Annotation resource | `