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:
polempandSam authored and GitHub committed 2026-06-06 20:59:21 +08:00
1 parent 0b07f05f72
commit a067ee8d76
15 files changed
+1190 -43

No files matched your search

@@ -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]">