Update locale files for English, Traditional Chinese, and Simplified Chinese to add new confirmation prompts for volume adjustments; refactor Home and EQPage components to integrate volume confirmation dialogs and enhance user interaction for setting volume levels.

This commit is contained in:
yangy
2026-05-26 10:20:05 +08:00
parent c34571392d
commit 8fcad50e7d
6 changed files with 536 additions and 75 deletions
+148 -49
View File
@@ -15,82 +15,156 @@ 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;
}
const InputIcon = ({ d }: { d: string }) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
<path d={d} />
</svg>
);
interface IOOptionDef {
index: number;
icon: React.ReactNode;
}
const INPUT_OPTIONS: IOOption[] = [
const INPUT_OPTION_DEFS: IOOptionDef[] = [
{
index: 0, label: "USB-B",
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7"><rect x="8" y="6" width="8" height="12" rx="1.5"/><line x1="10" y1="6" x2="10" y2="4"/><line x1="14" y1="6" x2="14" y2="4"/></svg>
index: 0,
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
<rect x="8" y="6" width="8" height="12" rx="1.5" />
<line x1="10" y1="6" x2="10" y2="4" />
<line x1="14" y1="6" x2="14" y2="4" />
</svg>
),
},
{
index: 1, label: "USB-C",
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: 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, label: "同轴",
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: 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, label: "光纤",
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: 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, label: "蓝牙",
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: 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, label: "IIS",
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7"><rect x="3" y="8" width="18" height="10" rx="1.5"/><line x1="7" y1="8" x2="7" y2="18"/><line x1="12" y1="8" x2="12" y2="18"/><line x1="17" y1="8" x2="17" y2="18"/></svg>
index: 5,
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
<rect x="3" y="8" width="18" height="10" rx="1.5" />
<line x1="7" y1="8" x2="7" y2="18" />
<line x1="12" y1="8" x2="12" y2="18" />
<line x1="17" y1="8" x2="17" y2="18" />
</svg>
),
},
];
const OUTPUT_OPTIONS: IOOption[] = [
const OUTPUT_OPTION_DEFS: IOOptionDef[] = [
{
index: 0, label: "XLR",
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: 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, label: "RCA",
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: 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: 2, label: "耳机",
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>
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>
),
},
{
index: 3, label: "XLR/RCA",
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: 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>
),
},
];
const FALLBACK_INPUT_LABELS = ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS"];
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 }) {
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"
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)",
}}
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>
@@ -105,19 +179,36 @@ export default function IOPage() {
const [, setLocation] = useLocation();
const { deviceState, setInput, setOutput } = useDevice();
const ioPageTitle = useMemo(() => {
const sourceText = useMemo((): SourceLocale => {
const lang = deviceState?.language;
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
return pack.source?.pageTitle ?? pack.source?.label ?? "输入输出";
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">
<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>
@@ -126,24 +217,32 @@ export default function IOPage() {
<div className="px-4 pb-32 pt-4 space-y-6">
<div>
<p className="text-[22px] font-bold text-white mb-4 px-1">Input</p>
<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">
{INPUT_OPTIONS.map((opt) => (
<IOCircleButton key={opt.index} option={opt} active={currentInput === opt.index}
onSelect={() => setInput(opt.index)} />
{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">Output</p>
<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">
{OUTPUT_OPTIONS.map((opt) => (
<IOCircleButton key={opt.index} option={opt} active={currentOutput === opt.index}
onSelect={() => setOutput(opt.index)} />
{outputOptions.map((opt) => (
<IOCircleButton
key={opt.index}
option={opt}
active={currentOutput === opt.index}
onSelect={() => setOutput(opt.index)}
/>
))}
</div>
</div>