feat: custom theme system with PostgreSQL PL/pgSQL highlighting fix
- Fix PostgreSQL function highlighting by disabling doubleDollarQuotedStrings in extended PostgreSQL dialect, enabling PL/pgSQL syntax highlighting inside $ blocks - Add complete custom editor theme system with multi-theme management (create, rename, duplicate, delete), visual color editor (12 colors), JSON import/export, real-time preview, and 12 preset color schemes - Add background/foreground color customization with system theme defaults - Optimize EditorSettingsDialog layout: 2-column grid with font selector taking available space and theme dropdown grouped with custom theme button - Add i18n support for custom theme UI (en, zh-CN, zh-TW, es) - Fix Tauri production build by adding custom-protocol feature - Update .gitignore for temporary build artifacts Closes t8y2#788 Co-authored-by: Sam <14344444@@qq.com>
This commit is contained in:
parent
0b07f05f72
commit
a067ee8d76
|
|
@ -1,5 +1,4 @@
|
|||
[env]
|
||||
# Enable SQLite's built-in math functions for rusqlite's bundled libsqlite3.
|
||||
LIBSQLITE3_FLAGS = "SQLITE_ENABLE_MATH_FUNCTIONS"
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
|
|
|
|||
|
|
@ -55,3 +55,7 @@ agents/*/libs/*.jar
|
|||
|
||||
# Generated changelog data
|
||||
releases-*.json
|
||||
|
||||
test.pdb
|
||||
portable/
|
||||
DBX_*_x64-portable.zip
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ import {
|
|||
type EditorTheme,
|
||||
type DesktopIconTheme,
|
||||
type DisconnectTabHandlingMode,
|
||||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
} from "@/stores/settingsStore";
|
||||
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
|
||||
import ThemeCustomizerDialog from "./ThemeCustomizerDialog.vue";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
|
|
@ -103,6 +106,9 @@ const editFontFamily = ref(settingsStore.editorSettings.fontFamily);
|
|||
const editFontSize = ref(settingsStore.editorSettings.fontSize);
|
||||
const editUiScale = ref(settingsStore.editorSettings.uiScale);
|
||||
const editTheme = ref(settingsStore.editorSettings.theme);
|
||||
const editCustomThemes = ref<CustomTheme[]>([...settingsStore.editorSettings.customThemes]);
|
||||
const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThemeId);
|
||||
const showThemeCustomizer = ref(false);
|
||||
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confirmDangerousSqlExecution);
|
||||
|
|
@ -256,6 +262,8 @@ watch(
|
|||
editFontSize.value = settingsStore.editorSettings.fontSize;
|
||||
editUiScale.value = settingsStore.editorSettings.uiScale;
|
||||
editTheme.value = settingsStore.editorSettings.theme;
|
||||
editCustomThemes.value = [...settingsStore.editorSettings.customThemes];
|
||||
editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId;
|
||||
editExecuteMode.value = settingsStore.editorSettings.executeMode;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editConfirmDangerousSqlExecution.value = settingsStore.editorSettings.confirmDangerousSqlExecution;
|
||||
|
|
@ -300,6 +308,8 @@ function hasChanges(): boolean {
|
|||
editFontSize.value !== settingsStore.editorSettings.fontSize ||
|
||||
editUiScale.value !== settingsStore.editorSettings.uiScale ||
|
||||
editTheme.value !== settingsStore.editorSettings.theme ||
|
||||
JSON.stringify(editCustomThemes.value) !== JSON.stringify(settingsStore.editorSettings.customThemes) ||
|
||||
editActiveCustomThemeId.value !== settingsStore.editorSettings.activeCustomThemeId ||
|
||||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editConfirmDangerousSqlExecution.value !== settingsStore.editorSettings.confirmDangerousSqlExecution ||
|
||||
|
|
@ -333,6 +343,8 @@ async function persistSettings() {
|
|||
fontSize: editFontSize.value,
|
||||
uiScale: editUiScale.value,
|
||||
theme: editTheme.value,
|
||||
customThemes: editCustomThemes.value,
|
||||
activeCustomThemeId: editActiveCustomThemeId.value,
|
||||
executeMode: editExecuteMode.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value,
|
||||
|
|
@ -375,6 +387,8 @@ function resetDefaults() {
|
|||
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
|
||||
editUiScale.value = DEFAULT_EDITOR_SETTINGS.uiScale;
|
||||
editTheme.value = DEFAULT_EDITOR_SETTINGS.theme;
|
||||
editCustomThemes.value = [...DEFAULT_EDITOR_SETTINGS.customThemes];
|
||||
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
|
||||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
|
||||
|
|
@ -405,8 +419,43 @@ function onFontFamilyChange(v: any) {
|
|||
if (typeof v === "string") editFontFamily.value = v;
|
||||
}
|
||||
|
||||
const themeSelectValue = computed(() => {
|
||||
if (editTheme.value === "custom") {
|
||||
return `custom:${editActiveCustomThemeId.value}`;
|
||||
}
|
||||
return editTheme.value;
|
||||
});
|
||||
|
||||
const themeSelectOptions = computed(() => [
|
||||
...EDITOR_THEMES.filter((theme) => theme.value !== "custom").map((theme) => ({
|
||||
value: theme.value,
|
||||
label: theme.value === "app" ? t("settings.followAppTheme") : theme.label,
|
||||
dark: theme.dark,
|
||||
isCustom: false,
|
||||
})),
|
||||
...editCustomThemes.value.map((theme) => ({
|
||||
value: `custom:${theme.id}`,
|
||||
label: theme.name,
|
||||
dark: true,
|
||||
isCustom: true,
|
||||
})),
|
||||
]);
|
||||
|
||||
function onThemeChange(v: any) {
|
||||
if (typeof v === "string") editTheme.value = v as typeof DEFAULT_EDITOR_SETTINGS.theme;
|
||||
if (typeof v !== "string") return;
|
||||
if (v.startsWith("custom:")) {
|
||||
editTheme.value = "custom";
|
||||
editActiveCustomThemeId.value = v.slice(7);
|
||||
} else {
|
||||
editTheme.value = v as typeof DEFAULT_EDITOR_SETTINGS.theme;
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeSave(updatedThemes: CustomTheme[], activeId: string) {
|
||||
editCustomThemes.value = updatedThemes;
|
||||
editActiveCustomThemeId.value = activeId;
|
||||
editTheme.value = "custom";
|
||||
showThemeCustomizer.value = false;
|
||||
}
|
||||
|
||||
function onDisconnectTabHandlingModeChange(v: any) {
|
||||
|
|
@ -1036,16 +1085,24 @@ async function aiTestConn() {
|
|||
const previewRef = ref<HTMLDivElement>();
|
||||
const previewView = shallowRef<EditorViewType | null>(null);
|
||||
|
||||
function getPreviewCustomThemeColors(): CustomThemeColors | undefined {
|
||||
if (editTheme.value !== "custom") return undefined;
|
||||
const activeTheme = editCustomThemes.value.find((t) => t.id === editActiveCustomThemeId.value);
|
||||
return activeTheme?.colors;
|
||||
}
|
||||
|
||||
const previewSettings = computed<{
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
theme: EditorTheme;
|
||||
appAppearance: AppThemeAppearance;
|
||||
customColors?: CustomThemeColors;
|
||||
}>(() => ({
|
||||
fontFamily: editFontFamily.value,
|
||||
fontSize: editFontSize.value,
|
||||
theme: editTheme.value,
|
||||
appAppearance: isDark.value ? "dark" : "light",
|
||||
customColors: getPreviewCustomThemeColors(),
|
||||
}));
|
||||
|
||||
const previewSql = `SELECT u.id, u.name
|
||||
|
|
@ -1057,11 +1114,11 @@ let themeComp: import("@codemirror/state").Compartment | null = null;
|
|||
let editorViewModule: typeof import("@codemirror/view") | null = null;
|
||||
|
||||
watch(
|
||||
previewSettings,
|
||||
async (ss) => {
|
||||
[previewSettings, editCustomThemes, editActiveCustomThemeId],
|
||||
async ([ss]) => {
|
||||
if (!previewView.value || !fontThemeComp || !themeComp || !editorViewModule) return;
|
||||
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance);
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors);
|
||||
previewView.value.dispatch({
|
||||
effects: [
|
||||
themeComp.reconfigure(themeExt),
|
||||
|
|
@ -1102,7 +1159,7 @@ watch(previewRef, async (el) => {
|
|||
themeComp = new Compartment();
|
||||
|
||||
const ss = previewSettings.value;
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance);
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors);
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: previewSql,
|
||||
|
|
@ -1160,9 +1217,9 @@ watch(
|
|||
<div class="min-w-0 flex-1 overflow-hidden px-1 flex flex-col">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-1 pr-2">
|
||||
<section v-if="activeSettingsTab === 'editor'" class="flex flex-col gap-5 py-2">
|
||||
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<div class="grid gap-4 md:grid-cols-[1fr_auto]">
|
||||
<!-- Font Family -->
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-2 min-w-0">
|
||||
<Label>{{ t("settings.fontFamily") }}</Label>
|
||||
<SearchableSelect
|
||||
:model-value="editFontFamily"
|
||||
|
|
@ -1196,29 +1253,40 @@ watch(
|
|||
</SearchableSelect>
|
||||
</div>
|
||||
|
||||
<!-- Theme -->
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.theme") }}</Label>
|
||||
<Select :model-value="editTheme" @update:model-value="onThemeChange">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="t('settings.selectTheme')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="theme in EDITOR_THEMES" :key="theme.value" :value="theme.value">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="h-3 w-3 rounded-full border"
|
||||
:class="
|
||||
theme.dark
|
||||
? 'bg-foreground border-foreground/20'
|
||||
: 'bg-muted-foreground/30 border-muted-foreground/40'
|
||||
"
|
||||
/>
|
||||
{{ theme.value === "app" ? t("settings.followAppTheme") : theme.label }}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<!-- Theme + Custom Theme Button -->
|
||||
<div class="flex gap-2 items-end">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.theme") }}</Label>
|
||||
<Select :model-value="themeSelectValue" @update:model-value="onThemeChange">
|
||||
<SelectTrigger class="min-w-[80px] max-w-[200px]">
|
||||
<SelectValue :placeholder="t('settings.selectTheme')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="theme in themeSelectOptions" :key="theme.value" :value="theme.value">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="h-3 w-3 rounded-full border"
|
||||
:class="
|
||||
theme.dark
|
||||
? 'bg-foreground border-foreground/20'
|
||||
: 'bg-muted-foreground/30 border-muted-foreground/40'
|
||||
"
|
||||
/>
|
||||
{{ theme.label }}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
v-if="editTheme === 'custom'"
|
||||
variant="outline"
|
||||
class="h-9 w-auto px-4"
|
||||
@click="showThemeCustomizer = true"
|
||||
>
|
||||
<Settings class="mr-2 h-4 w-4" />
|
||||
{{ t("settings.customThemeConfigure") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -2548,6 +2616,14 @@ watch(
|
|||
</div>
|
||||
</DialogContent>
|
||||
|
||||
<!-- Theme Customizer Dialog -->
|
||||
<ThemeCustomizerDialog
|
||||
v-model:open="showThemeCustomizer"
|
||||
:themes="editCustomThemes"
|
||||
:active-theme-id="editActiveCustomThemeId"
|
||||
@save="handleThemeSave"
|
||||
/>
|
||||
|
||||
<!-- Snippet Add/Edit Dialog -->
|
||||
<Dialog :open="snippetDialogOpen" @update:open="snippetDialogOpen = $event">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed } from "vue";
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
watch,
|
||||
shallowRef,
|
||||
computed,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { Play, Copy, TextSelect } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
||||
|
|
@ -1405,17 +1415,28 @@ onMounted(async () => {
|
|||
],
|
||||
});
|
||||
|
||||
const ss = settingsStore.editorSettings;
|
||||
|
||||
const baseDialect = props.dialect === "postgres" ? PostgreSQL : props.dialect === "sqlserver" ? MSSQL : MySQL;
|
||||
const extraKeywords =
|
||||
"PIVOT UNPIVOT EXCLUDE REPLACE QUALIFY ASOF POSITIONAL ANTI SEMI SAMPLE TABLESAMPLE STRUCT MAP LIST ARRAY LAMBDA UNNEST LATERAL FILTER RECURSIVE SUMMARIZE PRAGMA READ_CSV READ_PARQUET READ_JSON DESCRIBE SHOW COPY EXPORT IMPORT";
|
||||
|
||||
// PL/pgSQL extension: add procedural language keywords and built-in variables for PostgreSQL function/procedure bodies
|
||||
const isPostgres = props.dialect === "postgres";
|
||||
const plpgsqlKeywords = isPostgres ? "PERFORM" : "";
|
||||
const plpgsqlTypes = isPostgres ? " RECORD JSON JSONB" : "";
|
||||
const plpgsqlBuiltin = isPostgres
|
||||
? "SQLERRM TG_NAME TG_WHEN TG_LEVEL TG_OP TG_RELID TG_RELNAME TG_TABLE_NAME TG_TABLE_SCHEMA TG_NARGS TG_ARGV"
|
||||
: "";
|
||||
|
||||
const dialect = SQLDialect.define({
|
||||
...baseDialect.spec,
|
||||
keywords: (baseDialect.spec.keywords || "") + " " + extraKeywords,
|
||||
keywords: [baseDialect.spec.keywords || "", extraKeywords, plpgsqlKeywords].filter(Boolean).join(" "),
|
||||
types: [baseDialect.spec.types || "", plpgsqlTypes].filter(Boolean).join(" ") || undefined,
|
||||
builtin: [baseDialect.spec.builtin || "", plpgsqlBuiltin].filter(Boolean).join(" ") || undefined,
|
||||
doubleDollarQuotedStrings: false,
|
||||
});
|
||||
|
||||
const theme = await loadEditorTheme(ss.theme, editorThemeAppearance());
|
||||
const initialSettings = settingsStore.editorSettings;
|
||||
const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors());
|
||||
|
||||
const activeLineHighlighter = ViewPlugin.fromClass(
|
||||
class {
|
||||
|
|
@ -1496,7 +1517,7 @@ onMounted(async () => {
|
|||
]),
|
||||
),
|
||||
runKeymapComp.of(runKeymapExtension(keymap)),
|
||||
wordWrapComp.of(props.forceWordWrap || ss.wordWrap ? EditorView.lineWrapping : []),
|
||||
wordWrapComp.of(props.forceWordWrap || initialSettings.wordWrap ? EditorView.lineWrapping : []),
|
||||
readOnlyComp.of([EditorState.readOnly.of(!!props.readOnly), EditorView.editable.of(!props.readOnly)]),
|
||||
rectangularSelection({ eventFilter: (e: MouseEvent) => e.altKey || e.button === 1 }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
|
|
@ -1518,7 +1539,7 @@ onMounted(async () => {
|
|||
}
|
||||
}),
|
||||
fontThemeComp.of(
|
||||
editorFontTheme(EditorView, liveFontSize.value, ss.fontFamily, {
|
||||
editorFontTheme(EditorView, liveFontSize.value, initialSettings.fontFamily, {
|
||||
fixedHeight: true,
|
||||
scrollable: true,
|
||||
}),
|
||||
|
|
@ -1678,12 +1699,23 @@ onMounted(async () => {
|
|||
|
||||
view.value = new EditorView({ state, parent: editorRef.value });
|
||||
syncContextMenuState(view.value);
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
syncEditorFontCssVars(liveFontSize.value, initialSettings.fontFamily);
|
||||
registerTableReferenceDropListener();
|
||||
|
||||
cachedTables = [];
|
||||
cachedCompletionObjects = [];
|
||||
scheduleSemanticDiagnostics();
|
||||
|
||||
// Ensure theme is applied with the latest settings after mount
|
||||
void nextTick(async () => {
|
||||
if (!view.value || !codeMirrorTheme) return;
|
||||
const settings = settingsStore.editorSettings;
|
||||
const themeColors = settings.theme === "custom" ? getCurrentCustomThemeColors() : settings.customThemeColors;
|
||||
const themeExt = await loadEditorTheme(settings.theme, editorThemeAppearance(), themeColors);
|
||||
view.value.dispatch({
|
||||
effects: [codeMirrorTheme.reconfigure(themeExt)],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
|
|
@ -1748,6 +1780,16 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
// Derive current custom theme colors from settingsStore
|
||||
function getCurrentCustomThemeColors() {
|
||||
const settings = settingsStore.editorSettings;
|
||||
if (settings.theme !== "custom") return settings.customThemeColors;
|
||||
const activeTheme =
|
||||
settings.customThemes?.find((t: { id: string }) => t.id === settings.activeCustomThemeId) ||
|
||||
settings.customThemes?.[0];
|
||||
return activeTheme?.colors ?? settings.customThemeColors;
|
||||
}
|
||||
|
||||
// Reactively apply editor settings changes
|
||||
watch(
|
||||
[() => settingsStore.editorSettings, () => isDark.value],
|
||||
|
|
@ -1759,7 +1801,8 @@ watch(
|
|||
liveFontSize.value = ss.fontSize;
|
||||
}
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
const themeExt = await loadEditorTheme(ss.theme, editorThemeAppearance());
|
||||
const themeColors = getCurrentCustomThemeColors();
|
||||
const themeExt = await loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors);
|
||||
view.value.dispatch({
|
||||
effects: [
|
||||
codeMirrorTheme.reconfigure(themeExt),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,621 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { CustomTheme, CustomThemeColors } from "@/stores/settingsStore";
|
||||
import { DEFAULT_CUSTOM_THEME_COLORS } from "@/stores/settingsStore";
|
||||
import { Plus, Trash2, Copy, Pencil, ChevronDown, Palette } from "@lucide/vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
themes: CustomTheme[];
|
||||
activeThemeId: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", value: boolean): void;
|
||||
(e: "save", themes: CustomTheme[], activeId: string): void;
|
||||
}>();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const localThemes = ref<CustomTheme[]>([]);
|
||||
const activeEditId = ref("");
|
||||
const jsonText = ref("");
|
||||
const renamingId = ref<string | null>(null);
|
||||
const renamingName = ref("");
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
localThemes.value = JSON.parse(JSON.stringify(props.themes));
|
||||
activeEditId.value = props.activeThemeId;
|
||||
syncJson();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function syncJson() {
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
jsonText.value = JSON.stringify(theme?.colors ?? DEFAULT_CUSTOM_THEME_COLORS, null, 2);
|
||||
}
|
||||
|
||||
watch(activeEditId, syncJson);
|
||||
|
||||
const activeTheme = computed(() => localThemes.value.find((t) => t.id === activeEditId.value));
|
||||
|
||||
const localColors = computed({
|
||||
get: () => activeTheme.value?.colors ?? { ...DEFAULT_CUSTOM_THEME_COLORS },
|
||||
set: (colors: CustomThemeColors) => {
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
if (theme) theme.colors = { ...colors };
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
localThemes,
|
||||
() => {
|
||||
syncJson();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const colorItems = [
|
||||
{ key: "keyword" as const, label: t("settings.customThemeKeyword"), example: "SELECT, WHERE, IF", num: "①" },
|
||||
{ key: "field" as const, label: t("settings.customThemeField"), example: "id, name, _var", num: "②" },
|
||||
{ key: "function" as const, label: t("settings.customThemeFunction"), example: "count, upper", num: "③" },
|
||||
{ key: "string" as const, label: t("settings.customThemeString"), example: "'hello', 'world'", num: "④" },
|
||||
{ key: "number" as const, label: t("settings.customThemeNumber"), example: "100, 3.14", num: "⑤" },
|
||||
{ key: "comment" as const, label: t("settings.customThemeComment"), example: "--, /* */", num: "⑥" },
|
||||
{ key: "table" as const, label: t("settings.customThemeTable"), example: "users, orders", num: "⑦" },
|
||||
{ key: "operator" as const, label: t("settings.customThemeOperator"), example: "=, >, <>", num: "⑧" },
|
||||
{ key: "type" as const, label: t("settings.customThemeType"), example: "INTEGER, TEXT", num: "⑨" },
|
||||
{ key: "builtin" as const, label: t("settings.customThemeBuiltin"), example: "FOUND, SQLERRM", num: "⑩" },
|
||||
{ key: "background" as const, label: t("settings.customThemeBackground"), example: "Editor background", num: "⑪" },
|
||||
{ key: "foreground" as const, label: t("settings.customThemeForeground"), example: "Default text color", num: "⑫" },
|
||||
];
|
||||
|
||||
// Preset color themes (including all built-in themes)
|
||||
const presetThemes = [
|
||||
{
|
||||
name: "Default",
|
||||
colors: { ...DEFAULT_CUSTOM_THEME_COLORS },
|
||||
},
|
||||
{
|
||||
name: "One Dark",
|
||||
colors: {
|
||||
keyword: "#c678dd",
|
||||
field: "#e06c75",
|
||||
function: "#61afef",
|
||||
string: "#98c379",
|
||||
number: "#d19a66",
|
||||
comment: "#5c6370",
|
||||
table: "#98c379",
|
||||
operator: "#56b6c2",
|
||||
type: "#e5c07b",
|
||||
builtin: "#61afef",
|
||||
background: "#282c34",
|
||||
foreground: "#abb2bf",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "VS Code Dark+",
|
||||
colors: {
|
||||
keyword: "#569cd6",
|
||||
field: "#9cdcfe",
|
||||
function: "#dcdcaa",
|
||||
string: "#ce9178",
|
||||
number: "#b5cea8",
|
||||
comment: "#6a9955",
|
||||
table: "#ce9178",
|
||||
operator: "#d4d4d4",
|
||||
type: "#4ec9b0",
|
||||
builtin: "#dcdcaa",
|
||||
background: "#1e1e1e",
|
||||
foreground: "#9cdcfe",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nord",
|
||||
colors: {
|
||||
keyword: "#5e81ac",
|
||||
field: "#88c0d0",
|
||||
function: "#8fbcbb",
|
||||
string: "#a3be8c",
|
||||
number: "#b48ead",
|
||||
comment: "#4c566a",
|
||||
table: "#a3be8c",
|
||||
operator: "#a3be8c",
|
||||
type: "#ebcb8b",
|
||||
builtin: "#8fbcbb",
|
||||
background: "#2e3440",
|
||||
foreground: "#d8dee9",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Okaidia",
|
||||
colors: {
|
||||
keyword: "#f92672",
|
||||
field: "#a6e22e",
|
||||
function: "#fd971f",
|
||||
string: "#e6db74",
|
||||
number: "#ae81ff",
|
||||
comment: "#8292a2",
|
||||
table: "#e6db74",
|
||||
operator: "#f92672",
|
||||
type: "#66d9ef",
|
||||
builtin: "#fd971f",
|
||||
background: "#272822",
|
||||
foreground: "#f8f8f2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Material",
|
||||
colors: {
|
||||
keyword: "#cf6edf",
|
||||
field: "#56c8d8",
|
||||
function: "#56c8d8",
|
||||
string: "#a3be8c",
|
||||
number: "#ffad42",
|
||||
comment: "#808080",
|
||||
table: "#a3be8c",
|
||||
operator: "#cf6edf",
|
||||
type: "#ffad42",
|
||||
builtin: "#56c8d8",
|
||||
background: "#2e3235",
|
||||
foreground: "#bdbdbd",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dracula",
|
||||
colors: {
|
||||
keyword: "#ff79c6",
|
||||
field: "#8be9fd",
|
||||
function: "#50fa7b",
|
||||
string: "#f1fa8c",
|
||||
number: "#bd93f9",
|
||||
comment: "#6272a4",
|
||||
table: "#f1fa8c",
|
||||
operator: "#ff79c6",
|
||||
type: "#8be9fd",
|
||||
builtin: "#50fa7b",
|
||||
background: "#282a36",
|
||||
foreground: "#f8f8f2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Solarized Dark",
|
||||
colors: {
|
||||
keyword: "#cb4b16",
|
||||
field: "#268bd2",
|
||||
function: "#b58900",
|
||||
string: "#2aa198",
|
||||
number: "#d33682",
|
||||
comment: "#586e75",
|
||||
table: "#2aa198",
|
||||
operator: "#cb4b16",
|
||||
type: "#859900",
|
||||
builtin: "#b58900",
|
||||
background: "#002b36",
|
||||
foreground: "#839496",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "VS Code Light+",
|
||||
colors: {
|
||||
keyword: "#0000ff",
|
||||
field: "#001080",
|
||||
function: "#795e26",
|
||||
string: "#a31515",
|
||||
number: "#098658",
|
||||
comment: "#008000",
|
||||
table: "#a31515",
|
||||
operator: "#000000",
|
||||
type: "#267f99",
|
||||
builtin: "#795e26",
|
||||
background: "#ffffff",
|
||||
foreground: "#000000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Duotone Light",
|
||||
colors: {
|
||||
keyword: "#6e4cbd",
|
||||
field: "#1a1a1a",
|
||||
function: "#6e4cbd",
|
||||
string: "#6e4cbd",
|
||||
number: "#6e4cbd",
|
||||
comment: "#a0a0a0",
|
||||
table: "#6e4cbd",
|
||||
operator: "#1a1a1a",
|
||||
type: "#6e4cbd",
|
||||
builtin: "#6e4cbd",
|
||||
background: "#faf8f5",
|
||||
foreground: "#1a1a1a",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Duotone Dark",
|
||||
colors: {
|
||||
keyword: "#9375f5",
|
||||
field: "#ddd",
|
||||
function: "#9375f5",
|
||||
string: "#9375f5",
|
||||
number: "#9375f5",
|
||||
comment: "#777",
|
||||
table: "#9375f5",
|
||||
operator: "#ddd",
|
||||
type: "#9375f5",
|
||||
builtin: "#9375f5",
|
||||
background: "#2a2734",
|
||||
foreground: "#ddd",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Xcode",
|
||||
colors: {
|
||||
keyword: "#ad3da4",
|
||||
field: "#5c2699",
|
||||
function: "#3d1c77",
|
||||
string: "#d12f1b",
|
||||
number: "#272ad8",
|
||||
comment: "#9ba2aa",
|
||||
table: "#d12f1b",
|
||||
operator: "#000000",
|
||||
type: "#234d97",
|
||||
builtin: "#3d1c77",
|
||||
background: "#ffffff",
|
||||
foreground: "#000000",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const selectedPreset = ref("");
|
||||
|
||||
function applyPreset() {
|
||||
const preset = presetThemes.find((p) => p.name === selectedPreset.value);
|
||||
if (!preset) return;
|
||||
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
if (theme) {
|
||||
theme.colors = { ...DEFAULT_CUSTOM_THEME_COLORS, ...preset.colors };
|
||||
toast(t("settings.customThemeApplied", { name: preset.name }), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Basic palette colors (similar to Windows color picker)
|
||||
const basicColors = [
|
||||
["#000000", "#7f7f7f", "#880015", "#ed1c24", "#ff7f27", "#fff200", "#22b14c", "#00a2e8"],
|
||||
["#3f48cc", "#a349a4", "#ffffff", "#c3c3c3", "#b97a57", "#ffaec9", "#ffc90e", "#efe4b0"],
|
||||
];
|
||||
|
||||
const expandedPalette = ref<string | null>(null);
|
||||
|
||||
function togglePalette(key: string) {
|
||||
expandedPalette.value = expandedPalette.value === key ? null : key;
|
||||
}
|
||||
|
||||
function applyBasicColor(key: keyof CustomThemeColors, color: string) {
|
||||
handleColorChange(key, color);
|
||||
expandedPalette.value = null;
|
||||
}
|
||||
|
||||
const previewCode = computed(() => {
|
||||
const c = localColors.value;
|
||||
return [
|
||||
{ text: "SELECT ", color: c.keyword, num: "①" },
|
||||
{ text: "id", color: c.field, num: "②" },
|
||||
{ text: ", ", color: c.operator, num: "⑧" },
|
||||
{ text: "count", color: c.function, num: "③" },
|
||||
{ text: "(*) ", color: c.operator, num: "⑧" },
|
||||
{ text: "FROM ", color: c.keyword, num: "①" },
|
||||
{ text: "users", color: c.table, num: "⑦" },
|
||||
{ text: " ", color: "", num: "" },
|
||||
{ text: "WHERE ", color: c.keyword, num: "①" },
|
||||
{ text: "status", color: c.field, num: "②" },
|
||||
{ text: " = ", color: c.operator, num: "⑧" },
|
||||
{ text: "'active'", color: c.string, num: "④" },
|
||||
{ text: " ", color: "", num: "" },
|
||||
{ text: "AND ", color: c.keyword, num: "①" },
|
||||
{ text: "id", color: c.field, num: "②" },
|
||||
{ text: " > ", color: c.operator, num: "⑧" },
|
||||
{ text: "100", color: c.number, num: "⑤" },
|
||||
{ text: ";", color: c.operator, num: "⑧" },
|
||||
];
|
||||
});
|
||||
|
||||
function handleColorChange(key: keyof CustomThemeColors, value: string) {
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
if (theme) {
|
||||
theme.colors = { ...theme.colors, [key]: value };
|
||||
}
|
||||
}
|
||||
|
||||
function handleJsonChange() {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText.value);
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
if (theme) {
|
||||
theme.colors = { ...DEFAULT_CUSTOM_THEME_COLORS, ...parsed };
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
emit(
|
||||
"save",
|
||||
localThemes.value.map((t) => ({ ...t })),
|
||||
activeEditId.value,
|
||||
);
|
||||
emit("update:open", false);
|
||||
}
|
||||
|
||||
function handleAddTheme() {
|
||||
const id = `custom-${Date.now()}`;
|
||||
const name = `${t("settings.customThemeDefaultName")} ${localThemes.value.length + 1}`;
|
||||
localThemes.value.push({
|
||||
id,
|
||||
name,
|
||||
colors: { ...DEFAULT_CUSTOM_THEME_COLORS },
|
||||
});
|
||||
activeEditId.value = id;
|
||||
}
|
||||
|
||||
function handleDeleteTheme(id: string) {
|
||||
if (localThemes.value.length <= 1) {
|
||||
toast(t("settings.customThemeKeepOne"), 3000);
|
||||
return;
|
||||
}
|
||||
localThemes.value = localThemes.value.filter((t) => t.id !== id);
|
||||
if (activeEditId.value === id) {
|
||||
activeEditId.value = localThemes.value[0]?.id ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
function handleDuplicateTheme(theme: CustomTheme) {
|
||||
const id = `custom-${Date.now()}`;
|
||||
localThemes.value.push({
|
||||
id,
|
||||
name: `${theme.name}${t("settings.customThemeCopySuffix")}`,
|
||||
colors: { ...theme.colors },
|
||||
});
|
||||
activeEditId.value = id;
|
||||
}
|
||||
|
||||
function startRename(theme: CustomTheme) {
|
||||
renamingId.value = theme.id;
|
||||
renamingName.value = theme.name;
|
||||
}
|
||||
|
||||
function confirmRename() {
|
||||
if (!renamingId.value) return;
|
||||
const theme = localThemes.value.find((t) => t.id === renamingId.value);
|
||||
if (theme && renamingName.value.trim()) {
|
||||
theme.name = renamingName.value.trim();
|
||||
}
|
||||
renamingId.value = null;
|
||||
renamingName.value = "";
|
||||
}
|
||||
|
||||
function cancelRename() {
|
||||
renamingId.value = null;
|
||||
renamingName.value = "";
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
if (!theme) return;
|
||||
const blob = new Blob([JSON.stringify(theme.colors, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `dbx-theme-${theme.name}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText.value);
|
||||
const theme = localThemes.value.find((t) => t.id === activeEditId.value);
|
||||
if (theme) {
|
||||
theme.colors = { ...DEFAULT_CUSTOM_THEME_COLORS, ...parsed };
|
||||
}
|
||||
} catch (e) {
|
||||
toast(t("settings.customThemeJsonError"), 3000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
||||
<DialogContent class="sm:max-w-[860px] max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("settings.customThemeTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex-1 min-h-0 flex gap-4">
|
||||
<!-- Theme list sidebar -->
|
||||
<div class="w-48 shrink-0 flex flex-col gap-2">
|
||||
<div class="text-sm font-medium px-1">{{ t("settings.customThemeMyThemes") }}</div>
|
||||
<div class="flex-1 overflow-y-auto space-y-1 pr-1">
|
||||
<div
|
||||
v-for="theme in localThemes"
|
||||
:key="theme.id"
|
||||
class="group flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer text-sm"
|
||||
:class="activeEditId === theme.id ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
@click="activeEditId = theme.id"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-if="renamingId === theme.id" class="flex items-center gap-1" @click.stop>
|
||||
<Input
|
||||
v-model="renamingName"
|
||||
class="h-6 text-xs px-1 py-0"
|
||||
@keydown.enter="confirmRename"
|
||||
@keydown.esc="cancelRename"
|
||||
@blur="confirmRename"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="truncate">{{ theme.name }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click.stop="startRename(theme)">
|
||||
<Pencil class="h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click.stop="handleDuplicateTheme(theme)">
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click.stop="handleDeleteTheme(theme.id)">
|
||||
<Trash2 class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" class="w-full gap-1" @click="handleAddTheme">
|
||||
<Plus class="h-4 w-4" />
|
||||
{{ t("settings.customThemeNewTheme") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Edit area -->
|
||||
<div class="flex-1 min-w-0 overflow-hidden flex flex-col">
|
||||
<Tabs defaultValue="visual" class="w-full flex-1 flex flex-col">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="visual">{{ t("settings.customThemeVisualEdit") }}</TabsTrigger>
|
||||
<TabsTrigger value="json">{{ t("settings.customThemeJsonConfig") }}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="visual" class="space-y-4 flex-1 overflow-y-auto pr-1">
|
||||
<!-- Preview area -->
|
||||
<div class="rounded-lg border bg-black/50 p-5 font-mono text-base">
|
||||
<div class="mb-2 text-sm text-muted-foreground">{{ t("settings.customThemeLivePreview") }}</div>
|
||||
<div class="leading-relaxed text-lg">
|
||||
<span v-for="(token, i) in previewCode" :key="i" :style="{ color: token.color }" class="inline">
|
||||
{{ token.text }}<sup v-if="token.num" class="text-xl opacity-60">{{ token.num }}</sup>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-lg" :style="{ color: localColors.comment }">
|
||||
<sup class="text-xl">⑥</sup> -- {{ t("settings.customThemePreviewExample") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preset color schemes -->
|
||||
<div class="flex items-center gap-2 rounded-lg border p-3 bg-muted/30">
|
||||
<Palette class="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span class="text-sm text-muted-foreground shrink-0">{{ t("settings.customThemePreset") }}:</span>
|
||||
<Select v-model="selectedPreset" class="flex-1">
|
||||
<SelectTrigger class="h-8 text-sm">
|
||||
<SelectValue :placeholder="t('settings.customThemeSelectPreset')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="preset in presetThemes" :key="preset.name" :value="preset.name">
|
||||
{{ preset.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" class="h-8 shrink-0" @click="applyPreset">
|
||||
<Copy class="mr-1 h-3 w-3" />
|
||||
{{ t("settings.customThemeApply") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Color configuration list -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="item in colorItems"
|
||||
:key="item.key"
|
||||
class="relative flex items-center gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<span class="text-xl font-bold w-8 text-center shrink-0">{{ item.num }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-sm">{{ item.label }}</div>
|
||||
<div class="text-xs text-muted-foreground truncate">{{ item.example }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<!-- Color square + dropdown arrow -->
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-0.5 rounded border p-0.5 hover:bg-muted transition-colors"
|
||||
@click.stop="togglePalette(item.key)"
|
||||
>
|
||||
<div class="h-6 w-6 rounded-sm" :style="{ backgroundColor: localColors[item.key] }" />
|
||||
<ChevronDown class="h-3 w-3 text-muted-foreground pointer-events-none" />
|
||||
</button>
|
||||
<!-- Palette popup -->
|
||||
<div
|
||||
v-if="expandedPalette === item.key"
|
||||
class="absolute right-0 top-full z-50 mt-1 rounded-lg border bg-popover p-2 shadow-lg"
|
||||
@click.stop
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div v-for="(row, rowIndex) in basicColors" :key="rowIndex" class="flex gap-1">
|
||||
<button
|
||||
v-for="color in row"
|
||||
:key="color"
|
||||
type="button"
|
||||
class="h-5 w-5 rounded-sm border border-border/50 hover:scale-110 transition-transform"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="applyBasicColor(item.key, color)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-t flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
:value="localColors[item.key]"
|
||||
@input="handleColorChange(item.key, ($event.target as HTMLInputElement).value)"
|
||||
class="h-6 w-6 cursor-pointer rounded border-0 p-0"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
:value="localColors[item.key]"
|
||||
@input="handleColorChange(item.key, ($event.target as HTMLInputElement).value)"
|
||||
class="w-20 rounded border px-2 py-0.5 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="json" class="space-y-4 flex-1 flex flex-col min-h-[400px]">
|
||||
<textarea
|
||||
v-model="jsonText"
|
||||
@blur="handleJsonChange"
|
||||
class="flex-1 w-full rounded-lg border bg-black/50 p-4 font-mono text-sm min-h-[360px]"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" size="sm" @click="handleImport">{{
|
||||
t("settings.customThemePasteImport")
|
||||
}}</Button>
|
||||
<Button variant="outline" size="sm" @click="handleExport">{{
|
||||
t("settings.customThemeExportJson")
|
||||
}}</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="gap-2">
|
||||
<Button variant="outline" @click="emit('update:open', false)">{{ t("settings.cancel") }}</Button>
|
||||
<Button @click="handleSave">{{ t("settings.customThemeSaveAndApply") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
|
@ -1756,6 +1756,38 @@ export default {
|
|||
shortcutPressShortcut: "Press shortcut",
|
||||
shortcutConflict: "This shortcut conflicts with another action in the same scope.",
|
||||
preview: "Live Preview",
|
||||
customTheme: "Custom Theme",
|
||||
customThemeConfigure: "Configure Custom Theme",
|
||||
customThemeTitle: "Custom Theme Configuration",
|
||||
customThemeMyThemes: "My Themes",
|
||||
customThemeNewTheme: "New Theme",
|
||||
customThemeVisualEdit: "Visual Editor",
|
||||
customThemeJsonConfig: "JSON Config",
|
||||
customThemeLivePreview: "Live Preview",
|
||||
customThemePreviewExample: "-- Query Example",
|
||||
customThemePreset: "Preset Theme",
|
||||
customThemeSelectPreset: "Select preset theme...",
|
||||
customThemeApply: "Apply",
|
||||
customThemePasteImport: "Paste Import",
|
||||
customThemeExportJson: "Export JSON",
|
||||
customThemeSaveAndApply: "Save & Apply",
|
||||
customThemeKeyword: "Keyword",
|
||||
customThemeField: "Field/Variable",
|
||||
customThemeFunction: "Function",
|
||||
customThemeString: "String",
|
||||
customThemeNumber: "Number",
|
||||
customThemeComment: "Comment",
|
||||
customThemeTable: "Table",
|
||||
customThemeOperator: "Operator",
|
||||
customThemeType: "Type",
|
||||
customThemeBuiltin: "Builtin",
|
||||
customThemeBackground: "Background",
|
||||
customThemeForeground: "Foreground",
|
||||
customThemeDefaultName: "Custom Theme",
|
||||
customThemeCopySuffix: " Copy",
|
||||
customThemeApplied: 'Applied "{name}" color scheme',
|
||||
customThemeKeepOne: "Keep at least one theme",
|
||||
customThemeJsonError: "Invalid JSON format, please check",
|
||||
jdbcPlugin: "DBX JDBC plugin",
|
||||
jdbcPluginInstall: "Install JDBC plugin",
|
||||
jdbcPluginInstallSuccess: "JDBC plugin installed",
|
||||
|
|
|
|||
|
|
@ -1646,6 +1646,38 @@ export default {
|
|||
shortcutPressShortcut: "Presiona un atajo",
|
||||
shortcutConflict: "Este atajo entra en conflicto con otra acción del mismo ámbito.",
|
||||
preview: "Vista previa en tiempo real",
|
||||
customTheme: "Tema personalizado",
|
||||
customThemeConfigure: "Configurar tema personalizado",
|
||||
customThemeTitle: "Configuración de tema personalizado",
|
||||
customThemeMyThemes: "Mis temas",
|
||||
customThemeNewTheme: "Nuevo tema",
|
||||
customThemeVisualEdit: "Editor visual",
|
||||
customThemeJsonConfig: "Configuración JSON",
|
||||
customThemeLivePreview: "Vista previa en tiempo real",
|
||||
customThemePreviewExample: "Ejemplo de consulta",
|
||||
customThemePreset: "Tema predefinido",
|
||||
customThemeSelectPreset: "Seleccionar tema predefinido...",
|
||||
customThemeApply: "Aplicar",
|
||||
customThemePasteImport: "Importar desde portapapeles",
|
||||
customThemeExportJson: "Exportar JSON",
|
||||
customThemeSaveAndApply: "Guardar y aplicar",
|
||||
customThemeKeyword: "Palabra clave",
|
||||
customThemeField: "Campo/Variable",
|
||||
customThemeFunction: "Función",
|
||||
customThemeString: "Cadena",
|
||||
customThemeNumber: "Número",
|
||||
customThemeComment: "Comentario",
|
||||
customThemeTable: "Tabla",
|
||||
customThemeOperator: "Operador",
|
||||
customThemeType: "Tipo",
|
||||
customThemeBuiltin: "Variable integrada",
|
||||
customThemeBackground: "Color de fondo",
|
||||
customThemeForeground: "Color de primer plano",
|
||||
customThemeDefaultName: "Tema personalizado",
|
||||
customThemeCopySuffix: " Copia",
|
||||
customThemeApplied: 'Esquema de color "{name}" aplicado',
|
||||
customThemeKeepOne: "Mantener al menos un tema",
|
||||
customThemeJsonError: "Formato JSON inválido, por favor verifique",
|
||||
jdbcPlugin: "Plugin JDBC de DBX",
|
||||
jdbcPluginInstall: "Instalar plugin JDBC",
|
||||
jdbcPluginInstallSuccess: "Plugin JDBC instalado",
|
||||
|
|
|
|||
|
|
@ -1712,6 +1712,38 @@ export default {
|
|||
shortcutPressShortcut: "按下快捷键",
|
||||
shortcutConflict: "这个快捷键与同一作用域内的其他操作冲突。",
|
||||
preview: "实时预览",
|
||||
customTheme: "自定义主题",
|
||||
customThemeConfigure: "自定义主题配置",
|
||||
customThemeTitle: "自定义主题配置",
|
||||
customThemeMyThemes: "我的主题",
|
||||
customThemeNewTheme: "新建主题",
|
||||
customThemeVisualEdit: "可视化编辑",
|
||||
customThemeJsonConfig: "JSON 配置",
|
||||
customThemeLivePreview: "实时预览",
|
||||
customThemePreviewExample: "查询示例",
|
||||
customThemePreset: "预设配色",
|
||||
customThemeSelectPreset: "选择预设配色方案",
|
||||
customThemeApply: "应用",
|
||||
customThemePasteImport: "粘贴导入",
|
||||
customThemeExportJson: "导出 JSON",
|
||||
customThemeSaveAndApply: "保存并应用",
|
||||
customThemeKeyword: "关键字",
|
||||
customThemeField: "字段/变量",
|
||||
customThemeFunction: "函数",
|
||||
customThemeString: "字符串",
|
||||
customThemeNumber: "数字",
|
||||
customThemeComment: "注释",
|
||||
customThemeTable: "表名",
|
||||
customThemeOperator: "运算符",
|
||||
customThemeType: "类型",
|
||||
customThemeBuiltin: "内置变量",
|
||||
customThemeBackground: "背景色",
|
||||
customThemeForeground: "前景色",
|
||||
customThemeDefaultName: "自定义主题",
|
||||
customThemeCopySuffix: " 副本",
|
||||
customThemeApplied: "已应用「{name}」配色方案",
|
||||
customThemeKeepOne: "至少保留一个主题",
|
||||
customThemeJsonError: "JSON 格式错误,请检查",
|
||||
jdbcPlugin: "DBX JDBC 插件",
|
||||
jdbcPluginInstall: "安装 JDBC 插件",
|
||||
jdbcPluginInstallSuccess: "JDBC 插件已安装",
|
||||
|
|
|
|||
|
|
@ -1686,6 +1686,38 @@ export default {
|
|||
shortcutPressShortcut: "按下快速鍵",
|
||||
shortcutConflict: "這個快速鍵與同一作用域內的其他操作衝突。",
|
||||
preview: "即時預覽",
|
||||
customTheme: "自定義主題",
|
||||
customThemeConfigure: "自定義主題配置",
|
||||
customThemeTitle: "自定義主題配置",
|
||||
customThemeMyThemes: "我的主題",
|
||||
customThemeNewTheme: "新建主題",
|
||||
customThemeVisualEdit: "可視化編輯",
|
||||
customThemeJsonConfig: "JSON 配置",
|
||||
customThemeLivePreview: "即時預覽",
|
||||
customThemePreviewExample: "查詢示例",
|
||||
customThemePreset: "預設配色",
|
||||
customThemeSelectPreset: "選擇預設配色方案",
|
||||
customThemeApply: "應用",
|
||||
customThemePasteImport: "貼上導入",
|
||||
customThemeExportJson: "導出 JSON",
|
||||
customThemeSaveAndApply: "儲存並應用",
|
||||
customThemeKeyword: "關鍵字",
|
||||
customThemeField: "字段/變量",
|
||||
customThemeFunction: "函數",
|
||||
customThemeString: "字符串",
|
||||
customThemeNumber: "數字",
|
||||
customThemeComment: "註釋",
|
||||
customThemeTable: "表名",
|
||||
customThemeOperator: "運算符",
|
||||
customThemeType: "類型",
|
||||
customThemeBuiltin: "內置變量",
|
||||
customThemeBackground: "背景色",
|
||||
customThemeForeground: "前景色",
|
||||
customThemeDefaultName: "自定義主題",
|
||||
customThemeCopySuffix: " 副本",
|
||||
customThemeApplied: "已應用「{name}」配色方案",
|
||||
customThemeKeepOne: "至少保留一個主題",
|
||||
customThemeJsonError: "JSON 格式錯誤,請檢查",
|
||||
jdbcPlugin: "DBX JDBC 外掛程式",
|
||||
jdbcPluginInstall: "安裝 JDBC 外掛程式",
|
||||
jdbcPluginInstallSuccess: "JDBC 外掛程式已安裝",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { Extension } from "@codemirror/state";
|
||||
import type { EditorTheme } from "@/stores/settingsStore";
|
||||
import type { EditorTheme, CustomThemeColors } from "@/stores/settingsStore";
|
||||
import type { AppThemeAppearance } from "@/lib/appTheme";
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
type CodeMirrorStyleSpec = Parameters<typeof import("@codemirror/view").EditorView.theme>[0];
|
||||
type LucideIconNode = Array<[string, Record<string, string>]>;
|
||||
|
|
@ -15,6 +17,179 @@ const SUPPORTS_COLOR_MIX =
|
|||
const SUPPORTS_OKLCH =
|
||||
typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", "oklch(0.62 0.19 255)");
|
||||
|
||||
// ==================== 自定义主题配置 ====================
|
||||
// 在这里修改你喜欢的颜色!
|
||||
|
||||
const customThemeColors = {
|
||||
lineNumber: "#6c7086", // 行号颜色
|
||||
lineNumberActive: "#cdd6f4", // 当前行号颜色
|
||||
selection: "#313244", // 选中文本背景
|
||||
cursor: "#f5e0dc", // 光标颜色
|
||||
|
||||
// 语法高亮颜色
|
||||
keyword: "#cba6f7", // 关键字 (SELECT, FROM, WHERE 等)
|
||||
string: "#a6e3a1", // 字符串
|
||||
number: "#fab387", // 数字
|
||||
comment: "#6c7086", // 注释
|
||||
type: "#89b4fa", // 类型 (INTEGER, TEXT 等)
|
||||
variable: "#f38ba8", // 变量
|
||||
function: "#89dceb", // 函数
|
||||
operator: "#89b4fa", // 运算符
|
||||
punctuation: "#9399b2", // 标点符号
|
||||
property: "#f9e2af", // 属性/字段名
|
||||
tag: "#cba6f7", // XML/HTML 标签
|
||||
attribute: "#fab387", // 属性名
|
||||
className: "#f9e2af", // 类名
|
||||
|
||||
// UI 元素
|
||||
gutterBackground: "#181825", // 侧边栏背景
|
||||
activeLine: "#313244", // 当前行高亮
|
||||
matchingBracket: "#45475a", // 匹配括号背景
|
||||
|
||||
// 特殊
|
||||
builtin: "#89dceb", // 内置函数
|
||||
meta: "#cdd6f4", // 元信息
|
||||
invalid: "#f38ba8", // 无效字符
|
||||
};
|
||||
|
||||
/** 创建自定义 CodeMirror 主题 */
|
||||
function createCustomTheme(
|
||||
EditorView: typeof import("@codemirror/view").EditorView,
|
||||
colors?: CustomThemeColors,
|
||||
isDark: boolean = true,
|
||||
): Extension {
|
||||
// 根据系统主题设置默认背景色和前景色
|
||||
const defaultColors = isDark
|
||||
? { background: "#1e1e2e", foreground: "#cdd6f4" }
|
||||
: { background: "#fafafa", foreground: "#242424" };
|
||||
|
||||
const c = { ...defaultColors, ...customThemeColors, ...(colors || {}) };
|
||||
|
||||
// 映射用户自定义属性名到 CodeMirror 内部属性名
|
||||
if (colors) {
|
||||
if (colors.field) {
|
||||
c.variable = colors.field;
|
||||
c.property = colors.field;
|
||||
}
|
||||
if (colors.table) {
|
||||
// 表名通常被识别为 propertyName,如果单独设置了表名颜色则覆盖
|
||||
c.property = colors.table;
|
||||
}
|
||||
}
|
||||
|
||||
const theme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
backgroundColor: c.background,
|
||||
color: c.foreground,
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: c.cursor,
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderLeftColor: c.cursor,
|
||||
},
|
||||
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{
|
||||
backgroundColor: c.selection,
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: c.activeLine,
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: c.gutterBackground,
|
||||
color: c.lineNumber,
|
||||
borderRight: "1px solid #313244",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: c.activeLine,
|
||||
color: c.lineNumberActive,
|
||||
},
|
||||
".cm-matchingBracket": {
|
||||
backgroundColor: c.matchingBracket,
|
||||
outline: "none",
|
||||
},
|
||||
},
|
||||
{ dark: isDark },
|
||||
);
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: c.keyword },
|
||||
{ tag: tags.controlKeyword, color: c.keyword },
|
||||
{ tag: tags.definitionKeyword, color: c.keyword },
|
||||
{ tag: tags.moduleKeyword, color: c.keyword },
|
||||
{ tag: tags.operatorKeyword, color: c.keyword },
|
||||
{ tag: tags.string, color: c.string },
|
||||
{ tag: tags.special(tags.string), color: c.string },
|
||||
{ tag: tags.number, color: c.number },
|
||||
{ tag: tags.integer, color: c.number },
|
||||
{ tag: tags.float, color: c.number },
|
||||
{ tag: tags.comment, color: c.comment, fontStyle: "italic" },
|
||||
{ tag: tags.lineComment, color: c.comment, fontStyle: "italic" },
|
||||
{ tag: tags.blockComment, color: c.comment, fontStyle: "italic" },
|
||||
{ tag: tags.typeName, color: c.type },
|
||||
{ tag: tags.typeOperator, color: c.type },
|
||||
{ tag: tags.name, color: c.variable }, // ← 添加:普通标识符(字段名、表名等)
|
||||
{ tag: tags.variableName, color: c.variable },
|
||||
{ tag: tags.definition(tags.variableName), color: c.variable },
|
||||
{ tag: tags.function(tags.variableName), color: c.function },
|
||||
{ tag: tags.function(tags.propertyName), color: c.function },
|
||||
{ tag: tags.standard(tags.variableName), color: c.builtin },
|
||||
{ tag: tags.propertyName, color: c.property },
|
||||
{ tag: tags.operator, color: c.operator },
|
||||
{ tag: tags.compareOperator, color: c.operator },
|
||||
{ tag: tags.logicOperator, color: c.operator },
|
||||
{ tag: tags.arithmeticOperator, color: c.operator },
|
||||
{ tag: tags.punctuation, color: c.punctuation },
|
||||
{ tag: tags.paren, color: c.punctuation },
|
||||
{ tag: tags.brace, color: c.punctuation },
|
||||
{ tag: tags.bracket, color: c.punctuation },
|
||||
{ tag: tags.tagName, color: c.tag },
|
||||
{ tag: tags.attributeName, color: c.attribute },
|
||||
{ tag: tags.attributeValue, color: c.string },
|
||||
{ tag: tags.className, color: c.className },
|
||||
{ tag: tags.bool, color: c.keyword },
|
||||
{ tag: tags.null, color: c.keyword },
|
||||
{ tag: tags.meta, color: c.meta },
|
||||
{ tag: tags.invalid, color: c.invalid },
|
||||
{ tag: tags.heading, color: c.keyword, fontWeight: "bold" },
|
||||
{ tag: tags.heading1, color: c.keyword, fontWeight: "bold" },
|
||||
{ tag: tags.heading2, color: c.keyword, fontWeight: "bold" },
|
||||
{ tag: tags.heading3, color: c.keyword, fontWeight: "bold" },
|
||||
{ tag: tags.strong, color: c.foreground, fontWeight: "bold" },
|
||||
{ tag: tags.emphasis, color: c.foreground, fontStyle: "italic" },
|
||||
{ tag: tags.link, color: c.type, textDecoration: "underline" },
|
||||
{ tag: tags.url, color: c.type, textDecoration: "underline" },
|
||||
{ tag: tags.labelName, color: c.property },
|
||||
{ tag: tags.namespace, color: c.className },
|
||||
{ tag: tags.macroName, color: c.function },
|
||||
{ tag: tags.literal, color: c.string },
|
||||
{ tag: tags.special(tags.string), color: c.string },
|
||||
{ tag: tags.regexp, color: c.string },
|
||||
{ tag: tags.escape, color: c.string },
|
||||
{ tag: tags.processingInstruction, color: c.keyword },
|
||||
{ tag: tags.inserted, color: c.string },
|
||||
{ tag: tags.deleted, color: c.invalid },
|
||||
{ tag: tags.changed, color: c.property },
|
||||
{ tag: tags.self, color: c.keyword },
|
||||
{ tag: tags.derefOperator, color: c.operator },
|
||||
{ tag: tags.unit, color: c.type },
|
||||
{ tag: tags.angleBracket, color: c.punctuation },
|
||||
{ tag: tags.annotation, color: c.property },
|
||||
{ tag: tags.modifier, color: c.keyword },
|
||||
{ tag: tags.list, color: c.foreground },
|
||||
{ tag: tags.quote, color: c.string, fontStyle: "italic" },
|
||||
{ tag: tags.monospace, color: c.foreground },
|
||||
{ tag: tags.strikethrough, color: c.invalid, textDecoration: "line-through" },
|
||||
{ tag: tags.contentSeparator, color: c.operator },
|
||||
{ tag: tags.special(tags.name), color: c.builtin },
|
||||
]);
|
||||
|
||||
return [theme, syntaxHighlighting(highlightStyle)];
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
|
||||
const TABLE_ICON: LucideIconNode = [
|
||||
["path", { d: "M12 3v18" }],
|
||||
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }],
|
||||
|
|
@ -88,6 +263,7 @@ export function resolveEditorTheme(theme: EditorTheme, appAppearance: AppThemeAp
|
|||
export async function loadEditorTheme(
|
||||
theme: EditorTheme,
|
||||
appAppearance: AppThemeAppearance = "dark",
|
||||
customColors?: CustomThemeColors,
|
||||
): Promise<Extension> {
|
||||
const resolvedTheme = resolveEditorTheme(theme, appAppearance);
|
||||
switch (resolvedTheme) {
|
||||
|
|
@ -109,6 +285,8 @@ export async function loadEditorTheme(
|
|||
return (await import("@uiw/codemirror-theme-duotone")).duotoneDark;
|
||||
case "xcode":
|
||||
return (await import("@uiw/codemirror-theme-xcode")).xcodeLight;
|
||||
case "custom":
|
||||
return createCustomTheme((await import("@codemirror/view")).EditorView, customColors, appAppearance === "dark");
|
||||
default:
|
||||
return (await import("@codemirror/theme-one-dark")).oneDark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ export type EditorTheme =
|
|||
| "material"
|
||||
| "duotone-light"
|
||||
| "duotone-dark"
|
||||
| "xcode";
|
||||
| "xcode"
|
||||
| "custom";
|
||||
|
||||
const STRUCTURE_EDITOR_DENSITIES = ["compact", "standard", "comfortable"] as const;
|
||||
export type StructureEditorDensity = (typeof STRUCTURE_EDITOR_DENSITIES)[number];
|
||||
|
|
@ -190,11 +191,53 @@ export type DataGridRenderMode = (typeof DATA_GRID_RENDER_MODES)[number];
|
|||
const DISCONNECT_TAB_HANDLING_MODES = ["close-tabs", "keep-tabs-clear-results", "keep-tabs-keep-results"] as const;
|
||||
export type DisconnectTabHandlingMode = (typeof DISCONNECT_TAB_HANDLING_MODES)[number];
|
||||
|
||||
// 自定义主题颜色配置
|
||||
export interface CustomThemeColors {
|
||||
keyword: string;
|
||||
field: string;
|
||||
function: string;
|
||||
string: string;
|
||||
number: string;
|
||||
comment: string;
|
||||
table: string;
|
||||
operator: string;
|
||||
type: string;
|
||||
builtin: string;
|
||||
background?: string;
|
||||
foreground?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_CUSTOM_THEME_COLORS: CustomThemeColors = {
|
||||
keyword: "#cba6f7",
|
||||
field: "#f9e2af",
|
||||
function: "#89dceb",
|
||||
string: "#a6e3a1",
|
||||
number: "#fab387",
|
||||
comment: "#6c7086",
|
||||
table: "#a6e3a1",
|
||||
operator: "#89b4fa",
|
||||
type: "#89b4fa",
|
||||
builtin: "#f38ba8",
|
||||
};
|
||||
|
||||
export interface CustomTheme {
|
||||
id: string;
|
||||
name: string;
|
||||
colors: CustomThemeColors;
|
||||
}
|
||||
|
||||
export const DEFAULT_CUSTOM_THEMES: CustomTheme[] = [
|
||||
{ id: "default", name: "自定义", colors: { ...DEFAULT_CUSTOM_THEME_COLORS } },
|
||||
];
|
||||
|
||||
export interface EditorSettings {
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
uiScale: number;
|
||||
theme: EditorTheme;
|
||||
customThemeColors: CustomThemeColors;
|
||||
customThemes: CustomTheme[];
|
||||
activeCustomThemeId: string;
|
||||
executeMode: "all" | "current";
|
||||
wordWrap: boolean;
|
||||
confirmDangerousSqlExecution: boolean;
|
||||
|
|
@ -236,6 +279,7 @@ export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }
|
|||
{ value: "duotone-light", label: "Duotone Light", dark: false },
|
||||
{ value: "duotone-dark", label: "Duotone Dark", dark: true },
|
||||
{ value: "xcode", label: "Xcode", dark: false },
|
||||
{ value: "custom", label: "Custom (可自定义)", dark: true },
|
||||
];
|
||||
|
||||
const EDITOR_THEME_VALUES = new Set<EditorTheme>(EDITOR_THEMES.map((theme) => theme.value));
|
||||
|
|
@ -255,6 +299,9 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
fontSize: 13,
|
||||
uiScale: 1,
|
||||
theme: "app",
|
||||
customThemeColors: { ...DEFAULT_CUSTOM_THEME_COLORS },
|
||||
customThemes: [...DEFAULT_CUSTOM_THEMES],
|
||||
activeCustomThemeId: "default",
|
||||
executeMode: "all",
|
||||
wordWrap: false,
|
||||
confirmDangerousSqlExecution: true,
|
||||
|
|
@ -385,6 +432,29 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
|
||||
uiScale: normalizeUiScale(settings.uiScale),
|
||||
theme: settings.theme && EDITOR_THEME_VALUES.has(settings.theme) ? settings.theme : DEFAULT_EDITOR_SETTINGS.theme,
|
||||
customThemeColors: {
|
||||
...DEFAULT_CUSTOM_THEME_COLORS,
|
||||
...settings.customThemeColors,
|
||||
},
|
||||
customThemes: (() => {
|
||||
if (Array.isArray(settings.customThemes) && settings.customThemes.length > 0) {
|
||||
// 自动重命名"默认"为"自定义"
|
||||
return settings.customThemes.map((theme) => (theme.name === "默认" ? { ...theme, name: "自定义" } : theme));
|
||||
}
|
||||
return [
|
||||
...(settings.customThemeColors
|
||||
? [
|
||||
{
|
||||
id: "migrated",
|
||||
name: "已迁移",
|
||||
colors: { ...DEFAULT_CUSTOM_THEME_COLORS, ...settings.customThemeColors },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...DEFAULT_CUSTOM_THEMES,
|
||||
];
|
||||
})(),
|
||||
activeCustomThemeId: settings.activeCustomThemeId ?? "default",
|
||||
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
|
||||
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
||||
confirmDangerousSqlExecution:
|
||||
|
|
@ -543,6 +613,29 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
if (partial.fontSize !== undefined) editorSettings.value.fontSize = partial.fontSize;
|
||||
if (partial.uiScale !== undefined) editorSettings.value.uiScale = normalizeUiScale(partial.uiScale);
|
||||
if (partial.theme !== undefined) editorSettings.value.theme = partial.theme;
|
||||
if (partial.customThemeColors !== undefined) {
|
||||
editorSettings.value.customThemeColors = {
|
||||
...editorSettings.value.customThemeColors,
|
||||
...partial.customThemeColors,
|
||||
};
|
||||
}
|
||||
if (partial.customThemes !== undefined) {
|
||||
editorSettings.value.customThemes = Array.isArray(partial.customThemes)
|
||||
? partial.customThemes
|
||||
: editorSettings.value.customThemes;
|
||||
}
|
||||
if (partial.activeCustomThemeId !== undefined) {
|
||||
editorSettings.value.activeCustomThemeId = partial.activeCustomThemeId;
|
||||
}
|
||||
// 同步 customThemeColors 为当前激活主题的颜色(兼容回退逻辑)
|
||||
if (partial.customThemes !== undefined || partial.activeCustomThemeId !== undefined) {
|
||||
const themes = editorSettings.value.customThemes;
|
||||
const activeId = editorSettings.value.activeCustomThemeId;
|
||||
const activeTheme = themes.find((t) => t.id === activeId) || themes[0];
|
||||
if (activeTheme) {
|
||||
editorSettings.value.customThemeColors = { ...activeTheme.colors };
|
||||
}
|
||||
}
|
||||
if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode;
|
||||
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
|
||||
if (partial.confirmDangerousSqlExecution !== undefined)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
"vue-virtual-scroller": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@oxfmt/binding-darwin-arm64": "0.53.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.68.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ importers:
|
|||
specifier: ^3.0.4
|
||||
version: 3.0.4(vue@3.5.35(typescript@6.0.3))
|
||||
devDependencies:
|
||||
'@lezer/highlight':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@oxfmt/binding-darwin-arm64':
|
||||
specifier: 0.53.0
|
||||
version: 0.53.0
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1.0"
|
||||
base64 = "0.22"
|
||||
log = "0.4"
|
||||
tauri = { version = "2.10.3", features = ["tray-icon"] }
|
||||
tauri = { version = "2.10.3", features = ["tray-icon", "custom-protocol"] }
|
||||
tauri-plugin-log = "2"
|
||||
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
fn main() {
|
||||
// Force rebuild to re-embed frontend assets
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue