/* 聆譯 — main app */
const { useState, useEffect, useRef } = React;
const D = window.APP_DATA;
const lang = (id) => D.LANG_BY[id];

/* viewport */
function useIsDesktop() {
  const force = new URLSearchParams(location.search).get("view");
  const [d, setD] = useState(() => force ? force === "desktop" : window.innerWidth >= 1024);
  useEffect(() => {
    if (force) return;
    const on = () => setD(window.innerWidth >= 1024);
    window.addEventListener("resize", on);
    return () => window.removeEventListener("resize", on);
  }, []);
  return d;
}

/* auto-scroll helper */
function useStickyBottom(dep) {
  const ref = useRef(null);
  useEffect(() => { const el = ref.current; if (el) el.scrollTop = el.scrollHeight; }, [dep]);
  return ref;
}

/* ============================== INDIVIDUAL ============================== */
function IndividualView({ settings, desktop, onActiveChange, onConn }) {
  const [source, setSource] = useState("mic");
  const [inLang, setInLang] = useState(settings.soloListen);
  const [outLang, setOutLang] = useState(settings.soloTarget);
  const [active, setActive] = useState(false);
  const [voice, setVoice] = useState(settings.voiceSolo);
  const [picker, setPicker] = useState(null);
  const { lines, partial, mode, errorMsg } = useTranslator(active, {
    source, outLang, voice, settings, onStatus: onConn,
    recordMeta: { title: "個人翻譯", fromLang: inLang, toLang: outLang, kind: "solo" },
  });
  const duck = 100 - settings.origVol;

  useEffect(() => { onActiveChange(active); }, [active]);
  const scrollRef = useStickyBottom("" + lines.length + (partial ? partial.out.length : 0));

  // 換語言:即時更新(live 模式由引擎送 session.update,不重連)
  const changeLang = (which, id) => { if (which === "in") setInLang(id); else setOutLang(id); setPicker(null); };
  const hasContent = lines.length > 0 || partial;
  const statusLabel = !active ? "待命中"
    : mode === "connecting" ? "連線中…"
    : mode === "error" ? "無法連線" : "翻譯中";

  return (
    <div className="iv">
      <div className="strip">
        <div className="seg">
          <button className={source === "mic" ? "on" : ""} onClick={() => setSource("mic")}><Svg>{I.mic}</Svg>我的麥克風</button>
          <button className={source === "tab" ? "on" : ""} onClick={() => setSource("tab")}><Svg>{I.tab}</Svg>此分頁聲音</button>
        </div>
        <div className="langbar">
          <button className="langchip" onClick={() => setPicker("in")}>
            <span className="lc-txt"><span className="lc-role">聆聽</span><span className="lc-name">{lang(inLang).native}</span></span>
          </button>
          <span className="lang-arrow"><Svg>{I.swap}</Svg></span>
          <button className="langchip out" onClick={() => setPicker("out")}>
            <span className="lc-txt"><span className="lc-role">翻譯為</span><span className="lc-name">{lang(outLang).native}<span className="caret"><Svg>{I.caretDown}</Svg></span></span></span>
          </button>
        </div>
      </div>

      <div className="stage">
        <div className="stage-head">
          <span className={"live" + (active ? " active" : "")}>
            <span className="ld"></span>{statusLabel}
            <span style={{ opacity: .55 }}>·</span><span style={{ opacity: .85 }}>{source === "mic" ? "麥克風" : "分頁聲音"}</span>
          </span>
          <span className="badges">
            {voice && settings.mimic && <span className="sbadge mimic"><Svg>{I.spark}</Svg>模仿音色</span>}
            {voice && <span className="sbadge">原音 ↓{duck}%</span>}
          </span>
        </div>
        {hasContent ? (
          <div className="subs" ref={scrollRef}>
            {lines.map(l => (
              <div key={l.id} className="bubble">
                <span className="bub-time">{fmtClock(l.ts)}</span>
                <div className="bub-text">{l.out}</div>
              </div>
            ))}
            {partial && <div className="bubble partial"><div className="bub-text">{partial.out}</div></div>}
          </div>
        ) : (
          <div className="stage-empty">
            <span className="ring"><Svg>{mode === "error" ? I.alert : I.ear}</Svg></span>
            <h4>{mode === "error" ? "無法連線" : active ? "連線中…" : "準備就緒"}</h4>
            <p>{mode === "error"
              ? (errorMsg || "請檢查網路或後台金鑰設定。")
              : `按下「開始」即會持續聆聽${source === "mic" ? "麥克風" : "目前分頁"}的聲音,並即時翻譯為${lang(outLang).native}。`}</p>
          </div>
        )}
      </div>

      <div className="dock">
        <button className={"btn-power" + (active ? " stop" : "")} onClick={() => setActive(a => !a)}>
          <Svg>{active ? I.stop : I.power}</Svg>{active ? "停止" : "開始"}
        </button>
        {desktop
          ? <button className={"btn-voice" + (voice ? " on" : "")} onClick={() => setVoice(v => !v)}>
              <Svg>{voice ? I.speaker : I.speakerOff}</Svg>{voice ? "翻譯發聲 · 開" : "翻譯發聲 · 關"}
            </button>
          : <button className={"btn-icon" + (voice ? " on" : "")} onClick={() => setVoice(v => !v)} title="發聲 / 不發聲">
              <Svg>{voice ? I.speaker : I.speakerOff}</Svg>{voice && <span className="duck">發聲</span>}
            </button>}
      </div>

      {picker === "in" && <LangPicker title="聆聽語言" subtitle="你正在聽的聲音是哪種語言" current={inLang} exclude={outLang} onPick={(id) => changeLang("in", id)} onClose={() => setPicker(null)} />}
      {picker === "out" && <LangPicker title="翻譯為" subtitle="字幕與語音要輸出的語言" current={outLang} exclude={inLang} onPick={(id) => changeLang("out", id)} onClose={() => setPicker(null)} />}
    </div>
  );
}

