mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-22 12:11:27 -07:00
fix(subtitle-sidebar): address latest CodeRabbit review
This commit is contained in:
@@ -299,6 +299,100 @@ test('subtitle sidebar rows support keyboard activation', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitle sidebar renders hour-long cue timestamps as HH:MM:SS', async () => {
|
||||||
|
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||||
|
const previousWindow = globals.window;
|
||||||
|
const previousDocument = globals.document;
|
||||||
|
|
||||||
|
const snapshot: SubtitleSidebarSnapshot = {
|
||||||
|
cues: [{ startTime: 3665, endTime: 3670, text: 'long cue' }],
|
||||||
|
currentSubtitle: {
|
||||||
|
text: 'long cue',
|
||||||
|
startTime: 3665,
|
||||||
|
endTime: 3670,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
enabled: true,
|
||||||
|
autoOpen: false,
|
||||||
|
layout: 'overlay',
|
||||||
|
toggleKey: 'Backslash',
|
||||||
|
pauseVideoOnHover: false,
|
||||||
|
autoScroll: true,
|
||||||
|
maxWidth: 420,
|
||||||
|
opacity: 0.92,
|
||||||
|
backgroundColor: 'rgba(54, 58, 79, 0.88)',
|
||||||
|
textColor: '#cad3f5',
|
||||||
|
fontFamily: '"Iosevka Aile", sans-serif',
|
||||||
|
fontSize: 17,
|
||||||
|
timestampColor: '#a5adcb',
|
||||||
|
activeLineColor: '#f5bde6',
|
||||||
|
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||||
|
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
electronAPI: {
|
||||||
|
getSubtitleSidebarSnapshot: async () => snapshot,
|
||||||
|
sendMpvCommand: () => {},
|
||||||
|
} as unknown as ElectronAPI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
createElement: () => createCueRow(),
|
||||||
|
body: {
|
||||||
|
classList: createClassList(),
|
||||||
|
},
|
||||||
|
documentElement: {
|
||||||
|
style: {
|
||||||
|
setProperty: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = createRendererState();
|
||||||
|
const cueList = createListStub();
|
||||||
|
const ctx = {
|
||||||
|
dom: {
|
||||||
|
overlay: { classList: createClassList() },
|
||||||
|
subtitleSidebarModal: {
|
||||||
|
classList: createClassList(['hidden']),
|
||||||
|
setAttribute: () => {},
|
||||||
|
style: { setProperty: () => {} },
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
subtitleSidebarContent: {
|
||||||
|
classList: createClassList(),
|
||||||
|
getBoundingClientRect: () => ({ width: 420 }),
|
||||||
|
},
|
||||||
|
subtitleSidebarClose: { addEventListener: () => {} },
|
||||||
|
subtitleSidebarStatus: { textContent: '' },
|
||||||
|
subtitleSidebarList: cueList,
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal = createSubtitleSidebarModal(ctx as never, {
|
||||||
|
modalStateReader: { isAnyModalOpen: () => false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await modal.openSubtitleSidebarModal();
|
||||||
|
|
||||||
|
const firstRow = cueList.children[0]!;
|
||||||
|
assert.equal(firstRow.attributes['aria-label'], 'Jump to subtitle at 01:01:05');
|
||||||
|
assert.equal((firstRow.children[0] as { textContent: string }).textContent, '01:01:05');
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('subtitle sidebar does not open when the feature is disabled', async () => {
|
test('subtitle sidebar does not open when the feature is disabled', async () => {
|
||||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||||
const previousWindow = globals.window;
|
const previousWindow = globals.window;
|
||||||
@@ -1501,6 +1595,127 @@ test('subtitle sidebar embedded layout reserves and releases mpv right margin',
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitle sidebar embedded layout measures reserved width after embedded classes apply', async () => {
|
||||||
|
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||||
|
const previousWindow = globals.window;
|
||||||
|
const previousDocument = globals.document;
|
||||||
|
const mpvCommands: Array<Array<string | number>> = [];
|
||||||
|
const rootStyleCalls: Array<[string, string]> = [];
|
||||||
|
const bodyClassList = createClassList();
|
||||||
|
const contentClassList = createClassList();
|
||||||
|
|
||||||
|
const snapshot: SubtitleSidebarSnapshot = {
|
||||||
|
cues: [{ startTime: 1, endTime: 2, text: 'first' }],
|
||||||
|
currentSubtitle: {
|
||||||
|
text: 'first',
|
||||||
|
startTime: 1,
|
||||||
|
endTime: 2,
|
||||||
|
},
|
||||||
|
currentTimeSec: 1.1,
|
||||||
|
config: {
|
||||||
|
enabled: true,
|
||||||
|
autoOpen: false,
|
||||||
|
layout: 'embedded',
|
||||||
|
toggleKey: 'Backslash',
|
||||||
|
pauseVideoOnHover: false,
|
||||||
|
autoScroll: true,
|
||||||
|
maxWidth: 420,
|
||||||
|
opacity: 0.92,
|
||||||
|
backgroundColor: 'rgba(54, 58, 79, 0.88)',
|
||||||
|
textColor: '#cad3f5',
|
||||||
|
fontFamily: '"Iosevka Aile", sans-serif',
|
||||||
|
fontSize: 17,
|
||||||
|
timestampColor: '#a5adcb',
|
||||||
|
activeLineColor: '#f5bde6',
|
||||||
|
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||||
|
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'window', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
innerWidth: 1200,
|
||||||
|
electronAPI: {
|
||||||
|
getSubtitleSidebarSnapshot: async () => snapshot,
|
||||||
|
sendMpvCommand: (command: Array<string | number>) => {
|
||||||
|
mpvCommands.push(command);
|
||||||
|
},
|
||||||
|
} as unknown as ElectronAPI,
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, 'document', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
createElement: () => createCueRow(),
|
||||||
|
body: {
|
||||||
|
classList: bodyClassList,
|
||||||
|
},
|
||||||
|
documentElement: {
|
||||||
|
style: {
|
||||||
|
setProperty: (name: string, value: string) => {
|
||||||
|
rootStyleCalls.push([name, value]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = createRendererState();
|
||||||
|
const ctx = {
|
||||||
|
dom: {
|
||||||
|
overlay: { classList: createClassList() },
|
||||||
|
subtitleSidebarModal: {
|
||||||
|
classList: createClassList(['hidden']),
|
||||||
|
setAttribute: () => {},
|
||||||
|
style: { setProperty: () => {} },
|
||||||
|
addEventListener: () => {},
|
||||||
|
},
|
||||||
|
subtitleSidebarContent: {
|
||||||
|
classList: contentClassList,
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
width: contentClassList.contains('subtitle-sidebar-content-embedded') ? 300 : 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
subtitleSidebarClose: { addEventListener: () => {} },
|
||||||
|
subtitleSidebarStatus: { textContent: '' },
|
||||||
|
subtitleSidebarList: createListStub(),
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
shouldToggleMouseIgnore: false,
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal = createSubtitleSidebarModal(ctx as never, {
|
||||||
|
modalStateReader: { isAnyModalOpen: () => false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await modal.openSubtitleSidebarModal();
|
||||||
|
|
||||||
|
assert.ok(bodyClassList.contains('subtitle-sidebar-embedded-open'));
|
||||||
|
assert.ok(
|
||||||
|
rootStyleCalls.some(
|
||||||
|
([name, value]) => name === '--subtitle-sidebar-reserved-width' && value === '300px',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
mpvCommands.some(
|
||||||
|
(command) =>
|
||||||
|
command[0] === 'set_property' &&
|
||||||
|
command[1] === 'video-margin-ratio-right' &&
|
||||||
|
command[2] === 0.25,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('subtitle sidebar embedded layout restores macOS and Windows passthrough outside sidebar hover', async () => {
|
test('subtitle sidebar embedded layout restores macOS and Windows passthrough outside sidebar hover', async () => {
|
||||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||||
const previousWindow = globals.window;
|
const previousWindow = globals.window;
|
||||||
|
|||||||
@@ -38,8 +38,16 @@ function normalizeCueText(text: string): string {
|
|||||||
|
|
||||||
function formatCueTimestamp(seconds: number): string {
|
function formatCueTimestamp(seconds: number): string {
|
||||||
const totalSeconds = Math.max(0, Math.floor(seconds));
|
const totalSeconds = Math.max(0, Math.floor(seconds));
|
||||||
const mins = Math.floor(totalSeconds / 60);
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const mins = Math.floor((totalSeconds % 3600) / 60);
|
||||||
const secs = totalSeconds % 60;
|
const secs = totalSeconds % 60;
|
||||||
|
if (hours > 0) {
|
||||||
|
return [
|
||||||
|
String(hours).padStart(2, '0'),
|
||||||
|
String(mins).padStart(2, '0'),
|
||||||
|
String(secs).padStart(2, '0'),
|
||||||
|
].join(':');
|
||||||
|
}
|
||||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +170,11 @@ export function createSubtitleSidebarModal(
|
|||||||
|
|
||||||
function syncEmbeddedSidebarLayout(): void {
|
function syncEmbeddedSidebarLayout(): void {
|
||||||
const config = ctx.state.subtitleSidebarConfig;
|
const config = ctx.state.subtitleSidebarConfig;
|
||||||
const reservedWidthPx = getReservedSidebarWidthPx();
|
const wantsEmbedded = Boolean(
|
||||||
const embedded = Boolean(config && config.layout === 'embedded' && reservedWidthPx > 0);
|
config && config.layout === 'embedded' && ctx.state.subtitleSidebarModalOpen,
|
||||||
|
);
|
||||||
|
|
||||||
if (embedded) {
|
if (wantsEmbedded) {
|
||||||
ctx.dom.subtitleSidebarContent.classList.add('subtitle-sidebar-content-embedded');
|
ctx.dom.subtitleSidebarContent.classList.add('subtitle-sidebar-content-embedded');
|
||||||
ctx.dom.subtitleSidebarModal.classList.add('subtitle-sidebar-modal-embedded');
|
ctx.dom.subtitleSidebarModal.classList.add('subtitle-sidebar-modal-embedded');
|
||||||
document.body.classList.add('subtitle-sidebar-embedded-open');
|
document.body.classList.add('subtitle-sidebar-embedded-open');
|
||||||
@@ -174,6 +183,8 @@ export function createSubtitleSidebarModal(
|
|||||||
ctx.dom.subtitleSidebarModal.classList.remove('subtitle-sidebar-modal-embedded');
|
ctx.dom.subtitleSidebarModal.classList.remove('subtitle-sidebar-modal-embedded');
|
||||||
document.body.classList.remove('subtitle-sidebar-embedded-open');
|
document.body.classList.remove('subtitle-sidebar-embedded-open');
|
||||||
}
|
}
|
||||||
|
const reservedWidthPx = wantsEmbedded ? getReservedSidebarWidthPx() : 0;
|
||||||
|
const embedded = wantsEmbedded && reservedWidthPx > 0;
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
'--subtitle-sidebar-reserved-width',
|
'--subtitle-sidebar-reserved-width',
|
||||||
`${Math.max(0, Math.round(reservedWidthPx))}px`,
|
`${Math.max(0, Math.round(reservedWidthPx))}px`,
|
||||||
|
|||||||
Reference in New Issue
Block a user