mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Update TASK-20.2 status to done
This commit is contained in:
113
src/renderer/overlay-content-measurement.ts
Normal file
113
src/renderer/overlay-content-measurement.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { OverlayContentMeasurement, OverlayContentRect } from "../types";
|
||||
import type { RendererContext } from "./context";
|
||||
|
||||
const MEASUREMENT_DEBOUNCE_MS = 80;
|
||||
|
||||
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 collectContentRect(ctx: RendererContext): OverlayContentRect | null {
|
||||
let combinedRect: OverlayContentRect | null = null;
|
||||
|
||||
const subtitleHasContent = hasVisibleTextContent(ctx.dom.subtitleRoot);
|
||||
if (subtitleHasContent) {
|
||||
const subtitleRect = toMeasuredRect(ctx.dom.subtitleRoot.getBoundingClientRect());
|
||||
if (subtitleRect) {
|
||||
combinedRect = subtitleRect;
|
||||
}
|
||||
}
|
||||
|
||||
const secondaryHasContent = hasVisibleTextContent(ctx.dom.secondarySubRoot);
|
||||
if (secondaryHasContent) {
|
||||
const secondaryRect = toMeasuredRect(
|
||||
ctx.dom.secondarySubContainer.getBoundingClientRect(),
|
||||
);
|
||||
if (secondaryRect) {
|
||||
combinedRect = combinedRect
|
||||
? unionRects(combinedRect, secondaryRect)
|
||||
: secondaryRect;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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(ctx),
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { createKikuModal } from "./modals/kiku.js";
|
||||
import { createRuntimeOptionsModal } from "./modals/runtime-options.js";
|
||||
import { createSubsyncModal } from "./modals/subsync.js";
|
||||
import { createPositioningController } from "./positioning.js";
|
||||
import { createOverlayContentMeasurementReporter } from "./overlay-content-measurement.js";
|
||||
import { createRendererState } from "./state.js";
|
||||
import { createSubtitleRenderer } from "./subtitle-render.js";
|
||||
import { resolveRendererDom } from "./utils/dom.js";
|
||||
@@ -69,6 +70,7 @@ function syncSettingsModalSubtitleSuppression(): void {
|
||||
}
|
||||
|
||||
const subtitleRenderer = createSubtitleRenderer(ctx);
|
||||
const measurementReporter = createOverlayContentMeasurementReporter(ctx);
|
||||
const positioning = createPositioningController(ctx, {
|
||||
modalStateReader: { isAnySettingsModalOpen },
|
||||
applySubtitleFontSize: subtitleRenderer.applySubtitleFontSize,
|
||||
@@ -114,6 +116,7 @@ async function init(): Promise<void> {
|
||||
|
||||
window.electronAPI.onSubtitle((data: SubtitleData) => {
|
||||
subtitleRenderer.renderSubtitle(data);
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
|
||||
window.electronAPI.onSubtitlePosition((position: SubtitlePosition | null) => {
|
||||
@@ -122,11 +125,13 @@ async function init(): Promise<void> {
|
||||
} else {
|
||||
positioning.applyStoredSubtitlePosition(position, "media-change");
|
||||
}
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
|
||||
if (ctx.platform.isInvisibleLayer) {
|
||||
window.electronAPI.onMpvSubtitleRenderMetrics((metrics: MpvSubtitleRenderMetrics) => {
|
||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(metrics, "event");
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
window.electronAPI.onOverlayDebugVisualization((enabled: boolean) => {
|
||||
document.body.classList.toggle("debug-invisible-visualization", enabled);
|
||||
@@ -135,16 +140,20 @@ async function init(): Promise<void> {
|
||||
|
||||
const initialSubtitle = await window.electronAPI.getCurrentSubtitle();
|
||||
subtitleRenderer.renderSubtitle(initialSubtitle);
|
||||
measurementReporter.schedule();
|
||||
|
||||
window.electronAPI.onSecondarySub((text: string) => {
|
||||
subtitleRenderer.renderSecondarySub(text);
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
window.electronAPI.onSecondarySubMode((mode: SecondarySubMode) => {
|
||||
subtitleRenderer.updateSecondarySubMode(mode);
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
|
||||
subtitleRenderer.updateSecondarySubMode(await window.electronAPI.getSecondarySubMode());
|
||||
subtitleRenderer.renderSecondarySub(await window.electronAPI.getCurrentSecondarySub());
|
||||
measurementReporter.schedule();
|
||||
|
||||
const hoverTarget = ctx.platform.isInvisibleLayer
|
||||
? ctx.dom.subtitleRoot
|
||||
@@ -159,6 +168,9 @@ async function init(): Promise<void> {
|
||||
mouseHandlers.setupResizeHandler();
|
||||
mouseHandlers.setupSelectionObserver();
|
||||
mouseHandlers.setupYomitanObserver();
|
||||
window.addEventListener("resize", () => {
|
||||
measurementReporter.schedule();
|
||||
});
|
||||
|
||||
jimakuModal.wireDomEvents();
|
||||
kikuModal.wireDomEvents();
|
||||
@@ -211,11 +223,14 @@ async function init(): Promise<void> {
|
||||
"startup",
|
||||
);
|
||||
subtitleRenderer.applySubtitleStyle(await window.electronAPI.getSubtitleStyle());
|
||||
measurementReporter.schedule();
|
||||
}
|
||||
|
||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||
}
|
||||
|
||||
measurementReporter.emitNow();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
||||
Reference in New Issue
Block a user