/* ============================== CONVERSATION ============================== */
function ConvWindow({ role, av, who, outLang, otherLang, flipped, voiceDefault, mimic, settings, onConn, onActiveChange }) {
  const [active, setActive] = useState(false);
  const [voice, setVoice] = useState(voiceDefault);
  const [out, setOut] = useState(outLang);
  const [picker, setPicker] = useState(false);
  // 每個方向各一個獨立 session(各自的麥克風串流,音軌分離、不混流)
  const { lines, partial, mode, errorMsg } = useTranslator(active, {
    source: "mic", outLang: out, voice, settings, onStatus: onConn,
    recordMeta: { title: role === "me" ? "對話 · 我說的話" : "對話 · 對方說的話", toLang: out, kind: "conv-" + role },
  });
  useEffect(() => { onActiveChange(role, active); }, [active]);
  const scrollRef = useStickyBottom("" + lines.length + (partial ? partial.out.length : 0));
  const hasContent = lines.length > 0 || partial;

  return (
    <div className={"win" + (flipped ? " flipped" : "")}>
      <span className="win-lang-badge" style={flipped ? { transform: "rotate(180deg)" } : null}>{lang(out).code}</span>
      <div className="win-inner">
        <div className="win-top">
          <span className="who">
            <span className={"av " + av}>{av === "me" ? <Svg>{I.user}</Svg> : <Svg>{I.chat}</Svg>}</span>
            <span className="wt-txt"><span className="wt-role">{who}</span></span>
          </span>
          <button className="win-mini-lang" onClick={() => setPicker(true)}>顯示 <b>{lang(out).native}</b><span className="ar"><Svg>{I.caretDown}</Svg></span></button>
        </div>
        <div className="win-stage" style={flipped ? { transform: "rotate(180deg)" } : null}>
          {hasContent ? (
            <div className="win-subs" ref={scrollRef}>
              {lines.map(l => (
                <div key={l.id} className="bubble win-bubble">
                  <span className="bub-time">{fmtClock(l.ts)}</span>
                  <div className="bub-text">{l.out}</div>
                </div>
              ))}
              {partial && <div className="bubble win-bubble partial"><div className="bub-text">{partial.out}</div></div>}
            </div>
          ) : (
            <div className="win-empty">{
              mode === "error" ? (errorMsg || "無法連線,請稍後再試。")
              : mode === "connecting" ? "連線中…"
              : role === "me" ? "按開始 · 翻譯我說的話給對方" : "按開始 · 翻譯對方說的話給我"
            }</div>
          )}
        </div>
        <div className="win-bottom">
          <button className={"win-power" + (active ? " stop" : "")} onClick={() => setActive(a => !a)}><Svg>{active ? I.stop : I.power}</Svg>{active ? "停止" : "開始"}</button>
          <button className={"win-vbtn" + (voice ? " on" : "")} onClick={() => setVoice(v => !v)} title="發聲 / 不發聲"><Svg>{voice ? I.speaker : I.speakerOff}</Svg></button>
        </div>
      </div>
      {picker && <LangPicker title="顯示語言" subtitle="這個視窗要呈現的語言" current={out} exclude={otherLang} onPick={(id) => { setOut(id); setPicker(false); }} onClose={() => setPicker(false)} />}
    </div>
  );
}

