287 lines
8.9 KiB
TypeScript
287 lines
8.9 KiB
TypeScript
/* ============================================================
|
|
IO PAGE — Input / Output Selection
|
|
Design: Reference luxsin_x8_输入输出.jpg
|
|
- Circular icon buttons in a grid (3 cols)
|
|
- Active = cyan fill + glow
|
|
- Inactive = dark gray circle
|
|
============================================================ */
|
|
import { useDevice } from "@/contexts/DeviceContext";
|
|
import { cn } from "@/lib/utils";
|
|
import BottomNav from "@/components/BottomNav";
|
|
import { ChevronLeft } from "lucide-react";
|
|
import { useLocation } from "wouter";
|
|
import { useMemo } from "react";
|
|
import localeZh from "@/locales/data-zh.json";
|
|
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
import localeEn from "@/locales/data-en.json";
|
|
|
|
type SourceLocale = NonNullable<(typeof localeZh)["source"]>;
|
|
|
|
interface IOOption {
|
|
index: number;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
interface IOOptionDef {
|
|
index: number;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
const INPUT_OPTION_DEFS: IOOptionDef[] = [
|
|
{
|
|
index: 0,
|
|
icon: (
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<g fill="none">
|
|
<path d="M3 12h18v9H3zm2-9h14v9H5z" />
|
|
<g stroke="currentColor" stroke-width="2">
|
|
<path d="M3 12h18v9H3zm16 0H5V3h14z" />
|
|
<path d="M9 7.496h.004V7.5H9zm6 0h.004V7.5H15z" />
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
|
|
),
|
|
},
|
|
{
|
|
index: 1,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<rect x="5" y="9" width="14" height="6" rx="3" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 2,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<circle cx="12" cy="12" r="8" />
|
|
<circle cx="12" cy="12" r="3" fill="currentColor" stroke="none" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 3,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<rect x="7" y="7" width="10" height="10" rx="1" />
|
|
<line x1="12" y1="3" x2="12" y2="7" />
|
|
<line x1="12" y1="17" x2="12" y2="21" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 4,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 5,
|
|
icon: (
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 16 16">
|
|
<path d="M0 0h16v16H0z" fill="none" />
|
|
<g fill="currentColor">
|
|
<path d="M2.5 7a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1z" />
|
|
<path d="M1 5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h.293l.707.707a1 1 0 0 0 .707.293h10.586a1 1 0 0 0 .707-.293l.707-.707H15a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0 1h14v3h-.293a1 1 0 0 0-.707.293l-.707.707H2.707L2 9.293A1 1 0 0 0 1.293 9H1z" />
|
|
</g>
|
|
</svg>
|
|
|
|
),
|
|
},
|
|
{
|
|
index: 6,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<circle cx="12" cy="12" r="8" />
|
|
<circle cx="12" cy="12" r="3" fill="currentColor" stroke="none" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 7,
|
|
icon: (
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<g fill="currentColor">
|
|
<path d="M10 4.5h1v2h-1zm4 0h-1v2h1z" />
|
|
<path fill-rule="evenodd" d="M7 8.5v-7h10v7h2v11a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-11zm2-5h6v5H9zm8 7H7v9a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z" clip-rule="evenodd" />
|
|
</g>
|
|
</svg>
|
|
|
|
),
|
|
},
|
|
];
|
|
|
|
const OUTPUT_OPTION_DEFS: IOOptionDef[] = [
|
|
{
|
|
index: 0,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<circle cx="12" cy="12" r="8" />
|
|
<circle cx="9" cy="10" r="1.5" fill="currentColor" stroke="none" />
|
|
<circle cx="15" cy="10" r="1.5" fill="currentColor" stroke="none" />
|
|
<circle cx="12" cy="15" r="1.5" fill="currentColor" stroke="none" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 1,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<circle cx="12" cy="12" r="8" />
|
|
<circle cx="12" cy="12" r="3" fill="currentColor" stroke="none" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 3,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<circle cx="8" cy="12" r="5" />
|
|
<circle cx="16" cy="12" r="5" />
|
|
<circle cx="8" cy="12" r="1.5" fill="currentColor" stroke="none" />
|
|
<circle cx="16" cy="12" r="1.5" fill="currentColor" stroke="none" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
index: 2,
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
|
<path d="M3 18v-6a9 9 0 0 1 18 0v6" />
|
|
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3z" />
|
|
<path d="M3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z" />
|
|
</svg>
|
|
),
|
|
},
|
|
];
|
|
|
|
const FALLBACK_INPUT_LABELS = ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "HDMI-EARC", "RCA", "USB Driver"];
|
|
const FALLBACK_OUTPUT_LABELS = ["XLR", "RCA", "XLR/RCA", "耳机"];
|
|
|
|
function withLabels(defs: IOOptionDef[], labels: string[] | undefined, fallback: string[]): IOOption[] {
|
|
return defs.map((def, i) => ({
|
|
...def,
|
|
label: labels?.[i] ?? fallback[i] ?? String(def.index),
|
|
}));
|
|
}
|
|
|
|
function IOCircleButton({
|
|
option,
|
|
active,
|
|
onSelect,
|
|
}: {
|
|
option: IOOption;
|
|
active: boolean;
|
|
onSelect: () => void;
|
|
}) {
|
|
return (
|
|
<button onClick={onSelect} className="flex flex-col items-center gap-2.5">
|
|
<div
|
|
className={cn(
|
|
"w-[68px] h-[68px] rounded-full flex items-center justify-center transition-all duration-200",
|
|
active ? "text-black" : "text-white/55 border border-white/10",
|
|
)}
|
|
style={
|
|
active
|
|
? {
|
|
background: "#00FFF6",
|
|
boxShadow: "0 0 24px rgba(0,255,246,0.55), 0 0 8px rgba(0,255,246,0.3)",
|
|
}
|
|
: { background: "rgba(44,44,46,0.9)" }
|
|
}
|
|
>
|
|
{option.icon}
|
|
</div>
|
|
<span className={cn("text-[13px] font-medium", active ? "text-[#00FFF6]" : "text-white/45")}>
|
|
{option.label}
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default function IOPage() {
|
|
const [, setLocation] = useLocation();
|
|
const { deviceState, setInput, setOutput } = useDevice();
|
|
|
|
const sourceText = useMemo((): SourceLocale => {
|
|
const lang = deviceState?.language;
|
|
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
|
return (pack.source ?? {}) as SourceLocale;
|
|
}, [deviceState?.language]);
|
|
|
|
const inputOptions = useMemo(
|
|
() => withLabels(INPUT_OPTION_DEFS, sourceText.inputOptions, FALLBACK_INPUT_LABELS),
|
|
[sourceText.inputOptions],
|
|
);
|
|
|
|
const outputOptions = useMemo(
|
|
() => withLabels(OUTPUT_OPTION_DEFS, sourceText.outputOptions, FALLBACK_OUTPUT_LABELS),
|
|
[sourceText.outputOptions],
|
|
);
|
|
|
|
const ioPageTitle = sourceText.pageTitle ?? sourceText.label ?? "输入输出";
|
|
const inputSectionTitle = sourceText.inputSection ?? "输入";
|
|
const outputSectionTitle = sourceText.outputSection ?? "输出";
|
|
|
|
const currentInput = deviceState?.input ?? 1;
|
|
const currentOutput = deviceState?.output ?? 2;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-black">
|
|
<div className="page-header">
|
|
<button
|
|
onClick={() => setLocation("/")}
|
|
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">{ioPageTitle}</h1>
|
|
<div className="w-8" />
|
|
</div>
|
|
|
|
<div className="px-4 pb-32 pt-4 space-y-6">
|
|
<div>
|
|
<p className="text-[22px] font-bold text-white mb-4 px-1">{inputSectionTitle}</p>
|
|
<div className="ios-list-group p-5">
|
|
<div className="grid grid-cols-3 gap-x-2 gap-y-6">
|
|
{inputOptions.map((opt) => (
|
|
<IOCircleButton
|
|
key={opt.index}
|
|
option={opt}
|
|
active={currentInput === opt.index}
|
|
onSelect={() => setInput(opt.index)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-[22px] font-bold text-white mb-4 px-1">{outputSectionTitle}</p>
|
|
<div className="ios-list-group p-5">
|
|
<div className="grid grid-cols-3 gap-x-2 gap-y-6">
|
|
{outputOptions.map((opt) => (
|
|
<IOCircleButton
|
|
key={opt.index}
|
|
option={opt}
|
|
active={currentOutput === opt.index}
|
|
onSelect={() => setOutput(opt.index)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<BottomNav />
|
|
</div>
|
|
);
|
|
}
|