feat(settings): add code editor preferences

This commit is contained in:
cfngc4594 2026-05-29 12:12:27 +08:00
parent 705e0f29e4
commit 12c2963447
7 changed files with 171 additions and 9 deletions

View File

@ -75,6 +75,15 @@
"showPassword": "Show password", "showPassword": "Show password",
"hidePassword": "Hide password" "hidePassword": "Hide password"
}, },
"CodeEditorSettings": {
"fontSize": "Font size",
"wordWrap": "Wrap long lines",
"wordWrapDescription": "Move code to the next line when it does not fit in one line.",
"minimap": "Minimap",
"minimapDescription": "Show a code overview on the right side of the editor.",
"stickyScroll": "Sticky scroll",
"stickyScrollDescription": "Keep the current code scope visible while scrolling."
},
"DetailsPage": { "DetailsPage": {
"BackButton": "All Submissions", "BackButton": "All Submissions",
"Time": "Submitted on", "Time": "Submitted on",
@ -129,8 +138,7 @@
"nav": { "nav": {
"Appearance": "Appearance", "Appearance": "Appearance",
"Language": "Language", "Language": "Language",
"CodeEditor": "CodeEditor", "CodeEditor": "CodeEditor"
"Advanced": "Advanced"
} }
}, },
"SignInForm": { "SignInForm": {

View File

@ -75,6 +75,15 @@
"showPassword": "显示密码", "showPassword": "显示密码",
"hidePassword": "隐藏密码" "hidePassword": "隐藏密码"
}, },
"CodeEditorSettings": {
"fontSize": "字体大小",
"wordWrap": "超长行换行显示",
"wordWrapDescription": "一行代码显示不下时,自动换到下一行显示。",
"minimap": "小地图",
"minimapDescription": "在编辑器右侧显示代码缩略导航。",
"stickyScroll": "粘性滚动",
"stickyScrollDescription": "滚动时固定显示当前所在的代码结构。"
},
"DetailsPage": { "DetailsPage": {
"BackButton": "所有提交记录", "BackButton": "所有提交记录",
"Time": "提交于", "Time": "提交于",
@ -129,8 +138,7 @@
"nav": { "nav": {
"Appearance": "外观", "Appearance": "外观",
"Language": "语言", "Language": "语言",
"CodeEditor": "代码编辑器", "CodeEditor": "代码编辑器"
"Advanced": "高级设置"
} }
}, },
"SignInForm": { "SignInForm": {

View File

@ -0,0 +1,88 @@
"use client";
import { useTranslations } from "next-intl";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useSettingsStore } from "@/stores/useSettingsStore";
const fontSizes = [12, 13, 14, 15, 16, 18, 20];
export function CodeEditorSettings() {
const t = useTranslations("CodeEditorSettings");
const { editorSettings, setEditorSetting } = useSettingsStore();
return (
<div className="max-w-md space-y-6">
<div className="space-y-2">
<Label>{t("fontSize")}</Label>
<Select
value={editorSettings.fontSize.toString()}
onValueChange={(value) =>
setEditorSetting("fontSize", Number(value))
}
>
<SelectTrigger className="w-[160px] shadow-none focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
{fontSizes.map((fontSize) => (
<SelectItem key={fontSize} value={fontSize.toString()}>
{fontSize}px
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<EditorSwitch
label={t("wordWrap")}
description={t("wordWrapDescription")}
checked={editorSettings.wordWrap}
onCheckedChange={(checked) => setEditorSetting("wordWrap", checked)}
/>
<EditorSwitch
label={t("minimap")}
description={t("minimapDescription")}
checked={editorSettings.minimap}
onCheckedChange={(checked) => setEditorSetting("minimap", checked)}
/>
<EditorSwitch
label={t("stickyScroll")}
description={t("stickyScrollDescription")}
checked={editorSettings.stickyScroll}
onCheckedChange={(checked) => setEditorSetting("stickyScroll", checked)}
/>
</div>
);
}
interface EditorSwitchProps {
label: string;
description: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}
function EditorSwitch({
label,
description,
checked,
onCheckedChange,
}: EditorSwitchProps) {
return (
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<Label>{label}</Label>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<Switch checked={checked} onCheckedChange={onCheckedChange} />
</div>
);
}

View File

@ -9,6 +9,7 @@ import { shikiToMonaco } from "@shikijs/monaco";
import type { Monaco } from "@monaco-editor/react"; import type { Monaco } from "@monaco-editor/react";
import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor"; import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor";
import { useMonacoTheme } from "@/hooks/use-monaco-theme"; import { useMonacoTheme } from "@/hooks/use-monaco-theme";
import { useSettingsStore } from "@/stores/useSettingsStore";
const MonacoEditor = dynamic( const MonacoEditor = dynamic(
async () => { async () => {
@ -38,6 +39,7 @@ export const CoreDiffEditor = ({
className, className,
}: CoreDiffEditorProps) => { }: CoreDiffEditorProps) => {
const { theme } = useMonacoTheme(); const { theme } = useMonacoTheme();
const { editorSettings } = useSettingsStore();
const editorRef = useRef<editor.IStandaloneDiffEditor | null>(null); const editorRef = useRef<editor.IStandaloneDiffEditor | null>(null);
@ -62,7 +64,21 @@ export const CoreDiffEditor = ({
modified={modified} modified={modified}
beforeMount={handleBeforeMount} beforeMount={handleBeforeMount}
onMount={handleOnMount} onMount={handleOnMount}
options={{ ...DEFAULT_EDITOR_OPTIONS, readOnly: true }} options={{
...DEFAULT_EDITOR_OPTIONS,
readOnly: true,
fontSize: editorSettings.fontSize,
lineHeight: Math.round(editorSettings.fontSize * 1.5),
wordWrap: editorSettings.wordWrap ? "on" : "off",
minimap: {
...DEFAULT_EDITOR_OPTIONS.minimap,
enabled: editorSettings.minimap,
},
stickyScroll: {
...DEFAULT_EDITOR_OPTIONS.stickyScroll,
enabled: editorSettings.stickyScroll,
},
}}
loading={<Loading />} loading={<Loading />}
className={className} className={className}
/> />

View File

@ -15,6 +15,7 @@ import type { Monaco } from "@monaco-editor/react";
import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor"; import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor";
import { useMonacoTheme } from "@/hooks/use-monaco-theme"; import { useMonacoTheme } from "@/hooks/use-monaco-theme";
import { LanguageServerConfig } from "@/generated/client"; import { LanguageServerConfig } from "@/generated/client";
import { useSettingsStore } from "@/stores/useSettingsStore";
import type { MessageTransports } from "vscode-languageclient"; import type { MessageTransports } from "vscode-languageclient";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import type { MonacoLanguageClient } from "monaco-languageclient"; import type { MonacoLanguageClient } from "monaco-languageclient";
@ -72,6 +73,7 @@ export const CoreEditor = ({
className, className,
}: CoreEditorProps) => { }: CoreEditorProps) => {
const { theme } = useMonacoTheme(); const { theme } = useMonacoTheme();
const { editorSettings } = useSettingsStore();
const [isEditorMounted, setIsEditorMounted] = useState(false); const [isEditorMounted, setIsEditorMounted] = useState(false);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null); const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
@ -159,7 +161,20 @@ export const CoreEditor = ({
onMount={handleOnMount} onMount={handleOnMount}
onChange={handleOnChange} onChange={handleOnChange}
onValidate={handleOnValidate} onValidate={handleOnValidate}
options={DEFAULT_EDITOR_OPTIONS} options={{
...DEFAULT_EDITOR_OPTIONS,
fontSize: editorSettings.fontSize,
lineHeight: Math.round(editorSettings.fontSize * 1.5),
wordWrap: editorSettings.wordWrap ? "on" : "off",
minimap: {
...DEFAULT_EDITOR_OPTIONS.minimap,
enabled: editorSettings.minimap,
},
stickyScroll: {
...DEFAULT_EDITOR_OPTIONS.stickyScroll,
enabled: editorSettings.stickyScroll,
},
}}
loading={<Loading />} loading={<Loading />}
className={className} className={className}
/> />

View File

@ -30,7 +30,8 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { useSettingsStore } from "@/stores/useSettingsStore"; import { useSettingsStore } from "@/stores/useSettingsStore";
import { LocaleSwitcher } from "@/components/locale-switcher"; import { LocaleSwitcher } from "@/components/locale-switcher";
import AppearanceSettings from "@/components/appearance-settings"; import AppearanceSettings from "@/components/appearance-settings";
import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react"; import { CodeEditorSettings } from "@/components/code-editor-settings";
import { CodeXml, Globe, Paintbrush } from "lucide-react";
export const SettingsDialog = () => { export const SettingsDialog = () => {
const t = useTranslations("SettingsDialog"); const t = useTranslations("SettingsDialog");
@ -39,7 +40,6 @@ export const SettingsDialog = () => {
{ id: "Appearance", name: t("nav.Appearance"), icon: Paintbrush }, { id: "Appearance", name: t("nav.Appearance"), icon: Paintbrush },
{ id: "Language", name: t("nav.Language"), icon: Globe }, { id: "Language", name: t("nav.Language"), icon: Globe },
{ id: "CodeEditor", name: t("nav.CodeEditor"), icon: CodeXml }, { id: "CodeEditor", name: t("nav.CodeEditor"), icon: CodeXml },
{ id: "Advanced", name: t("nav.Advanced"), icon: Settings },
], ],
}; };
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } = const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } =
@ -99,6 +99,7 @@ export const SettingsDialog = () => {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{activeSetting === "Appearance" && <AppearanceSettings />} {activeSetting === "Appearance" && <AppearanceSettings />}
{activeSetting === "Language" && <LocaleSwitcher />} {activeSetting === "Language" && <LocaleSwitcher />}
{activeSetting === "CodeEditor" && <CodeEditorSettings />}
</div> </div>
</ScrollArea> </ScrollArea>
</main> </main>

View File

@ -1,11 +1,23 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
export interface EditorSettings {
fontSize: number;
wordWrap: boolean;
minimap: boolean;
stickyScroll: boolean;
}
interface SettingsState { interface SettingsState {
activeSetting: string; activeSetting: string;
isDialogOpen: boolean; isDialogOpen: boolean;
editorSettings: EditorSettings;
setActiveSetting: (setting: string) => void; setActiveSetting: (setting: string) => void;
setDialogOpen: (open: boolean) => void; setDialogOpen: (open: boolean) => void;
setEditorSetting: <K extends keyof EditorSettings>(
key: K,
value: EditorSettings[K]
) => void;
} }
export const useSettingsStore = create<SettingsState>()( export const useSettingsStore = create<SettingsState>()(
@ -13,14 +25,28 @@ export const useSettingsStore = create<SettingsState>()(
(set) => ({ (set) => ({
activeSetting: "Appearance", activeSetting: "Appearance",
isDialogOpen: false, isDialogOpen: false,
editorSettings: {
fontSize: 14,
wordWrap: true,
minimap: false,
stickyScroll: true,
},
setActiveSetting: (setting) => set({ activeSetting: setting }), setActiveSetting: (setting) => set({ activeSetting: setting }),
setDialogOpen: (open) => set({ isDialogOpen: open }), setDialogOpen: (open) => set({ isDialogOpen: open }),
setEditorSetting: (key, value) =>
set((state) => ({
editorSettings: {
...state.editorSettings,
[key]: value,
},
})),
}), }),
{ {
name: "settings-state", name: "settings-state",
partialize: (state) => ({ partialize: (state) => ({
activeNav: state.activeSetting, activeSetting: state.activeSetting,
isDialogOpen: state.isDialogOpen, isDialogOpen: state.isDialogOpen,
editorSettings: state.editorSettings,
}), }),
} }
) )