/* 自動 / 手動 切換 */
function ConvModeToggle({ convMode, setConvMode }) {
  return (
    <div className="convmode">
      <button className={convMode === "auto" ? "on" : ""} onClick={() => setConvMode("auto")}>自動</button>
      <button className={convMode === "manual" ? "on" : ""} onClick={() => setConvMode("manual")}>手動</button>
    </div>
  );
}

/* 對話用:依 convMode 切換自動 / 手動,並記住選擇 */
function ConversationModes({ settings, onAnyActive, onConn }) {
  const [convMode, setConvMode] = useState(() => localStorage.getItem("ly_convmode") || "auto");
  useEffect(() => { localStorage.setItem("ly_convmode", convMode); }, [convMode]);
  return convMode === "auto"
    ? <AutoConversationView settings={settings} onAnyActive={onAnyActive} onConn={onConn} convMode={convMode} setConvMode={setConvMode} />
    : <ConversationView settings={settings} onAnyActive={onAnyActive} onConn={onConn} convMode={convMode} setConvMode={setConvMode} />;
}

/* ===== 手動模式(輪流):上一版,作為備援 ===== */
function ConversationView({ settings, onAnyActive, onConn, convMode, setConvMode }) {
  const [flipped, setFlipped] = useState(false);
  const act = useRef({ me: false, them: false });
  const handleActive = (role, v) => { act.current[role] = v; onAnyActive(act.current.me || act.current.them); };
  return (
    <div className="cv">
      <div className="cv-windows">
        <ConvWindow role="me" av="me" who="給對方看 · 我說的話" outLang={settings.convTo} otherLang={settings.convFrom}
          flipped={flipped} voiceDefault={settings.voiceMe} mimic={settings.mimic} settings={settings} onConn={onConn} onActiveChange={handleActive} />
        <ConvWindow role="them" av="them" who="我看 · 對方說的話" outLang={settings.convFrom} otherLang={settings.convTo}
          flipped={false} voiceDefault={settings.voiceThem} mimic={settings.mimic} settings={settings} onConn={onConn} onActiveChange={handleActive} />
      </div>
      <div className="cv-foot">
        <ConvModeToggle convMode={convMode} setConvMode={setConvMode} />
        <button className={"flip-btn" + (flipped ? " flipped" : "")} onClick={() => setFlipped(f => !f)}><Svg>{I.flip}</Svg>{flipped ? "取消翻轉" : "翻轉對方視窗"}</button>
      </div>
    </div>
  );
}

