mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-11 17:16:18 -07:00
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
import type { OverlayContentMeasurement, OverlayContentRect } from '../types';
|
|
import type { RendererContext } from './context';
|
|
|
|
const MEASUREMENT_DEBOUNCE_MS = 80;
|
|
|
|
function isMeasurableOverlayLayer(layer: string): layer is 'visible' {
|
|
return layer === 'visible';
|
|
}
|
|
|
|
function round2(value: number): number {
|
|
return Math.round(value * 100) / 100;
|
|
}
|
|
|
|
function toMeasuredRect(rect: DOMRect): OverlayContentRect | null {
|
|
if (!Number.isFinite(rect.left) || !Number.isFinite(rect.top)) {
|
|
return null;
|
|
}
|
|
if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)) {
|
|
return null;
|
|
}
|
|
|
|
const width = Math.max(0, rect.width);
|
|
const height = Math.max(0, rect.height);
|
|
|
|
return {
|
|
x: round2(rect.left),
|
|
y: round2(rect.top),
|
|
width: round2(width),
|
|
height: round2(height),
|
|
};
|
|
}
|
|
|
|
function unionRects(a: OverlayContentRect, b: OverlayContentRect): OverlayContentRect {
|
|
const left = Math.min(a.x, b.x);
|
|
const top = Math.min(a.y, b.y);
|
|
const right = Math.max(a.x + a.width, b.x + b.width);
|
|
const bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
return {
|
|
x: round2(left),
|
|
y: round2(top),
|
|
width: round2(Math.max(0, right - left)),
|
|
height: round2(Math.max(0, bottom - top)),
|
|
};
|
|
}
|
|
|
|
function hasVisibleTextContent(element: HTMLElement): boolean {
|
|
return Boolean(element.textContent && element.textContent.trim().length > 0);
|
|
}
|
|
|
|
function hasArea(rect: OverlayContentRect): boolean {
|
|
return rect.width > 0 && rect.height > 0;
|
|
}
|
|
|
|
function collectInteractiveRects(ctx: RendererContext): OverlayContentRect[] {
|
|
const rects: OverlayContentRect[] = [];
|
|
const subtitleHasContent = hasVisibleTextContent(ctx.dom.subtitleRoot);
|
|
if (subtitleHasContent) {
|
|
const subtitleRect = toMeasuredRect(ctx.dom.subtitleContainer.getBoundingClientRect());
|
|
if (subtitleRect && hasArea(subtitleRect)) {
|
|
rects.push(subtitleRect);
|
|
}
|
|
}
|
|
|
|
const secondaryHasContent = hasVisibleTextContent(ctx.dom.secondarySubRoot);
|
|
if (secondaryHasContent) {
|
|
const secondaryRect = toMeasuredRect(ctx.dom.secondarySubContainer.getBoundingClientRect());
|
|
if (secondaryRect && hasArea(secondaryRect)) {
|
|
rects.push(secondaryRect);
|
|
}
|
|
}
|
|
|
|
if (ctx.state?.subtitleSidebarModalOpen) {
|
|
const sidebarRect = toMeasuredRect(ctx.dom.subtitleSidebarContent.getBoundingClientRect());
|
|
if (sidebarRect && hasArea(sidebarRect)) {
|
|
rects.push(sidebarRect);
|
|
}
|
|
}
|
|
|
|
if (ctx.dom.overlayNotificationStack?.children.length > 0) {
|
|
const notificationRect = toMeasuredRect(
|
|
ctx.dom.overlayNotificationStack.getBoundingClientRect(),
|
|
);
|
|
if (notificationRect && hasArea(notificationRect)) {
|
|
rects.push(notificationRect);
|
|
}
|
|
}
|
|
|
|
if (ctx.state?.notificationHistoryOpen) {
|
|
const historyRect = toMeasuredRect(ctx.dom.overlayNotificationHistory.getBoundingClientRect());
|
|
if (historyRect && hasArea(historyRect)) {
|
|
rects.push(historyRect);
|
|
}
|
|
}
|
|
|
|
return rects;
|
|
}
|
|
|
|
function collectContentRect(rects: OverlayContentRect[]): OverlayContentRect | null {
|
|
let combinedRect: OverlayContentRect | null = null;
|
|
for (const rect of rects) {
|
|
combinedRect = combinedRect ? unionRects(combinedRect, rect) : rect;
|
|
}
|
|
if (!combinedRect) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
x: combinedRect.x,
|
|
y: combinedRect.y,
|
|
width: round2(Math.max(0, combinedRect.width)),
|
|
height: round2(Math.max(0, combinedRect.height)),
|
|
};
|
|
}
|
|
|
|
export function createOverlayContentMeasurementReporter(ctx: RendererContext) {
|
|
let debounceTimer: number | null = null;
|
|
|
|
function emitNow(): void {
|
|
if (!isMeasurableOverlayLayer(ctx.platform.overlayLayer)) {
|
|
return;
|
|
}
|
|
|
|
const interactiveRects = collectInteractiveRects(ctx);
|
|
const measurement: OverlayContentMeasurement = {
|
|
layer: ctx.platform.overlayLayer,
|
|
measuredAtMs: Date.now(),
|
|
viewport: {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
},
|
|
// Explicit null rect signals "no content yet", and main should use fallback bounds.
|
|
contentRect: collectContentRect(interactiveRects),
|
|
interactiveRects,
|
|
};
|
|
|
|
window.electronAPI.reportOverlayContentBounds(measurement);
|
|
}
|
|
|
|
function schedule(): void {
|
|
if (debounceTimer !== null) {
|
|
window.clearTimeout(debounceTimer);
|
|
}
|
|
debounceTimer = window.setTimeout(() => {
|
|
debounceTimer = null;
|
|
emitNow();
|
|
}, MEASUREMENT_DEBOUNCE_MS);
|
|
}
|
|
|
|
return {
|
|
emitNow,
|
|
schedule,
|
|
};
|
|
}
|