x9适配
This commit is contained in:
+429
-26
@@ -1,190 +1,593 @@
|
||||
/* ============================================================
|
||||
|
||||
SELECT PAGE — Universal full-screen option picker
|
||||
|
||||
Design: iOS Settings style selection list
|
||||
|
||||
Usage: Navigate to /select with state object
|
||||
|
||||
|
||||
|
||||
State params:
|
||||
|
||||
title - Page title shown in header
|
||||
|
||||
options - Array of option strings
|
||||
|
||||
selected - Currently selected index (number)
|
||||
|
||||
back - Route to navigate back to after selection
|
||||
|
||||
key - Identifier for which setting this controls
|
||||
|
||||
============================================================ */
|
||||
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
import { ChevronLeft, Check } from "lucide-react";
|
||||
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
|
||||
AlertDialog,
|
||||
|
||||
AlertDialogAction,
|
||||
|
||||
AlertDialogCancel,
|
||||
|
||||
AlertDialogContent,
|
||||
|
||||
AlertDialogDescription,
|
||||
|
||||
AlertDialogFooter,
|
||||
|
||||
AlertDialogHeader,
|
||||
|
||||
AlertDialogTitle,
|
||||
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
|
||||
|
||||
|
||||
const iisModeImageUrl = (index: number) =>
|
||||
|
||||
`${import.meta.env.BASE_URL}IIS/iis_app${index + 1}.png`;
|
||||
|
||||
/* ── Key → API field mapping ── */
|
||||
const KEY_TO_SETTING: Record<string, string> = {
|
||||
effect_value: "effect_value",
|
||||
crossfeed_value: "crossfeed_value",
|
||||
language: "language",
|
||||
analogGain: "analogGain",
|
||||
soundStep: "soundStep",
|
||||
bootSound: "bootSound",
|
||||
filterCharacteristic: "pcm",
|
||||
mutePolar: "hdmimutepolar",
|
||||
IISMode: "hdmiType",
|
||||
xlr: "xlr",
|
||||
dacGain: "dacGain",
|
||||
dacImpedance: "dacImpedance",
|
||||
vu: "vu",
|
||||
input: "input",
|
||||
output: "output",
|
||||
screenLight: "screenLight",
|
||||
screenOff: "screenOff",
|
||||
buttonLight: "buttonLight",
|
||||
autoHome: "autoHome",
|
||||
sleep: "sleep",
|
||||
headphone: "peqSelect",
|
||||
|
||||
|
||||
type DacVolumeDirectLocale = {
|
||||
|
||||
label?: string;
|
||||
|
||||
confirmTitle?: string;
|
||||
|
||||
confirm0dB?: string;
|
||||
|
||||
confirm12dB?: string;
|
||||
|
||||
confirmOk?: string;
|
||||
|
||||
confirmCancel?: string;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
type LocaleRoot = {
|
||||
|
||||
dac?: { dacVolumeDirect?: DacVolumeDirectLocale };
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const LOCALE_BY_LANG: Record<number, LocaleRoot> = {
|
||||
|
||||
0: localeEn as LocaleRoot,
|
||||
|
||||
1: localeZhHK as LocaleRoot,
|
||||
|
||||
2: localeZh as LocaleRoot,
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/* ── Key → API field mapping ── */
|
||||
|
||||
const KEY_TO_SETTING: Record<string, string> = {
|
||||
|
||||
effect_value: "effect_value",
|
||||
|
||||
crossfeed_value: "crossfeed_value",
|
||||
|
||||
subwoofer_rate: "subwoofer_rate",
|
||||
|
||||
subwoofer_mix_type: "subwoofer_mix_type",
|
||||
|
||||
language: "language",
|
||||
|
||||
analogGain: "analogGain",
|
||||
|
||||
soundStep: "soundStep",
|
||||
|
||||
bootSound: "bootSound",
|
||||
|
||||
filterCharacteristic: "pcm",
|
||||
|
||||
mutePolar: "hdmimutepolar",
|
||||
|
||||
IISMode: "hdmiType",
|
||||
|
||||
xlr: "xlr",
|
||||
|
||||
dacGain: "dacGain",
|
||||
|
||||
dacImpedance: "dacImpedance",
|
||||
|
||||
dacVolumeDirect: "dacVolumeDirect",
|
||||
|
||||
dacArc: "dacArc",
|
||||
|
||||
vu: "vu",
|
||||
|
||||
input: "input",
|
||||
|
||||
output: "output",
|
||||
|
||||
screenLight: "screenLight",
|
||||
|
||||
screenOff: "screenOff",
|
||||
|
||||
buttonLight: "buttonLight",
|
||||
|
||||
autoHome: "autoHome",
|
||||
|
||||
sleep: "sleep",
|
||||
|
||||
headphone: "peqSelect",
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Global state for select page (to avoid URL params)
|
||||
|
||||
export let selectPageState: {
|
||||
|
||||
title: string;
|
||||
|
||||
options: string[];
|
||||
|
||||
selected: number;
|
||||
|
||||
back: string;
|
||||
|
||||
key: string;
|
||||
|
||||
} | null = null;
|
||||
|
||||
|
||||
|
||||
// Last select result for pages that need local state update
|
||||
|
||||
export let selectPageResult: { key: string; selected: number } | null = null;
|
||||
|
||||
|
||||
|
||||
export function consumeSelectPageResult() {
|
||||
|
||||
const result = selectPageResult;
|
||||
|
||||
selectPageResult = null;
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper function to set select page state
|
||||
|
||||
export function navigateToSelect(title: string, options: string[], selected: number, back: string, key: string) {
|
||||
|
||||
selectPageState = { title, options, selected, back, key };
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function SelectPage() {
|
||||
|
||||
const [location, setLocation] = useLocation();
|
||||
const { updateSetting } = useDevice();
|
||||
|
||||
const { updateSetting, deviceState } = useDevice();
|
||||
|
||||
const [state, setState] = useState<{
|
||||
|
||||
title: string;
|
||||
|
||||
options: string[];
|
||||
|
||||
selected: number;
|
||||
|
||||
back: string;
|
||||
|
||||
key: string;
|
||||
|
||||
} | null>(() => selectPageState);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [passthroughConfirmIdx, setPassthroughConfirmIdx] = useState<number | null>(null);
|
||||
|
||||
|
||||
|
||||
const langIdx = deviceState?.language ?? 2;
|
||||
|
||||
const dacVolumeDirectText = (LOCALE_BY_LANG[langIdx] ?? localeZh as LocaleRoot).dac?.dacVolumeDirect;
|
||||
|
||||
|
||||
|
||||
const passthroughConfirmCopy = useMemo(() => {
|
||||
|
||||
if (passthroughConfirmIdx === null) return null;
|
||||
|
||||
const message =
|
||||
|
||||
passthroughConfirmIdx === 1
|
||||
|
||||
? dacVolumeDirectText?.confirm0dB
|
||||
|
||||
: passthroughConfirmIdx === 2
|
||||
|
||||
? dacVolumeDirectText?.confirm12dB
|
||||
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
title: dacVolumeDirectText?.confirmTitle ?? "提示",
|
||||
|
||||
message:
|
||||
|
||||
message ??
|
||||
|
||||
(passthroughConfirmIdx === 1
|
||||
|
||||
? "直通模式会将音量调到最大,并且不可调节"
|
||||
|
||||
: "直通模式会将音量调到-12dB,并且不可调节"),
|
||||
|
||||
ok: dacVolumeDirectText?.confirmOk ?? "确认",
|
||||
|
||||
cancel: dacVolumeDirectText?.confirmCancel ?? "取消",
|
||||
|
||||
};
|
||||
|
||||
}, [passthroughConfirmIdx, dacVolumeDirectText]);
|
||||
|
||||
|
||||
|
||||
// Re-read global state whenever we enter /select (avoids stale key/options after remount)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (location === "/select" && selectPageState) {
|
||||
|
||||
setState({ ...selectPageState });
|
||||
|
||||
}
|
||||
|
||||
}, [location]);
|
||||
|
||||
|
||||
|
||||
if (!state) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const { title, options, selected: selectedIdx, back, key } = state;
|
||||
|
||||
const handleSelect = async (idx: number) => {
|
||||
|
||||
|
||||
const applySelect = async (idx: number) => {
|
||||
|
||||
if (saving) return;
|
||||
|
||||
selectPageResult = { key, selected: idx };
|
||||
|
||||
|
||||
|
||||
const apiField = KEY_TO_SETTING[key];
|
||||
|
||||
if (apiField) {
|
||||
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
|
||||
await updateSetting({ [apiField]: idx });
|
||||
|
||||
} finally {
|
||||
|
||||
setSaving(false);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
selectPageState = null;
|
||||
|
||||
setLocation(back);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSelect = async (idx: number) => {
|
||||
|
||||
if (saving) return;
|
||||
|
||||
if (key === "dacVolumeDirect" && (idx === 1 || idx === 2) && idx !== selectedIdx) {
|
||||
|
||||
setPassthroughConfirmIdx(idx);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
await applySelect(idx);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handlePassthroughConfirm = async () => {
|
||||
|
||||
if (passthroughConfirmIdx === null) return;
|
||||
|
||||
const idx = passthroughConfirmIdx;
|
||||
|
||||
setPassthroughConfirmIdx(null);
|
||||
|
||||
await applySelect(idx);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="min-h-screen bg-black">
|
||||
|
||||
{/* ── Header ── */}
|
||||
|
||||
<div className="page-header">
|
||||
|
||||
<button
|
||||
|
||||
onClick={() => setLocation(back)}
|
||||
|
||||
className="mr-4 text-white/60 active:text-white transition-colors">
|
||||
|
||||
<ChevronLeft size={24} />
|
||||
|
||||
</button>
|
||||
|
||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{title}</h1>
|
||||
|
||||
<div className="w-8" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="px-4 pt-4 pb-24">
|
||||
|
||||
{key === "IISMode" ? (
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-3">
|
||||
|
||||
{options.map((opt, idx) => {
|
||||
|
||||
const isSelected = idx === selectedIdx;
|
||||
|
||||
return (
|
||||
|
||||
<button
|
||||
|
||||
key={idx}
|
||||
|
||||
type="button"
|
||||
|
||||
disabled={saving}
|
||||
|
||||
className={cn(
|
||||
|
||||
"flex flex-col items-center gap-2 rounded-xl border-2 p-1.5 transition-all active:scale-[0.98] disabled:opacity-60",
|
||||
|
||||
isSelected
|
||||
|
||||
? "border-[#00FFF6] shadow-[0_0_14px_rgba(0,255,246,0.35)]"
|
||||
|
||||
: "border-white/10",
|
||||
|
||||
)}
|
||||
|
||||
onClick={() => void handleSelect(idx)}
|
||||
|
||||
aria-pressed={isSelected}
|
||||
|
||||
aria-label={opt}
|
||||
|
||||
>
|
||||
|
||||
<img
|
||||
|
||||
src={iisModeImageUrl(idx)}
|
||||
|
||||
alt={opt}
|
||||
|
||||
className="block w-full h-auto rounded-lg bg-black/40"
|
||||
|
||||
loading="lazy"
|
||||
|
||||
/>
|
||||
|
||||
<span
|
||||
|
||||
className={cn(
|
||||
|
||||
"w-6 h-6 rounded-full flex items-center justify-center shrink-0",
|
||||
|
||||
isSelected
|
||||
|
||||
? "bg-[#00FFF6] text-black shadow-[0_0_10px_rgba(0,255,246,0.45)]"
|
||||
|
||||
: "border-2 border-white/20",
|
||||
|
||||
)}
|
||||
|
||||
>
|
||||
|
||||
{isSelected && <Check size={12} strokeWidth={3} />}
|
||||
|
||||
</span>
|
||||
|
||||
</button>
|
||||
|
||||
);
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
) : (
|
||||
|
||||
<div className="ios-list-group">
|
||||
|
||||
{options.map((opt, idx) => {
|
||||
|
||||
const isSelected = idx === selectedIdx;
|
||||
|
||||
return (
|
||||
|
||||
<button
|
||||
|
||||
key={idx}
|
||||
|
||||
className="ios-list-row w-full text-left active:bg-white/5 transition-colors"
|
||||
|
||||
onClick={() => void handleSelect(idx)}
|
||||
|
||||
disabled={saving}
|
||||
|
||||
>
|
||||
|
||||
<span className={`flex-1 text-[16px] ${isSelected ? "text-white font-medium" : "text-white/75"}`}>
|
||||
|
||||
{opt}
|
||||
|
||||
</span>
|
||||
|
||||
{isSelected && (
|
||||
|
||||
<Check size={18} style={{ color: "#00FFF6" }} />
|
||||
|
||||
)}
|
||||
|
||||
</button>
|
||||
|
||||
);
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<AlertDialog
|
||||
|
||||
open={passthroughConfirmIdx !== null}
|
||||
|
||||
onOpenChange={(open) => {
|
||||
|
||||
if (!open) setPassthroughConfirmIdx(null);
|
||||
|
||||
}}
|
||||
|
||||
>
|
||||
|
||||
<AlertDialogContent className="top-[42%] border-white/10 bg-zinc-900 text-white sm:max-w-md">
|
||||
|
||||
<AlertDialogHeader>
|
||||
|
||||
<AlertDialogTitle className="text-white">
|
||||
|
||||
{passthroughConfirmCopy?.title}
|
||||
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription className="text-white/60">
|
||||
|
||||
{passthroughConfirmCopy?.message}
|
||||
|
||||
</AlertDialogDescription>
|
||||
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
|
||||
<AlertDialogCancel className="border-white/20 bg-transparent text-white hover:bg-white/10">
|
||||
|
||||
{passthroughConfirmCopy?.cancel}
|
||||
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
|
||||
className="bg-[#00FFF6] text-black hover:bg-[#00FFF6]/90 focus-visible:ring-[#00FFF6]"
|
||||
|
||||
disabled={saving}
|
||||
|
||||
onClick={() => void handlePassthroughConfirm()}
|
||||
|
||||
>
|
||||
|
||||
{passthroughConfirmCopy?.ok}
|
||||
|
||||
</AlertDialogAction>
|
||||
|
||||
</AlertDialogFooter>
|
||||
|
||||
</AlertDialogContent>
|
||||
|
||||
</AlertDialog>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user