/* ===== 自動模式:一支麥克風、語言自動分流、播音時暫停收音 ===== */
function useAuto(active, opts) {
  const { settings, onConn, voiceMe } = opts;
  const [otherLines, setOtherLines] = useState([]);
  const [otherPartial, setOtherPartial] = useState(null);
  const [meLines, setMeLines] = useState([]);
  const [mePartial, setMePartial] = useState(null);
  const [mode, setMode] = useState("idle");
  const [errorMsg, setErrorMsg] = useState("");
  const [gated, setGated] = useState(false);
  const autoRef = useRef(null);
  const status = onConn || function () {};
  const mkLine = (prev, out, src) => [...prev, { id: prev.length + "-" + Math.random().toString(36).slice(2, 6), out, src, ts: Date.now() }];

  useEffect(() => {
    if (!active) {
      if (autoRef.current) { autoRef.current.stop(); autoRef.current = null; }
      setOtherLines([]); setOtherPartial(null); setMeLines([]); setMePartial(null);
      setMode("idle"); setErrorMsg(""); setGated(false);
      return;
    }
    let cancelled = false;
    setOtherLines([]); setOtherPartial(null); setMeLines([]); setMePartial(null); setErrorMsg("");
    setMode("connecting"); status("connecting");
    const auto = new LingyiAuto({
      voiceMe,
      onOtherPartial: (t) => { if (!cancelled) setOtherPartial({ out: t }); },
      onOtherFinal: ({ out, src }) => { if (!cancelled) { setOtherLines((p) => mkLine(p, out, src)); setOtherPartial(null); } },
      onMePartial: (t) => { if (!cancelled) setMePartial({ out: t }); },
      onMeFinal: ({ out, src }) => { if (!cancelled) { setMeLines((p) => mkLine(p, out, src)); setMePartial(null); } },
      onStatus: (st) => { if (!cancelled) status(st); },
      onError: (e) => { if (!cancelled) setErrorMsg(e.message || ""); },
      onGate: (g) => { if (!cancelled) setGated(g); },
    });
    autoRef.current = auto;
    auto.start({
      myLang: settings.convFrom, otherLang: settings.convTo,
      backendUrl: settings.backendUrl, overrideKey: settings.apiKey,
      inputDeviceId: settings.inputDevice, outputDeviceId: settings.outputDevice,
      origVol: settings.origVol, transVol: settings.transVol,
    })
      .then(() => { if (!cancelled) setMode("live"); })
      .catch((err) => {
        try { auto.stop(); } catch (e) { /* 釋放可能已開啟的麥克風/連線 */ }
        if (cancelled) return;
        autoRef.current = null; setMode("error"); status("err");
        setErrorMsg(err && err.code === "no_api_key"
          ? "尚未連線:請到「設定 → 連線」確認已填入 OpenAI 金鑰。"
          : (err && err.message) || "無法連線,請稍後再試。");
      });
    return () => { cancelled = true; if (autoRef.current) { autoRef.current.stop(); autoRef.current = null; } };
  }, [active]);

  useEffect(() => { if (autoRef.current) autoRef.current.setVoiceMe(voiceMe); }, [voiceMe]);
  useEffect(() => { if (autoRef.current) autoRef.current.setVolumes(settings.origVol, settings.transVol); }, [settings.origVol, settings.transVol]);

  return { otherLines, otherPartial, meLines, mePartial, mode, errorMsg, gated };
}

