Files
cc-switch/src/hooks/useSettingsForm.ts
v2v cfcf9452d0 添加应用级别窗口按钮,以改善linux wayland下系统窗口按钮失效的问题 (#1119)
* feat(window): add app-level window controls with settings toggle

Add a persistent settings toggle to enable app-level minimize/maximize/close controls and hide system decorations when enabled, providing a Wayland-friendly fallback for broken native titlebar interactions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(window): restrict app-level window controls to Linux only and fix startup flicker

- Guard useAppWindowControls with isLinux() in App.tsx so it's always
  false on macOS/Windows even if persisted as true
- Wrap set_decorations call in lib.rs with #[cfg(target_os = "linux")]
- Only show the toggle in WindowSettings on Linux
- Skip setDecorations effect while settingsData is still loading to
  prevent the Rust-side decoration state from being overridden by the
  undefined->false fallback, which caused a brief title bar flicker

---------

Co-authored-by: wzk <wx13571681304@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-04-12 20:59:04 +08:00

172 lines
5.2 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSettingsQuery } from "@/lib/query";
import type { Settings } from "@/types";
type Language = "zh" | "en" | "ja";
export type SettingsFormState = Omit<Settings, "language"> & {
language: Language;
};
const normalizeLanguage = (lang?: string | null): Language => {
if (!lang) return "zh";
const normalized = lang.toLowerCase();
return normalized === "en" || normalized === "ja" ? normalized : "zh";
};
const sanitizeDir = (value?: string | null): string | undefined => {
if (!value) return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
export interface UseSettingsFormResult {
settings: SettingsFormState | null;
isLoading: boolean;
initialLanguage: Language;
updateSettings: (updates: Partial<SettingsFormState>) => void;
resetSettings: (serverData: Settings | null) => void;
readPersistedLanguage: () => Language;
syncLanguage: (lang: Language) => void;
}
/**
* useSettingsForm - 表单状态管理
* 负责:
* - 表单数据状态
* - 表单字段更新
* - 语言同步
* - 表单重置
*/
export function useSettingsForm(): UseSettingsFormResult {
const { i18n } = useTranslation();
const { data, isLoading } = useSettingsQuery();
const [settingsState, setSettingsState] = useState<SettingsFormState | null>(
null,
);
const initialLanguageRef = useRef<Language>("zh");
const readPersistedLanguage = useCallback((): Language => {
if (typeof window !== "undefined") {
const stored = window.localStorage.getItem("language");
if (stored === "en" || stored === "zh" || stored === "ja") {
return stored as Language;
}
}
return normalizeLanguage(i18n.language);
}, [i18n]);
const syncLanguage = useCallback(
(lang: Language) => {
const current = normalizeLanguage(i18n.language);
if (current !== lang) {
void i18n.changeLanguage(lang);
}
},
[i18n],
);
// 初始化设置数据
useEffect(() => {
if (!data) return;
const normalizedLanguage = normalizeLanguage(
data.language ?? readPersistedLanguage(),
);
const normalized: SettingsFormState = {
...data,
showInTray: data.showInTray ?? true,
minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true,
useAppWindowControls: data.useAppWindowControls ?? false,
enableClaudePluginIntegration:
data.enableClaudePluginIntegration ?? false,
silentStartup: data.silentStartup ?? false,
skipClaudeOnboarding: data.skipClaudeOnboarding ?? false,
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
codexConfigDir: sanitizeDir(data.codexConfigDir),
geminiConfigDir: sanitizeDir(data.geminiConfigDir),
opencodeConfigDir: sanitizeDir(data.opencodeConfigDir),
language: normalizedLanguage,
};
setSettingsState(normalized);
initialLanguageRef.current = normalizedLanguage;
syncLanguage(normalizedLanguage);
}, [data, readPersistedLanguage, syncLanguage]);
const updateSettings = useCallback(
(updates: Partial<SettingsFormState>) => {
setSettingsState((prev) => {
const base =
prev ??
({
showInTray: true,
minimizeToTrayOnClose: true,
useAppWindowControls: false,
enableClaudePluginIntegration: false,
skipClaudeOnboarding: false,
language: readPersistedLanguage(),
} as SettingsFormState);
const next: SettingsFormState = {
...base,
...updates,
};
if (updates.language) {
const normalized = normalizeLanguage(updates.language);
next.language = normalized;
syncLanguage(normalized);
}
return next;
});
},
[readPersistedLanguage, syncLanguage],
);
const resetSettings = useCallback(
(serverData: Settings | null) => {
if (!serverData) return;
const normalizedLanguage = normalizeLanguage(
serverData.language ?? readPersistedLanguage(),
);
const normalized: SettingsFormState = {
...serverData,
showInTray: serverData.showInTray ?? true,
minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true,
useAppWindowControls: serverData.useAppWindowControls ?? false,
enableClaudePluginIntegration:
serverData.enableClaudePluginIntegration ?? false,
silentStartup: serverData.silentStartup ?? false,
skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false,
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
geminiConfigDir: sanitizeDir(serverData.geminiConfigDir),
opencodeConfigDir: sanitizeDir(serverData.opencodeConfigDir),
language: normalizedLanguage,
};
setSettingsState(normalized);
syncLanguage(initialLanguageRef.current);
},
[readPersistedLanguage, syncLanguage],
);
return {
settings: settingsState,
isLoading,
initialLanguage: initialLanguageRef.current,
updateSettings,
resetSettings,
readPersistedLanguage,
syncLanguage,
};
}