镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat: 增强剧集提取失败项定位与引导
这个提交包含在:
父节点
4df5375b92
当前提交
2559ef127f
@ -12,3 +12,5 @@
|
|||||||
- 2026-03-25:`config-panel` 将 `activeRuleConfig` 的同步逻辑从 `updated()` 调整到 `willUpdate()`,避免 Lit 在测试与运行时发出“update completed 后又触发 update”的低效更新警告。
|
- 2026-03-25:`config-panel` 将 `activeRuleConfig` 的同步逻辑从 `updated()` 调整到 `willUpdate()`,避免 Lit 在测试与运行时发出“update completed 后又触发 update”的低效更新警告。
|
||||||
- 2026-03-25:多语言文案已补齐到 `zh_CN` / `zh_TW` / `en`,并确认占位符必须使用 Chrome i18n 兼容的 `$1` 形式,不能写成 `{0}`。
|
- 2026-03-25:多语言文案已补齐到 `zh_CN` / `zh_TW` / `en`,并确认占位符必须使用 Chrome i18n 兼容的 `$1` 形式,不能写成 `{0}`。
|
||||||
- 2026-03-25:完整验证结果:`npm run typecheck` 通过,`npx vitest run` 通过(36 files / 263 tests);`npm run lint` 通过但仓库内仍存在大量历史 warning,本次未扩散为新的 lint error。
|
- 2026-03-25:完整验证结果:`npm run typecheck` 通过,`npx vitest run` 通过(36 files / 263 tests);`npm run lint` 通过但仓库内仍存在大量历史 warning,本次未扩散为新的 lint error。
|
||||||
|
- 2026-03-25:继续完成“剧集提取失败态增强”:`suggestedFailureFileId` 现在真正用于右侧预览的失败项高亮与自动滚动定位,避免“点了首个失败项但用户还要自己找”的断层体验。
|
||||||
|
- 2026-03-25:为失败项增加内联引导文案 `episode_assist_focus_hint`,明确告诉用户可以直接点整段/片段继续修正,再次预览。
|
||||||
|
|||||||
@ -11,3 +11,5 @@
|
|||||||
- 2026-03-25:完成 `preview-panel` / `virtual-preview-list` / `file-selector-panel` 的辅助回填接线,实现从预览项整段回填、片段点选、失败项快捷修正、执行成功后自动记录 recent preset。
|
- 2026-03-25:完成 `preview-panel` / `virtual-preview-list` / `file-selector-panel` 的辅助回填接线,实现从预览项整段回填、片段点选、失败项快捷修正、执行成功后自动记录 recent preset。
|
||||||
- 2026-03-25:补齐 `zh_CN` / `zh_TW` / `en` 新文案,并新增 4 个单测文件覆盖 config-panel、preview-panel、file-selector-panel 的新交互。
|
- 2026-03-25:补齐 `zh_CN` / `zh_TW` / `en` 新文案,并新增 4 个单测文件覆盖 config-panel、preview-panel、file-selector-panel 的新交互。
|
||||||
- 2026-03-25:完成验证:`npm run typecheck` ✅,`npx vitest run` ✅,`npm run lint` ✅(仅剩历史 warning)。
|
- 2026-03-25:完成验证:`npm run typecheck` ✅,`npx vitest run` ✅,`npm run lint` ✅(仅剩历史 warning)。
|
||||||
|
- 2026-03-25:完成下一批失败态增强:右侧预览支持根据 `suggestedFailureFileId` 自动高亮并滚动定位失败项,且在整段/片段回填成功后清除聚焦态,避免提示残留。
|
||||||
|
- 2026-03-25:补充失败态引导文案与测试,验证通过:`typecheck` ✅,`vitest` 全量 266 tests ✅,`lint` ✅(仅历史 warning)。
|
||||||
|
|||||||
@ -570,12 +570,16 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
|
|
||||||
private handleAssistApplyFullName(e: CustomEvent<{ fileId: string; fileName: string }>): void {
|
private handleAssistApplyFullName(e: CustomEvent<{ fileId: string; fileName: string }>): void {
|
||||||
this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.fileName);
|
this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.fileName);
|
||||||
this.setAssistState({ sampleFileId: e.detail.fileId, sampleFileName: e.detail.fileName });
|
this.setAssistState({
|
||||||
|
sampleFileId: e.detail.fileId,
|
||||||
|
sampleFileName: e.detail.fileName,
|
||||||
|
suggestedFailureFileId: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAssistApplySegment(e: CustomEvent<{ fileId: string; segment: string }>): void {
|
private handleAssistApplySegment(e: CustomEvent<{ fileId: string; segment: string }>): void {
|
||||||
this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.segment);
|
this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.segment);
|
||||||
this.setAssistState({ sampleFileId: e.detail.fileId });
|
this.setAssistState({ sampleFileId: e.detail.fileId, suggestedFailureFileId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAssistUseItemAsSample(e: CustomEvent<{ fileId: string; fileName: string }>): void {
|
private handleAssistUseItemAsSample(e: CustomEvent<{ fileId: string; fileName: string }>): void {
|
||||||
@ -605,6 +609,7 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
sampleFileId: e.detail.fileId,
|
sampleFileId: e.detail.fileId,
|
||||||
sampleFileName: raw,
|
sampleFileName: raw,
|
||||||
fillTarget: 'prefix',
|
fillTarget: 'prefix',
|
||||||
|
suggestedFailureFileId: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
*/
|
*/
|
||||||
private _virtualizerInitialized = false;
|
private _virtualizerInitialized = false;
|
||||||
private _itemsKey: string | null = null;
|
private _itemsKey: string | null = null;
|
||||||
|
private _lastFocusedFailureId: string | null = null;
|
||||||
|
|
||||||
private shouldVirtualize(): boolean {
|
private shouldVirtualize(): boolean {
|
||||||
return this.items.length > VirtualPreviewList.VIRTUALIZE_MIN_ITEMS;
|
return this.items.length > VirtualPreviewList.VIRTUALIZE_MIN_ITEMS;
|
||||||
@ -124,11 +125,16 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
const conflictHint = item.conflict
|
const conflictHint = item.conflict
|
||||||
? html`<span class="conflict-hint">${I18nService.t('error_api_conflict')}</span>`
|
? html`<span class="conflict-hint">${I18nService.t('error_api_conflict')}</span>`
|
||||||
: '';
|
: '';
|
||||||
const previewItemClasses = ['preview-item', statusClass].filter(Boolean).join(' ');
|
const isSuggestedFailure =
|
||||||
|
this.episodeExtractAssistEnabled &&
|
||||||
|
this.episodeExtractAssistState.suggestedFailureFileId === item.file.id;
|
||||||
|
const previewItemClasses = ['preview-item', statusClass, isSuggestedFailure ? 'assist-focus' : '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' ');
|
const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="${previewItemClasses}">
|
<div class="${previewItemClasses}" data-file-id=${item.file.id}>
|
||||||
<div class="preview-content">
|
<div class="preview-content">
|
||||||
<div class="old-name-row">
|
<div class="old-name-row">
|
||||||
<div class="old-name old-name-secondary" title=${item.file.name}>
|
<div class="old-name old-name-secondary" title=${item.file.name}>
|
||||||
@ -145,6 +151,9 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
? html`<div class="error-message" title=${errorTitle}>${errorText}</div>`
|
? html`<div class="error-message" title=${errorTitle}>${errorText}</div>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
${isSuggestedFailure
|
||||||
|
? html`<div class="assist-focus-hint" data-role="episode-assist-focus-hint">${I18nService.t('episode_assist_focus_hint')}</div>`
|
||||||
|
: ''}
|
||||||
${this.renderAssistActions(item)}
|
${this.renderAssistActions(item)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -251,6 +260,24 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
protected updated(changedProperties: PropertyValues<this>): void {
|
protected updated(changedProperties: PropertyValues<this>): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
const suggestedFailureFileId = this.episodeExtractAssistState.suggestedFailureFileId;
|
||||||
|
if (
|
||||||
|
this.episodeExtractAssistEnabled &&
|
||||||
|
suggestedFailureFileId &&
|
||||||
|
suggestedFailureFileId !== this._lastFocusedFailureId &&
|
||||||
|
(changedProperties.has('episodeExtractAssistState') || changedProperties.has('items'))
|
||||||
|
) {
|
||||||
|
this._lastFocusedFailureId = suggestedFailureFileId;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const target = this.renderRoot.querySelector<HTMLElement>(`[data-file-id="${suggestedFailureFileId}"]`);
|
||||||
|
target?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!suggestedFailureFileId) {
|
||||||
|
this._lastFocusedFailureId = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (changedProperties.has('items') && this.items.length > 0) {
|
if (changedProperties.has('items') && this.items.length > 0) {
|
||||||
const nextKey = this.computeItemsKey(this.items);
|
const nextKey = this.computeItemsKey(this.items);
|
||||||
if (nextKey !== this._itemsKey) {
|
if (nextKey !== this._itemsKey) {
|
||||||
@ -356,6 +383,10 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
border-left: 3px solid var(--cdr-border-strong, #d9d9d9);
|
border-left: 3px solid var(--cdr-border-strong, #d9d9d9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-item.assist-focus {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--cdr-primary, #1890ff), 0 0 0 2px rgba(24, 144, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
.preview-content {
|
.preview-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -434,6 +465,16 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assist-focus-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--cdr-info-text, #0958d9);
|
||||||
|
background: var(--cdr-info-bg, #e6f4ff);
|
||||||
|
border: 1px solid var(--cdr-info-border, #91caff);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.assist-actions {
|
.assist-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@ -240,5 +240,6 @@
|
|||||||
"episode_assist_focus_segment_mode": "Switch to segment mode",
|
"episode_assist_focus_segment_mode": "Switch to segment mode",
|
||||||
"rule_preset_invalid": "This template is incompatible with the current version. Please delete and save it again.",
|
"rule_preset_invalid": "This template is incompatible with the current version. Please delete and save it again.",
|
||||||
"rule_preset_invalid_badge": "Invalid",
|
"rule_preset_invalid_badge": "Invalid",
|
||||||
"rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied."
|
"rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied.",
|
||||||
|
"episode_assist_focus_hint": "The failed item is highlighted. Fill from the full text or a segment, then preview again."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -240,5 +240,6 @@
|
|||||||
"episode_assist_focus_segment_mode": "切换到片段点选模式",
|
"episode_assist_focus_segment_mode": "切换到片段点选模式",
|
||||||
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
|
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
|
||||||
"rule_preset_invalid_badge": "已失效",
|
"rule_preset_invalid_badge": "已失效",
|
||||||
"rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。"
|
"rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。",
|
||||||
|
"episode_assist_focus_hint": "已为你定位失败项,可直接点整段或片段回填后继续预览。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -240,5 +240,6 @@
|
|||||||
"episode_assist_focus_segment_mode": "切換到片段點選模式",
|
"episode_assist_focus_segment_mode": "切換到片段點選模式",
|
||||||
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
|
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
|
||||||
"rule_preset_invalid_badge": "已失效",
|
"rule_preset_invalid_badge": "已失效",
|
||||||
"rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。"
|
"rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。",
|
||||||
|
"episode_assist_focus_hint": "已為你定位失敗項,可直接點整段或片段回填後繼續預覽。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,6 +71,11 @@ describe('PreviewPanel episode assist bridge', () => {
|
|||||||
expect(prefixEvents).toHaveLength(1);
|
expect(prefixEvents).toHaveLength(1);
|
||||||
expect(modeEvents).toHaveLength(1);
|
expect(modeEvents).toHaveLength(1);
|
||||||
|
|
||||||
|
const focusedItem = previewList?.shadowRoot?.querySelector('[data-file-id="file-1"]');
|
||||||
|
const focusHint = previewList?.shadowRoot?.querySelector('[data-role="episode-assist-focus-hint"]');
|
||||||
|
expect(focusedItem?.className).toContain('assist-focus');
|
||||||
|
expect(focusHint?.textContent).toContain('定位');
|
||||||
|
|
||||||
panel.remove();
|
panel.remove();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -151,6 +151,51 @@ describe('VirtualPreviewList pending preview mapping', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('highlights and scrolls to the suggested failure item in assist mode', async () => {
|
||||||
|
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
|
||||||
|
const scrollSpy = vi.fn();
|
||||||
|
HTMLElement.prototype.scrollIntoView = scrollSpy;
|
||||||
|
|
||||||
|
const element = new VirtualPreviewList();
|
||||||
|
element.episodeExtractAssistEnabled = true;
|
||||||
|
element.episodeExtractAssistState = {
|
||||||
|
sampleFileId: 'file-error',
|
||||||
|
sampleFileName: 'Show.S01E01',
|
||||||
|
fillTarget: 'prefix',
|
||||||
|
fillMode: 'segment',
|
||||||
|
suggestedFailureFileId: 'file-error',
|
||||||
|
};
|
||||||
|
element.items = [
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
id: 'file-error',
|
||||||
|
name: 'Show.S01E01.mkv',
|
||||||
|
ext: '.mkv',
|
||||||
|
size: 1,
|
||||||
|
mtime: Date.now(),
|
||||||
|
isDir: false,
|
||||||
|
},
|
||||||
|
newName: 'Show.S01E01.mkv',
|
||||||
|
conflict: false,
|
||||||
|
error: 'extract_episode_not_found',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
|
||||||
|
|
||||||
|
const focusedItem = element.shadowRoot?.querySelector('[data-file-id="file-error"]');
|
||||||
|
const hint = element.shadowRoot?.querySelector('[data-role="episode-assist-focus-hint"]');
|
||||||
|
|
||||||
|
expect(focusedItem?.className).toContain('assist-focus');
|
||||||
|
expect(hint?.textContent).toContain(I18nService.t('episode_assist_focus_hint'));
|
||||||
|
expect(scrollSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
it('locks preview-item structure with semantic classes and target-specific truncation rules', async () => {
|
it('locks preview-item structure with semantic classes and target-specific truncation rules', async () => {
|
||||||
const element = new VirtualPreviewList();
|
const element = new VirtualPreviewList();
|
||||||
element.items = [
|
element.items = [
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户