function AutoWindow({ who, av, langId, lines, partial, flipped, mode, errorMsg, active }) {
  const scrollRef = useStickyBottom("" + lines.length + (partial ? partial.out.length : 0));
  const hasContent = lines.length > 0 || partial;
  return (
    <div className={"win" + (flipped ? " flipped" : "")}>
      <span className="win-lang-badge" style={flipped ? { transform: "rotate(180deg)" } : null}>{lang(langId).code}</span>
      <div className="win-inner">
        <div className="win-top">
          <span className="who">
            <span className={"av " + av}>{av === "me" ? <Svg>{I.user}</Svg> : <Svg>{I.chat}</Svg>}</span>
            <span className="wt-txt"><span className="wt-role">{who}</span></span>
          </span>
          <span className="win-mini-lang static">顯示 <b>{lang(langId).native}</b></span>
        </div>
        <div className="win-stage" style={flipped ? { transform: "rotate(180deg)" } : null}>
          {hasContent ? (
            <div className="win-subs" ref={scrollRef}>
              {lines.map(l => (
                <div key={l.id} className="bubble win-bubble">
                  <span className="bub-time">{fmtClock(l.ts)}</span>
                  <div className="bub-text">{l.out}</div>
                </div>
              ))}
              {partial && <div className="bubble win-bubble partial"><div className="bub-text">{partial.out}</div></div>}
            </div>
          ) : (
            <div className="win-empty">{
              mode === "error" ? (errorMsg || "無法連線")
              : mode === "connecting" ? "連線中…"
              : active ? "聆聽中…請開始說話" : "按「開始自動聆聽」"
            }</div>
          )}
        </div>
      </div>
    </div>
  );
}

function AutoConversationView({ settings, onAnyActive, onConn, convMode, setConvMode }) {
  const [active, setActive] = useState(false);
  const [flipped, setFlipped] = useState(false);
  const [voiceMe, setVoiceMe] = useState(settings.voiceThem);
  const a = useAuto(active, { settings, onConn, voiceMe });
  useEffect(() => { onAnyActive(active); }, [active]);

  const statusText = !active ? "" : a.gated ? "播放翻譯中 · 暫停收音(防迴授)"
    : a.mode === "connecting" ? "連線中…"
    : a.mode === "live" ? "聆聽中 · 語言自動分流" : "";

  return (
    <div className="cv">
      <div className="auto-status">
        <ConvModeToggle convMode={convMode} setConvMode={setConvMode} />
        {statusText && <span className={"auto-stat" + (a.gated ? " gated" : "")}><span className="dot"></span>{statusText}</span>}
      </div>
      <div className="cv-windows">
        <AutoWindow who="給對方看 · 我說的話" av="me" langId={settings.convTo} lines={a.otherLines} partial={a.otherPartial}
          flipped={flipped} mode={a.mode} errorMsg={a.errorMsg} active={active} />
        <AutoWindow who="我看 · 對方說的話" av="them" langId={settings.convFrom} lines={a.meLines} partial={a.mePartial}
          flipped={false} mode={a.mode} errorMsg={a.errorMsg} active={active} />
      </div>
      <div className="cv-foot auto-foot">
        <button className={"flip-btn" + (flipped ? " flipped" : "")} onClick={() => setFlipped(f => !f)}><Svg>{I.flip}</Svg>{flipped ? "取消翻轉" : "翻轉對方視窗"}</button>
        <button className={"auto-power" + (active ? " stop" : "")} onClick={() => setActive(v => !v)}>
          <Svg>{active ? I.stop : I.power}</Svg>{active ? "停止" : "開始自動聆聽"}
        </button>
        <button className={"auto-vbtn" + (voiceMe ? " on" : "")} onClick={() => setVoiceMe(v => !v)} title="對方語音(英→中)是否播出">
          <Svg>{voiceMe ? I.speaker : I.speakerOff}</Svg>
        </button>
      </div>
    </div>
  );
}

/* shared bits */
function ModeSwitch({ mode, setMode }) {
  return (
    <div className="modeswitch">
      <button className={mode === "solo" ? "active" : ""} onClick={() => setMode("solo")}><span className="mi"><Svg>{I.user}</Svg></span>個人用</button>
      <button className={mode === "pair" ? "active" : ""} onClick={() => setMode("pair")}><span className="mi"><Svg>{I.chat}</Svg></span>對話用</button>
    </div>
  );
}
function MainViews({ mode, settings, desktop, setAnyActive, onConn }) {
  return mode === "solo"
    ? <IndividualView settings={settings} desktop={desktop} onActiveChange={setAnyActive} onConn={onConn} />
    : <ConversationModes settings={settings} onAnyActive={setAnyActive} onConn={onConn} />;
}

