镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 21:57:47 +08:00
feat: 增加剧集提取失败项导航
这个提交包含在:
父节点
2559ef127f
当前提交
a3c6ed48ed
@ -14,3 +14,8 @@
|
|||||||
- 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:继续完成“剧集提取失败态增强”:`suggestedFailureFileId` 现在真正用于右侧预览的失败项高亮与自动滚动定位,避免“点了首个失败项但用户还要自己找”的断层体验。
|
||||||
- 2026-03-25:为失败项增加内联引导文案 `episode_assist_focus_hint`,明确告诉用户可以直接点整段/片段继续修正,再次预览。
|
- 2026-03-25:为失败项增加内联引导文案 `episode_assist_focus_hint`,明确告诉用户可以直接点整段/片段继续修正,再次预览。
|
||||||
|
- 2026-03-25:已提交第三批改动 `2559ef1 feat: 增强剧集提取失败项定位与引导`,当前工作区已恢复干净,可继续下一批优化。
|
||||||
|
- 2026-03-25:已确认下一批范围为右侧预览区顶部失败导航(B1),仅做失败总数、当前序号、上一个/下一个失败项,不扩展到失败筛选或自动跳转。
|
||||||
|
- 2026-03-25:本批已完成右侧预览区失败导航条:显示失败总数、当前序号、上一个/下一个失败项,并通过 `episode-assist-focus-failure` 事件复用既有失败聚焦逻辑。
|
||||||
|
- 2026-03-25:额外补了一层 `scrollIntoView` 守卫,避免测试环境或部分宿主环境缺少该方法时抛出异常。
|
||||||
|
- 2026-03-25:本批验证结果:`npm run typecheck` 通过,`npx vitest run` 通过(36 files / 268 tests),`npm run lint` 通过但仍仅存在仓库历史 warning。
|
||||||
|
|||||||
@ -13,3 +13,8 @@
|
|||||||
- 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:完成下一批失败态增强:右侧预览支持根据 `suggestedFailureFileId` 自动高亮并滚动定位失败项,且在整段/片段回填成功后清除聚焦态,避免提示残留。
|
||||||
- 2026-03-25:补充失败态引导文案与测试,验证通过:`typecheck` ✅,`vitest` 全量 266 tests ✅,`lint` ✅(仅历史 warning)。
|
- 2026-03-25:补充失败态引导文案与测试,验证通过:`typecheck` ✅,`vitest` 全量 266 tests ✅,`lint` ✅(仅历史 warning)。
|
||||||
|
- 2026-03-25:已提交第三批改动 `2559ef1 feat: 增强剧集提取失败项定位与引导`,准备进入下一批优化设计。
|
||||||
|
- 2026-03-25:与用户确认下一批方案:右侧预览区顶部增加失败导航条,采用非循环边界禁用策略,并继续复用 `suggestedFailureFileId` 做高亮与滚动。
|
||||||
|
- 2026-03-25:实现右侧预览区顶部失败导航条,支持显示失败总数、当前序号,并通过上一个/下一个按钮切换失败聚焦项。
|
||||||
|
- 2026-03-25:完成接线:`preview-panel` 负责计算 prev/next 目标并派发 `episode-assist-focus-failure`,`file-selector-panel` 统一更新 sample 与 `suggestedFailureFileId`。
|
||||||
|
- 2026-03-25:完成验证:`npm run typecheck` ✅,`npx vitest run` ✅(36 files / 268 tests),`npm run lint` ✅(仅历史 warning)。
|
||||||
|
|||||||
@ -543,6 +543,17 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
this.setAssistState({ fillMode: e.detail.mode });
|
this.setAssistState({ fillMode: e.detail.mode });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private focusAssistFailure(fileId: string): void {
|
||||||
|
const file = this.selectedFiles.find((item) => item.id === fileId) || this.filteredFiles.find((item) => item.id === fileId);
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
this.setAssistState({
|
||||||
|
sampleFileId: file.id,
|
||||||
|
sampleFileName: file.name,
|
||||||
|
suggestedFailureFileId: file.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private handleAssistUseRandomSample(): void {
|
private handleAssistUseRandomSample(): void {
|
||||||
const candidates = this.selectedFiles.length > 0 ? this.selectedFiles : this.filteredFiles;
|
const candidates = this.selectedFiles.length > 0 ? this.selectedFiles : this.filteredFiles;
|
||||||
const sample = candidates[Math.floor(Math.random() * Math.max(1, candidates.length))];
|
const sample = candidates[Math.floor(Math.random() * Math.max(1, candidates.length))];
|
||||||
@ -553,11 +564,7 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
private handleAssistUseFirstFailure(): void {
|
private handleAssistUseFirstFailure(): void {
|
||||||
const firstFailure = this.previewList.find((item) => item.error);
|
const firstFailure = this.previewList.find((item) => item.error);
|
||||||
if (!firstFailure) return;
|
if (!firstFailure) return;
|
||||||
this.setAssistState({
|
this.focusAssistFailure(firstFailure.file.id);
|
||||||
sampleFileId: firstFailure.file.id,
|
|
||||||
sampleFileName: firstFailure.file.name,
|
|
||||||
suggestedFailureFileId: firstFailure.file.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAssistClearSample(): void {
|
private handleAssistClearSample(): void {
|
||||||
@ -613,6 +620,10 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleAssistFocusFailure(e: CustomEvent<{ fileId: string }>): void {
|
||||||
|
this.focusAssistFailure(e.detail.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
private handleAssistFocusSegmentMode(e: CustomEvent<{ fileId: string }>): void {
|
private handleAssistFocusSegmentMode(e: CustomEvent<{ fileId: string }>): void {
|
||||||
const file = this.selectedFiles.find((item) => item.id === e.detail.fileId) || this.filteredFiles.find((item) => item.id === e.detail.fileId);
|
const file = this.selectedFiles.find((item) => item.id === e.detail.fileId) || this.filteredFiles.find((item) => item.id === e.detail.fileId);
|
||||||
this.setAssistState({
|
this.setAssistState({
|
||||||
@ -1662,6 +1673,7 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
@episode-assist-use-item-as-sample=${this.handleAssistUseItemAsSample}
|
@episode-assist-use-item-as-sample=${this.handleAssistUseItemAsSample}
|
||||||
@episode-assist-use-prefix-from-item=${this.handleAssistUsePrefixFromItem}
|
@episode-assist-use-prefix-from-item=${this.handleAssistUsePrefixFromItem}
|
||||||
@episode-assist-focus-segment-mode=${this.handleAssistFocusSegmentMode}
|
@episode-assist-focus-segment-mode=${this.handleAssistFocusSegmentMode}
|
||||||
|
@episode-assist-focus-failure=${this.handleAssistFocusFailure}
|
||||||
></preview-panel>
|
></preview-panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,31 @@ import './virtual-preview-list';
|
|||||||
*/
|
*/
|
||||||
@customElement('preview-panel')
|
@customElement('preview-panel')
|
||||||
export class PreviewPanel extends LitElement {
|
export class PreviewPanel extends LitElement {
|
||||||
|
private getFailureNavigationState() {
|
||||||
|
const failureItems = this.items.filter((item) => Boolean(item.error));
|
||||||
|
const currentId = this.episodeExtractAssistState.suggestedFailureFileId;
|
||||||
|
const currentIndex = failureItems.findIndex((item) => item.file.id === currentId);
|
||||||
|
const normalizedIndex = failureItems.length === 0 ? -1 : currentIndex >= 0 ? currentIndex : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
failureItems,
|
||||||
|
normalizedIndex,
|
||||||
|
currentItem: normalizedIndex >= 0 ? failureItems[normalizedIndex] : null,
|
||||||
|
previousItem: normalizedIndex > 0 ? failureItems[normalizedIndex - 1] : null,
|
||||||
|
nextItem: normalizedIndex >= 0 && normalizedIndex < failureItems.length - 1 ? failureItems[normalizedIndex + 1] : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitAssistFocusFailure(fileId: string): void {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('episode-assist-focus-failure', {
|
||||||
|
detail: { fileId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of preview items
|
* Array of preview items
|
||||||
*/
|
*/
|
||||||
@ -49,6 +74,47 @@ export class PreviewPanel extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
episodeExtractAssistState: EpisodeExtractAssistState = DEFAULT_EPISODE_EXTRACT_ASSIST_STATE;
|
episodeExtractAssistState: EpisodeExtractAssistState = DEFAULT_EPISODE_EXTRACT_ASSIST_STATE;
|
||||||
|
|
||||||
|
private renderFailureNavigation() {
|
||||||
|
if (!this.episodeExtractAssistEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { failureItems, normalizedIndex, previousItem, nextItem } = this.getFailureNavigationState();
|
||||||
|
if (failureItems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPosition = normalizedIndex >= 0 ? normalizedIndex + 1 : 0;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="failure-nav" data-role="episode-assist-failure-nav">
|
||||||
|
<div class="failure-nav-summary" data-role="episode-assist-failure-nav-summary">
|
||||||
|
<span class="failure-nav-label">${I18nService.t('episode_assist_failure_nav_label')}</span>
|
||||||
|
<span class="failure-nav-count">${failureItems.length}</span>
|
||||||
|
<span class="failure-nav-position">${currentPosition} / ${failureItems.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="failure-nav-actions">
|
||||||
|
<button
|
||||||
|
class="failure-nav-button"
|
||||||
|
data-role="episode-assist-prev-failure"
|
||||||
|
?disabled=${!previousItem}
|
||||||
|
@click=${() => previousItem && this.emitAssistFocusFailure(previousItem.file.id)}
|
||||||
|
>
|
||||||
|
${I18nService.t('episode_assist_prev_failure')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="failure-nav-button"
|
||||||
|
data-role="episode-assist-next-failure"
|
||||||
|
?disabled=${!nextItem}
|
||||||
|
@click=${() => nextItem && this.emitAssistFocusFailure(nextItem.file.id)}
|
||||||
|
>
|
||||||
|
${I18nService.t('episode_assist_next_failure')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hasConflicts = this.conflictCount > 0;
|
const hasConflicts = this.conflictCount > 0;
|
||||||
const successCount = this.items.filter((i) => i.done).length;
|
const successCount = this.items.filter((i) => i.done).length;
|
||||||
@ -104,6 +170,8 @@ export class PreviewPanel extends LitElement {
|
|||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${this.renderFailureNavigation()}
|
||||||
|
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
${this.loading
|
${this.loading
|
||||||
? html`
|
? html`
|
||||||
@ -208,6 +276,65 @@ export class PreviewPanel extends LitElement {
|
|||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.failure-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--cdr-info-bg, #e6f4ff);
|
||||||
|
border-bottom: 1px solid var(--cdr-info-border, #91caff);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-nav-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--cdr-info-text, #0958d9);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-nav-label {
|
||||||
|
color: var(--cdr-text, #262626);
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-nav-count,
|
||||||
|
.failure-nav-position {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(9, 88, 217, 0.18);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-nav-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-nav-button {
|
||||||
|
border: 1px solid var(--cdr-border, #d9d9d9);
|
||||||
|
background: var(--cdr-surface, #fff);
|
||||||
|
color: var(--cdr-text, #262626);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure-nav-button:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.list-container {
|
.list-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -270,7 +270,9 @@ export class VirtualPreviewList extends LitElement {
|
|||||||
this._lastFocusedFailureId = suggestedFailureFileId;
|
this._lastFocusedFailureId = suggestedFailureFileId;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const target = this.renderRoot.querySelector<HTMLElement>(`[data-file-id="${suggestedFailureFileId}"]`);
|
const target = this.renderRoot.querySelector<HTMLElement>(`[data-file-id="${suggestedFailureFileId}"]`);
|
||||||
target?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
if (target && typeof target.scrollIntoView === 'function') {
|
||||||
|
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -241,5 +241,8 @@
|
|||||||
"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_failure_nav_label": "Failed items",
|
||||||
|
"episode_assist_prev_failure": "Previous",
|
||||||
|
"episode_assist_next_failure": "Next",
|
||||||
"episode_assist_focus_hint": "The failed item is highlighted. Fill from the full text or a segment, then preview again."
|
"episode_assist_focus_hint": "The failed item is highlighted. Fill from the full text or a segment, then preview again."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -241,5 +241,8 @@
|
|||||||
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
|
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
|
||||||
"rule_preset_invalid_badge": "已失效",
|
"rule_preset_invalid_badge": "已失效",
|
||||||
"rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。",
|
"rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。",
|
||||||
|
"episode_assist_failure_nav_label": "失败项",
|
||||||
|
"episode_assist_prev_failure": "上一个",
|
||||||
|
"episode_assist_next_failure": "下一个",
|
||||||
"episode_assist_focus_hint": "已为你定位失败项,可直接点整段或片段回填后继续预览。"
|
"episode_assist_focus_hint": "已为你定位失败项,可直接点整段或片段回填后继续预览。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -241,5 +241,8 @@
|
|||||||
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
|
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
|
||||||
"rule_preset_invalid_badge": "已失效",
|
"rule_preset_invalid_badge": "已失效",
|
||||||
"rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。",
|
"rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。",
|
||||||
|
"episode_assist_failure_nav_label": "失敗項",
|
||||||
|
"episode_assist_prev_failure": "上一個",
|
||||||
|
"episode_assist_next_failure": "下一個",
|
||||||
"episode_assist_focus_hint": "已為你定位失敗項,可直接點整段或片段回填後繼續預覽。"
|
"episode_assist_focus_hint": "已為你定位失敗項,可直接點整段或片段回填後繼續預覽。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,3 +23,4 @@
|
|||||||
| P0-2:剧集提取交互增强(B) | complete | 已实现样本回填、片段点选、失败项快捷修正、预览项回填接线。 |
|
| P0-2:剧集提取交互增强(B) | complete | 已实现样本回填、片段点选、失败项快捷修正、预览项回填接线。 |
|
||||||
| P0-3:规则模板 / 最近使用(B) | complete | 已实现 recent 自动记录、模板保存/删除、同名覆盖确认。 |
|
| P0-3:规则模板 / 最近使用(B) | complete | 已实现 recent 自动记录、模板保存/删除、同名覆盖确认。 |
|
||||||
| 验证与回归 | complete | `typecheck` / `vitest` / `lint` 已跑通;lint 仅剩仓库历史 warning。 |
|
| 验证与回归 | complete | `typecheck` / `vitest` / `lint` 已跑通;lint 仅剩仓库历史 warning。 |
|
||||||
|
| 后续批次:继续优化剧集提取失败修正体验 | complete | 已完成右侧预览区顶部失败导航(B1):失败总数 + 当前序号 + 上一个/下一个失败项,并完成验证。 |
|
||||||
|
|||||||
@ -15,6 +15,7 @@ type FileSelectorPanelPresetHarness = FileSelectorPanel & {
|
|||||||
recentRulePresets: any[];
|
recentRulePresets: any[];
|
||||||
handleApplyRulePreset(event: CustomEvent<{ presetId: string }>): Promise<void>;
|
handleApplyRulePreset(event: CustomEvent<{ presetId: string }>): Promise<void>;
|
||||||
handleExecute(): Promise<void>;
|
handleExecute(): Promise<void>;
|
||||||
|
handleAssistFocusFailure(event: CustomEvent<{ fileId: string }>): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PresetTestAdapter implements PlatformAdapter {
|
class PresetTestAdapter implements PlatformAdapter {
|
||||||
@ -208,4 +209,43 @@ describe('FileSelectorPanel rule preset integration', () => {
|
|||||||
expect(panel.recentRulePresets).toHaveLength(1);
|
expect(panel.recentRulePresets).toHaveLength(1);
|
||||||
expect(panel.recentRulePresets[0].id).toBe('recent-1');
|
expect(panel.recentRulePresets[0].id).toBe('recent-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('focuses the target failure item from preview navigation', async () => {
|
||||||
|
const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness;
|
||||||
|
panel.allFiles = [
|
||||||
|
{
|
||||||
|
id: 'file-1',
|
||||||
|
name: 'Show.S01E01.mkv',
|
||||||
|
ext: '.mkv',
|
||||||
|
parentId: 'root',
|
||||||
|
size: 1,
|
||||||
|
mtime: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
name: 'Show.S01E02.mkv',
|
||||||
|
ext: '.mkv',
|
||||||
|
parentId: 'root',
|
||||||
|
size: 1,
|
||||||
|
mtime: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
panel.uncheckList = new Set();
|
||||||
|
panel.episodeExtractAssistState = {
|
||||||
|
sampleFileId: null,
|
||||||
|
sampleFileName: null,
|
||||||
|
fillTarget: 'prefix',
|
||||||
|
fillMode: 'segment',
|
||||||
|
suggestedFailureFileId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.handleAssistFocusFailure(
|
||||||
|
new CustomEvent('episode-assist-focus-failure', { detail: { fileId: 'file-2' } })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(panel.episodeExtractAssistState.sampleFileId).toBe('file-2');
|
||||||
|
expect(panel.episodeExtractAssistState.sampleFileName).toBe('Show.S01E02.mkv');
|
||||||
|
expect(panel.episodeExtractAssistState.suggestedFailureFileId).toBe('file-2');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -78,4 +78,84 @@ describe('PreviewPanel episode assist bridge', () => {
|
|||||||
|
|
||||||
panel.remove();
|
panel.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders failure navigation in preview header and dispatches focus events', async () => {
|
||||||
|
const panel = new PreviewPanel();
|
||||||
|
panel.episodeExtractAssistEnabled = true;
|
||||||
|
panel.episodeExtractAssistState = {
|
||||||
|
sampleFileId: 'file-2',
|
||||||
|
sampleFileName: 'My.Show.S01E02',
|
||||||
|
fillTarget: 'prefix',
|
||||||
|
fillMode: 'segment',
|
||||||
|
suggestedFailureFileId: 'file-2',
|
||||||
|
};
|
||||||
|
panel.items = [
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
id: 'file-1',
|
||||||
|
name: 'My.Show.S01E01.mkv',
|
||||||
|
ext: '.mkv',
|
||||||
|
parentId: 'root',
|
||||||
|
size: 1,
|
||||||
|
mtime: Date.now(),
|
||||||
|
},
|
||||||
|
newName: 'My.Show.S01E01.mkv',
|
||||||
|
conflict: false,
|
||||||
|
error: 'extract_episode_not_found',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
id: 'file-2',
|
||||||
|
name: 'My.Show.S01E02.mkv',
|
||||||
|
ext: '.mkv',
|
||||||
|
parentId: 'root',
|
||||||
|
size: 1,
|
||||||
|
mtime: Date.now(),
|
||||||
|
},
|
||||||
|
newName: 'My.Show.S01E02.mkv',
|
||||||
|
conflict: false,
|
||||||
|
error: 'extract_episode_not_found',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
id: 'file-3',
|
||||||
|
name: 'My.Show.S01E03.mkv',
|
||||||
|
ext: '.mkv',
|
||||||
|
parentId: 'root',
|
||||||
|
size: 1,
|
||||||
|
mtime: Date.now(),
|
||||||
|
},
|
||||||
|
newName: 'My.Show.S01E03.mkv',
|
||||||
|
conflict: false,
|
||||||
|
error: 'extract_episode_not_found',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const focusEvents: Array<CustomEvent<{ fileId: string }>> = [];
|
||||||
|
panel.addEventListener('episode-assist-focus-failure', (event) => {
|
||||||
|
focusEvents.push(event as CustomEvent<{ fileId: string }>);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
await panel.updateComplete;
|
||||||
|
|
||||||
|
const summary = panel.shadowRoot?.querySelector('[data-role="episode-assist-failure-nav-summary"]');
|
||||||
|
const prevButton = panel.shadowRoot?.querySelector<HTMLButtonElement>('[data-role="episode-assist-prev-failure"]');
|
||||||
|
const nextButton = panel.shadowRoot?.querySelector<HTMLButtonElement>('[data-role="episode-assist-next-failure"]');
|
||||||
|
|
||||||
|
expect(summary?.textContent).toContain('3');
|
||||||
|
expect(summary?.textContent).toContain('2 / 3');
|
||||||
|
expect(prevButton?.disabled).toBe(false);
|
||||||
|
expect(nextButton?.disabled).toBe(false);
|
||||||
|
|
||||||
|
prevButton?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||||
|
nextButton?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
|
expect(focusEvents).toHaveLength(2);
|
||||||
|
expect(focusEvents[0].detail.fileId).toBe('file-1');
|
||||||
|
expect(focusEvents[1].detail.fileId).toBe('file-3');
|
||||||
|
|
||||||
|
panel.remove();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户