import type { SubtitlePosition } from "../../types"; import type { RendererContext } from "../context"; const PREFERRED_Y_PERCENT_MIN = 2; const PREFERRED_Y_PERCENT_MAX = 80; export type SubtitlePositionController = { applyStoredSubtitlePosition: (position: SubtitlePosition | null, source: string) => void; getCurrentYPercent: () => number; applyYPercent: (yPercent: number) => void; persistSubtitlePositionPatch: (patch: Partial) => void; }; function clampYPercent(yPercent: number): number { return Math.max(PREFERRED_Y_PERCENT_MIN, Math.min(PREFERRED_Y_PERCENT_MAX, yPercent)); } function getPersistedYPercent( ctx: RendererContext, position: SubtitlePosition | null, ): number { if (!position || typeof position.yPercent !== "number" || !Number.isFinite(position.yPercent)) { return ctx.state.persistedSubtitlePosition.yPercent; } return position.yPercent; } function getPersistedOffset( ctx: RendererContext, position: SubtitlePosition | null, key: "invisibleOffsetXPx" | "invisibleOffsetYPx", ): number { if ( position && typeof position[key] === "number" && Number.isFinite(position[key]) ) { return position[key]; } return 0; } function updatePersistedSubtitlePosition( ctx: RendererContext, position: SubtitlePosition | null, ): void { ctx.state.persistedSubtitlePosition = { yPercent: getPersistedYPercent(ctx, position), invisibleOffsetXPx: getPersistedOffset(ctx, position, "invisibleOffsetXPx"), invisibleOffsetYPx: getPersistedOffset(ctx, position, "invisibleOffsetYPx"), }; } function getNextPersistedPosition( ctx: RendererContext, patch: Partial, ): SubtitlePosition { return { yPercent: typeof patch.yPercent === "number" && Number.isFinite(patch.yPercent) ? patch.yPercent : ctx.state.persistedSubtitlePosition.yPercent, invisibleOffsetXPx: typeof patch.invisibleOffsetXPx === "number" && Number.isFinite(patch.invisibleOffsetXPx) ? patch.invisibleOffsetXPx : ctx.state.persistedSubtitlePosition.invisibleOffsetXPx ?? 0, invisibleOffsetYPx: typeof patch.invisibleOffsetYPx === "number" && Number.isFinite(patch.invisibleOffsetYPx) ? patch.invisibleOffsetYPx : ctx.state.persistedSubtitlePosition.invisibleOffsetYPx ?? 0, }; } export function createInMemorySubtitlePositionController( ctx: RendererContext, ): SubtitlePositionController { function getCurrentYPercent(): number { if (ctx.state.currentYPercent !== null) { return ctx.state.currentYPercent; } const marginBottom = parseFloat(ctx.dom.subtitleContainer.style.marginBottom) || 60; ctx.state.currentYPercent = clampYPercent((marginBottom / window.innerHeight) * 100); return ctx.state.currentYPercent; } function applyYPercent(yPercent: number): void { const clampedPercent = clampYPercent(yPercent); ctx.state.currentYPercent = clampedPercent; const marginBottom = (clampedPercent / 100) * window.innerHeight; ctx.dom.subtitleContainer.style.position = ""; ctx.dom.subtitleContainer.style.left = ""; ctx.dom.subtitleContainer.style.top = ""; ctx.dom.subtitleContainer.style.right = ""; ctx.dom.subtitleContainer.style.transform = ""; ctx.dom.subtitleContainer.style.marginBottom = `${marginBottom}px`; } function persistSubtitlePositionPatch(patch: Partial): void { const nextPosition = getNextPersistedPosition(ctx, patch); ctx.state.persistedSubtitlePosition = nextPosition; window.electronAPI.saveSubtitlePosition(nextPosition); } function applyStoredSubtitlePosition(position: SubtitlePosition | null, source: string): void { updatePersistedSubtitlePosition(ctx, position); if (position && position.yPercent !== undefined) { applyYPercent(position.yPercent); console.log( "Applied subtitle position from", source, ":", position.yPercent, "%", ); return; } const defaultMarginBottom = 60; const defaultYPercent = (defaultMarginBottom / window.innerHeight) * 100; applyYPercent(defaultYPercent); console.log("Applied default subtitle position from", source); } return { applyStoredSubtitlePosition, getCurrentYPercent, applyYPercent, persistSubtitlePositionPatch, }; }