/* ============================== SETTINGS STATE ============================== */
/* 字幕顯示:字型 / 大小 / 顏色(只作用於字幕泡泡文字) */
const SUB_FONTS = {
  sans: '"Noto Sans TC", -apple-system, "PingFang TC", "Microsoft JhengHei", sans-serif',
  system: '-apple-system, "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif',
  serif: '"Noto Serif TC", "Songti TC", "PMingLiU", serif',
};
const SUB_FONT_OPTS = [{ v: "sans", l: "黑體（預設）" }, { v: "system", l: "系統字型" }, { v: "serif", l: "明體" }];
const SUB_SIZES = { standard: 20, large: 25, xlarge: 31 };       // px;標準調得比舊版(27)小、適中
const SUB_SIZE_OPTS = [{ v: "standard", l: "標準" }, { v: "large", l: "大" }, { v: "xlarge", l: "特大" }];
const SUB_COLORS = { teal: "#173b39", ink: "#1b2a3a", black: "#202020", brown: "#46342a" };
const SUB_COLOR_OPTS = [
  { v: "teal", l: "墨綠", c: "#173b39" }, { v: "ink", l: "藍黑", c: "#1b2a3a" },
  { v: "black", l: "純黑", c: "#202020" }, { v: "brown", l: "暖棕", c: "#46342a" },
];

const SETTINGS_DEFAULTS = {
  apiKey: "", backendUrl: "",
  uiLang: "zh",
  soloListen: "en", soloTarget: "zh-TW",
  convFrom: "zh-TW", convTo: "en",
  voiceSolo: false, voiceMe: false, voiceThem: false,
  inputDevice: "", outputDevice: "",
  mimic: true,
  origVol: 30, transVol: 90,
  saveTranscript: true, autoSummary: true,
  region: "asia-east",
  subFont: "sans", subFontSize: "standard", subColor: "teal",
};
window.SUB_FONTS = SUB_FONTS; window.SUB_FONT_OPTS = SUB_FONT_OPTS;
window.SUB_SIZE_OPTS = SUB_SIZE_OPTS; window.SUB_COLOR_OPTS = SUB_COLOR_OPTS;
function loadSettings() {
  try { return { ...SETTINGS_DEFAULTS, ...JSON.parse(localStorage.getItem("ly_settings") || "{}") }; }
  catch (e) { return { ...SETTINGS_DEFAULTS }; }
}

