refactor(fe): eliminate every explicit any; enforce no-explicit-any (R7 S3a·4)
Removes all 13 remaining `any`s and flips the ESLint rule from off to error so new ones can't creep back: - api.ts prefs/data JSON bags: Record<string, any> -> Record<string, unknown>, narrowed at consumers (App language guard; NotificationsPanel shapes the notification `data` per type behind its existing typeof guards) - catch (e: any) -> catch (e) + `e instanceof HttpError` narrowing (SettingsPanel, SetupWizard, PlexPlayer) - the YouTube IFrame API boundary: new lib/youtube.ts types (YTPlayer/ YTPlayerEvent/YTNamespace + a Window augmentation) replace the 6 player anys; tsc surfaced the exact method surface to declare - NotificationsPanel `t` props: any-options -> i18next TFunction Gate green: typecheck (app+node+e2e), eslint 0 errors (rule now enforced), prettier, 74 vitest.
This commit is contained in:
@@ -30,6 +30,7 @@ import AddToPlaylist from "./AddToPlaylist";
|
||||
import { modalCount } from "./Modal";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
import { api, type Video, type VideoStatus } from "../lib/api";
|
||||
import type { YTNamespace, YTPlayer, YTPlayerEvent } from "../lib/youtube";
|
||||
import {
|
||||
channelYouTubeUrl,
|
||||
formatDate,
|
||||
@@ -82,16 +83,17 @@ function canScrollBy(el: HTMLElement | null, dir: -1 | 1): boolean {
|
||||
}
|
||||
|
||||
// --- IFrame Player API loader (singleton) ---
|
||||
let apiPromise: Promise<any> | null = null;
|
||||
function loadYouTubeApi(): Promise<any> {
|
||||
let apiPromise: Promise<YTNamespace> | null = null;
|
||||
function loadYouTubeApi(): Promise<YTNamespace> {
|
||||
if (apiPromise) return apiPromise;
|
||||
apiPromise = new Promise((resolve) => {
|
||||
const w = window as any;
|
||||
apiPromise = new Promise<YTNamespace>((resolve) => {
|
||||
const w = window;
|
||||
if (w.YT && w.YT.Player) return resolve(w.YT);
|
||||
const prev = w.onYouTubeIframeAPIReady;
|
||||
w.onYouTubeIframeAPIReady = () => {
|
||||
if (typeof prev === "function") prev();
|
||||
resolve(w.YT);
|
||||
// The API-ready callback only fires once the script has installed window.YT.
|
||||
resolve(w.YT!);
|
||||
};
|
||||
const tag = document.createElement("script");
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
@@ -158,7 +160,7 @@ export default function PlayerModal({
|
||||
const resumeAt =
|
||||
startAt != null && active.id === video.id ? startAt : active.position_seconds || 0;
|
||||
const mountRef = useRef<HTMLDivElement | null>(null);
|
||||
const playerRef = useRef<any>(null);
|
||||
const playerRef = useRef<YTPlayer | null>(null);
|
||||
const autoMarkedRef = useRef(false);
|
||||
// Bound consecutive unplayable items so an auto-advancing queue (esp. loop=all) can't spin over a
|
||||
// run of dead videos — after the cap we stop on the error overlay instead of skipping forever.
|
||||
@@ -706,7 +708,7 @@ export default function PlayerModal({
|
||||
focusModal();
|
||||
applyVolume(volumeRef.current, false);
|
||||
},
|
||||
onStateChange: (e: any) => {
|
||||
onStateChange: (e: YTPlayerEvent) => {
|
||||
// 1 === playing → sync the navigated-to video's title/author for display.
|
||||
if (e?.data === 1) {
|
||||
errorStreakRef.current = 0; // a successful play breaks any unplayable run
|
||||
@@ -725,7 +727,7 @@ export default function PlayerModal({
|
||||
},
|
||||
// The embed couldn't play this video (embedding disabled, removed, private…).
|
||||
// Surface our own message + an "Open on YouTube" escape hatch.
|
||||
onError: (e: any) => {
|
||||
onError: (e: YTPlayerEvent) => {
|
||||
setPlayerError(typeof e?.data === "number" ? e.data : -1);
|
||||
// The IFrame API fires onError (not 'ended') for an unplayable item, so advanceOnEnd()
|
||||
// never runs and an auto-advancing queue would freeze here. Skip forward instead — but
|
||||
|
||||
Reference in New Issue
Block a user