diff --git a/VERSION b/VERSION index b0ac4d72..8ee93b94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.63.2 \ No newline at end of file +0.63.3 \ No newline at end of file diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index 437bad27..108ca1a3 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -36,7 +36,8 @@ import { subShadowCss, type PlexPlayerPrefs, } from "../lib/plexPlayerPrefs"; -import { Ctrl, PanelHead, Slider, Toggle } from "./plexPlayerControls"; +import { Ctrl, PanelHead } from "./plexPlayerControls"; +import { PlexPlayerSettings } from "./PlexPlayerSettings"; import { useAutoSkip, useControlsVisibility, @@ -1048,246 +1049,16 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { {menuOpen && ( -
e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - className="border border-white/15 bg-neutral-900/95 absolute bottom-full right-0 mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl" - > -
- {settingsTabs.map((tab) => ( - - ))} -
- - {activeTab === "sync" && ( - <> - patchPrefs({ subOffset: v })} - onReset={() => patchPrefs({ subOffset: 0 })} - /> - changeAudioOffset(0)} - /> - - )} - - {activeTab === "playback" && ( - <> - patchPrefs({ seekStep: v })} - /> - patchPrefs({ fineSeekStep: v })} - /> - patchPrefs({ barAutoHide: v })} - /> - patchPrefs({ autoSkipIntro: v })} - /> - patchPrefs({ autoSkipCredits: v })} - /> - {(prefs.autoSkipIntro || prefs.autoSkipCredits) && ( - patchPrefs({ autoSkipDelay: v })} - /> - )} - - )} - - {activeTab === "subtitle" && ( - <> - patchPrefs({ subSize: v })} - /> - patchPrefs({ subPos: v })} - /> -
- - {t("plex.player.subColor")} - - patchPrefs({ subColor: e.target.value })} - className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" - /> -
-
- - {t("plex.player.subBg")} - -
- - {prefs.subBg !== "transparent" && ( - patchPrefs({ subBg: e.target.value })} - className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" - /> - )} -
-
- patchPrefs({ subShadow: v })} - /> - {prefs.subShadow > 0 && ( -
- - {t("plex.player.subShadowColor")} - - patchPrefs({ subShadowColor: e.target.value })} - className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" - /> -
- )} - - )} - - {activeTab === "clock" && ( - <> - patchPrefs({ clockShow: v })} - /> - patchPrefs({ clock24h: v })} - /> - patchPrefs({ clockSize: v })} - /> -
- - {t("plex.player.clockColor")} - - patchPrefs({ clockColor: e.target.value })} - className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" - /> -
- patchPrefs({ clockDate: v })} - /> - {prefs.clockDate && ( -
- {(["hu", "en"] as const).map((st) => ( - - ))} -
- )} - - )} -
+ )} )} diff --git a/frontend/src/components/PlexPlayerSettings.tsx b/frontend/src/components/PlexPlayerSettings.tsx new file mode 100644 index 00000000..c96ab238 --- /dev/null +++ b/frontend/src/components/PlexPlayerSettings.tsx @@ -0,0 +1,266 @@ +import { type RefObject } from "react"; +import { useTranslation } from "react-i18next"; + +import { Slider, Toggle } from "./plexPlayerControls"; +import { clockParts, type PlexPlayerPrefs } from "../lib/plexPlayerPrefs"; + +type SettingsTab = "sync" | "playback" | "subtitle" | "clock"; + +// The gear/tuning panel of the Plex player — a tabbed (sync / playback / subtitle / clock) sheet of +// Slider/Toggle rows over the per-account prefs. Extracted verbatim from PlexPlayer to keep that +// component focused on the media engine; every row is still `prefs.` + `patchPrefs`, so the +// panel owns no state of its own. `now` is the live wall clock (passed in, not a second ticker) used +// only for the clock-date-style preview buttons. +export function PlexPlayerSettings(props: { + menuRef: RefObject; + settingsTabs: { key: SettingsTab; label: string }[]; + activeTab: SettingsTab; + setSettingsTab: (tab: SettingsTab) => void; + prefs: PlexPlayerPrefs; + patchPrefs: (patch: Partial) => void; + changeAudioOffset: (v: number) => void; + now: Date; +}) { + const { + menuRef, + settingsTabs, + activeTab, + setSettingsTab, + prefs, + patchPrefs, + changeAudioOffset, + now, + } = props; + const { t } = useTranslation(); + return ( +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + className="border border-white/15 bg-neutral-900/95 absolute bottom-full right-0 mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl" + > +
+ {settingsTabs.map((tab) => ( + + ))} +
+ + {activeTab === "sync" && ( + <> + patchPrefs({ subOffset: v })} + onReset={() => patchPrefs({ subOffset: 0 })} + /> + changeAudioOffset(0)} + /> + + )} + + {activeTab === "playback" && ( + <> + patchPrefs({ seekStep: v })} + /> + patchPrefs({ fineSeekStep: v })} + /> + patchPrefs({ barAutoHide: v })} + /> + patchPrefs({ autoSkipIntro: v })} + /> + patchPrefs({ autoSkipCredits: v })} + /> + {(prefs.autoSkipIntro || prefs.autoSkipCredits) && ( + patchPrefs({ autoSkipDelay: v })} + /> + )} + + )} + + {activeTab === "subtitle" && ( + <> + patchPrefs({ subSize: v })} + /> + patchPrefs({ subPos: v })} + /> +
+ {t("plex.player.subColor")} + patchPrefs({ subColor: e.target.value })} + className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" + /> +
+
+ {t("plex.player.subBg")} +
+ + {prefs.subBg !== "transparent" && ( + patchPrefs({ subBg: e.target.value })} + className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" + /> + )} +
+
+ patchPrefs({ subShadow: v })} + /> + {prefs.subShadow > 0 && ( +
+ + {t("plex.player.subShadowColor")} + + patchPrefs({ subShadowColor: e.target.value })} + className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" + /> +
+ )} + + )} + + {activeTab === "clock" && ( + <> + patchPrefs({ clockShow: v })} + /> + patchPrefs({ clock24h: v })} + /> + patchPrefs({ clockSize: v })} + /> +
+ {t("plex.player.clockColor")} + patchPrefs({ clockColor: e.target.value })} + className="h-6 w-10 cursor-pointer rounded-sm bg-transparent" + /> +
+ patchPrefs({ clockDate: v })} + /> + {prefs.clockDate && ( +
+ {(["hu", "en"] as const).map((st) => ( + + ))} +
+ )} + + )} +
+ ); +} diff --git a/frontend/src/lib/playerVolume.test.ts b/frontend/src/lib/playerVolume.test.ts index 606fa739..795573d3 100644 --- a/frontend/src/lib/playerVolume.test.ts +++ b/frontend/src/lib/playerVolume.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from "vitest"; -import { DEFAULT_VOLUME, normalizeVolume, stepVolume } from "./playerVolume"; +import { + DEFAULT_VOLUME, + PLEX_SCALE, + YT_SCALE, + normalizeVolume, + normalizeVolumeIn, + stepVolume, + stepVolumeIn, +} from "./playerVolume"; describe("normalizeVolume", () => { it("keeps a valid level", () => { @@ -56,3 +64,38 @@ describe("stepVolume", () => { expect(stepVolume(NaN, { delta: -5, fallback: 90 })).toBe(85); }); }); + +describe("scale-generic core (shared by both players)", () => { + it("keeps 0..1 floats WITHOUT rounding on the Plex scale", () => { + // PLEX_SCALE.round is false — the HTML5