/* ============================== APP ROOT ============================== */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "primary": "#1f9c96",
  "subSize": 27,
  "radius": "標準",
  "duck": 70
}/*EDITMODE-END*/;
const RADIUS_MAP = { "柔和": "24px", "標準": "18px", "銳利": "10px" };

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const isDesktop = useIsDesktop();
  const [mode, setMode] = useState(() => localStorage.getItem("ly_mode") || "solo");
  const [route, setRoute] = useState("app"); // app | settings
  const [settings, setSettings] = useState(loadSettings);
  const [conn, setConn] = useState("ok");
  const lat = null; // 不顯示假延遲(尚未量測真實 RTT)
  const [connPop, setConnPop] = useState(false);
  const [anyActive, setAnyActive] = useState(false);

  useEffect(() => { localStorage.setItem("ly_mode", mode); }, [mode]);
  useEffect(() => { localStorage.setItem("ly_settings", JSON.stringify(settings)); }, [settings]);

  // 停止翻譯後把狀態回復為待命的「已連線」
  useEffect(() => { if (!anyActive) setConn("ok"); }, [anyActive]);
  // 引擎狀態 → 狀態藥丸(connecting 對應重新連線中)
  const handleConn = (st) => setConn(st === "connecting" ? "warn" : st);
  // 精簡模式:個人用或對話用,只要已開始翻譯,就讓對話框佔滿畫面、收起其他元素
  const focus = anyActive && route === "app";

  const rootStyle = {
    "--primary": t.primary,
    "--primary-bright": `color-mix(in srgb, ${t.primary}, white 18%)`,
    "--primary-soft": `color-mix(in srgb, ${t.primary}, white 86%)`,
    "--primary-ring": `color-mix(in srgb, ${t.primary} 26%, transparent)`,
    // 字幕顯示:由後台「字幕顯示」設定驅動(只影響字幕泡泡文字)
    "--sub-size": (SUB_SIZES[settings.subFontSize] || SUB_SIZES.standard) + "px",
    "--sub-font": SUB_FONTS[settings.subFont] || SUB_FONTS.sans,
    "--sub-color": SUB_COLORS[settings.subColor] || SUB_COLORS.teal,
    "--radius": RADIUS_MAP[t.radius] || "18px",
    "--radius-lg": `calc(${RADIUS_MAP[t.radius] || "18px"} + 8px)`,
  };

  const Logo = () => <img className="brand-logo" src="assets/iohs-mark.png" alt="IOHS 聆譯" />;
  const ConnArea = () => (
    <span className="conn-wrap">
      <ConnPill state={conn} latency={lat} onClick={() => setConnPop(p => !p)} />
      {connPop && <ConnPopover state={conn} latency={lat} onClose={() => setConnPop(false)} />}
    </span>
  );

  const tweaks = (
    <TweaksPanel>
      <TweakSection label="色彩方向" />
      <TweakColor label="主色調" value={t.primary} options={["#1f9c96", "#36b6b0", "#157f86", "#2b9bb0", "#0f8f7e"]} onChange={v => setTweak("primary", v)} />
      <TweakSection label="字幕" />
      <TweakSlider label="字幕字級" value={t.subSize} min={20} max={36} unit="px" onChange={v => setTweak("subSize", v)} />
      <TweakSection label="外觀" />
      <TweakRadio label="圓角 / 卡片" value={t.radius} options={["柔和", "標準", "銳利"]} onChange={v => setTweak("radius", v)} />
      <TweakSection label="原音處理" />
      <TweakSlider label="原音壓低" value={t.duck} min={0} max={90} unit="%" onChange={v => setTweak("duck", v)} />
    </TweaksPanel>
  );

  /* ---------- DESKTOP ---------- */
  if (isDesktop) {
    return (
      <div className={"deskshell is-desktop" + (focus ? " is-focus" : "")} style={rootStyle}>
        <header className="appbar">
          <Logo />
          <span className="ab-spacer"></span>
          {route === "app" && <ModeSwitch mode={mode} setMode={setMode} />}
          <span className="ab-spacer"></span>
          <span className="appbar-right">
            <ConnArea />
            <FullscreenBtn className="set-btn fs-btn" />
            <button className={"set-btn" + (route === "settings" ? " active" : "")} onClick={() => setRoute(route === "settings" ? "app" : "settings")}><Svg>{I.gear}</Svg>{route === "settings" ? "返回" : "設定"}</button>
          </span>
        </header>
        <main className="deskmain">
          {route === "settings"
            ? <SettingsView s={settings} setS={setSettings} embedded onClose={() => setRoute("app")} />
            : <div className="deskstage"><MainViews mode={mode} settings={settings} desktop={true} setAnyActive={setAnyActive} onConn={handleConn} /></div>}
        </main>
        {tweaks}
      </div>
    );
  }

  /* ---------- PHONE ---------- */
  return (
    <div className={"device" + (focus ? " is-focus" : "")} style={rootStyle}>
      <StatusBar dark={false} />
      <div className="app">
        <div className="topbar">
          <div className="brand"><Logo /></div>
          <div className="topbar-actions">
            <ConnArea />
            <FullscreenBtn className="icon-btn" />
            <button className="icon-btn" onClick={() => setRoute("settings")} title="設定"><Svg>{I.gear}</Svg></button>
          </div>
        </div>
        <ModeSwitch mode={mode} setMode={setMode} />
        <div className="content"><MainViews mode={mode} settings={settings} setAnyActive={setAnyActive} onConn={handleConn} /></div>
        {route === "settings" && <SettingsView s={settings} setS={setSettings} onClose={() => setRoute("app")} />}
      </div>
      <div className="home-ind"></div>
      {tweaks}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
