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 && (
-
+
)}
)}
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 (
+
+ );
+}
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