mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-02 00:29:30 +00:00
471 lines
16 KiB
TypeScript
471 lines
16 KiB
TypeScript
import { defineStore } from "pinia";
|
|
import { ref } from "vue";
|
|
import * as api from "@/lib/api";
|
|
import {
|
|
normalizeColumnFormatter,
|
|
normalizeCustomColumnFormatter,
|
|
type ColumnFormatterConfig,
|
|
type CustomColumnFormatterConfig,
|
|
} from "@/lib/columnFormatter";
|
|
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/shortcutRegistry";
|
|
import { normalizeResultPageSize } from "@/lib/paginationPageSize";
|
|
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebarTableNameDisplay";
|
|
import type { SidebarActivation } from "@/lib/treeNodeClick";
|
|
import type { SqlSnippet } from "@/types/database";
|
|
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sqlCompletion";
|
|
|
|
export type AiProvider =
|
|
| "claude"
|
|
| "openai"
|
|
| "gemini"
|
|
| "deepseek"
|
|
| "qwen"
|
|
| "ollama"
|
|
| "openai-compatible"
|
|
| "custom";
|
|
export type AiApiStyle = "completions" | "responses";
|
|
|
|
export interface AiConfig {
|
|
provider: AiProvider;
|
|
apiKey: string;
|
|
endpoint: string;
|
|
model: string;
|
|
apiStyle: AiApiStyle;
|
|
proxyEnabled?: boolean;
|
|
proxyUrl?: string;
|
|
enableThinking?: boolean;
|
|
}
|
|
|
|
export interface DesktopSettings {
|
|
show_tray_icon: boolean;
|
|
}
|
|
|
|
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
|
show_tray_icon: true,
|
|
};
|
|
|
|
export interface AiProviderPreset extends Omit<AiConfig, "apiKey"> {
|
|
label: string;
|
|
iconSlug?: string;
|
|
requiresApiKey: boolean;
|
|
}
|
|
|
|
export const AI_PROVIDER_PRESETS: Record<AiProvider, AiProviderPreset> = {
|
|
claude: {
|
|
label: "Claude",
|
|
iconSlug: "anthropic",
|
|
provider: "claude",
|
|
endpoint: "https://api.anthropic.com/v1/messages",
|
|
model: "claude-sonnet-4-20250514",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
openai: {
|
|
label: "OpenAI",
|
|
iconSlug: "openai",
|
|
provider: "openai",
|
|
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
model: "gpt-4o-mini",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
gemini: {
|
|
label: "Gemini",
|
|
iconSlug: "googlegemini",
|
|
provider: "gemini",
|
|
endpoint: "https://generativelanguage.googleapis.com",
|
|
model: "gemini-1.5-pro",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
deepseek: {
|
|
label: "DeepSeek",
|
|
iconSlug: "deepseek",
|
|
provider: "deepseek",
|
|
endpoint: "https://api.deepseek.com/v1",
|
|
model: "deepseek-v4-flash",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
qwen: {
|
|
label: "Qwen",
|
|
iconSlug: "alibabacloud",
|
|
provider: "qwen",
|
|
endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
model: "qwen-plus",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
ollama: {
|
|
label: "Ollama",
|
|
iconSlug: "ollama",
|
|
provider: "ollama",
|
|
endpoint: "http://localhost:11434/v1",
|
|
model: "llama3.1",
|
|
apiStyle: "completions",
|
|
requiresApiKey: false,
|
|
},
|
|
"openai-compatible": {
|
|
label: "OpenAI Compatible",
|
|
iconSlug: "openai",
|
|
provider: "openai-compatible",
|
|
endpoint: "",
|
|
model: "",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
custom: {
|
|
label: "Custom",
|
|
provider: "custom",
|
|
endpoint: "",
|
|
model: "",
|
|
apiStyle: "completions",
|
|
requiresApiKey: true,
|
|
},
|
|
};
|
|
|
|
const defaultConfigs: Record<AiProvider, Omit<AiConfig, "apiKey">> = Object.fromEntries(
|
|
Object.entries(AI_PROVIDER_PRESETS).map(([provider, preset]) => {
|
|
const { label: _label, iconSlug: _iconSlug, requiresApiKey: _requiresApiKey, ...config } = preset;
|
|
return [provider, config];
|
|
}),
|
|
) as Record<AiProvider, Omit<AiConfig, "apiKey">>;
|
|
|
|
export function normalizeAiConfig(config: Partial<AiConfig> | null | undefined): AiConfig {
|
|
const provider =
|
|
config?.provider && config.provider in AI_PROVIDER_PRESETS ? config.provider : inferAiProviderFromConfig(config);
|
|
return {
|
|
...defaultConfigs[provider],
|
|
apiKey: config?.apiKey ?? "",
|
|
...config,
|
|
provider,
|
|
apiStyle: config?.apiStyle ?? defaultConfigs[provider].apiStyle,
|
|
proxyEnabled: !!config?.proxyEnabled,
|
|
proxyUrl: config?.proxyUrl ?? "",
|
|
enableThinking: config?.enableThinking ?? true,
|
|
};
|
|
}
|
|
|
|
function inferAiProviderFromConfig(config: Partial<AiConfig> | null | undefined): AiProvider {
|
|
const endpoint = config?.endpoint?.toLowerCase() ?? "";
|
|
const model = config?.model?.toLowerCase() ?? "";
|
|
if (endpoint.includes("deepseek") || model.includes("deepseek")) return "deepseek";
|
|
if (endpoint.includes("dashscope") || endpoint.includes("aliyuncs") || model.includes("qwen")) return "qwen";
|
|
if (endpoint.includes("generativelanguage.googleapis.com") || model.includes("gemini")) return "gemini";
|
|
if (endpoint.includes("localhost:11434") || endpoint.includes("127.0.0.1:11434")) return "ollama";
|
|
if (endpoint.includes("openai.com") || model.startsWith("gpt-")) return "openai";
|
|
return "claude";
|
|
}
|
|
|
|
export type EditorTheme =
|
|
| "app"
|
|
| "one-dark"
|
|
| "vscode-dark"
|
|
| "vscode-light"
|
|
| "nord"
|
|
| "okaidia"
|
|
| "material"
|
|
| "duotone-light"
|
|
| "duotone-dark"
|
|
| "xcode";
|
|
|
|
export interface EditorSettings {
|
|
fontFamily: string;
|
|
fontSize: number;
|
|
theme: EditorTheme;
|
|
executeMode: "all" | "current";
|
|
wordWrap: boolean;
|
|
compactTabTitle: boolean;
|
|
appLayout: "separated" | "classic";
|
|
pageSize: number;
|
|
redisScanPageSize: number;
|
|
mongoViewMode: "document" | "table";
|
|
shortcuts: ShortcutSettings;
|
|
sidebarActivation: SidebarActivation;
|
|
autoSelectActiveSidebarNode: boolean;
|
|
sidebarHiddenTablePrefixes: string[];
|
|
columnFormatters: Record<string, ColumnFormatterConfig>;
|
|
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
|
|
snippets: SqlSnippet[];
|
|
}
|
|
|
|
export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [
|
|
{ value: "app", label: "Follow app theme", dark: false },
|
|
{ value: "one-dark", label: "One Dark", dark: true },
|
|
{ value: "vscode-dark", label: "VS Dark+", dark: true },
|
|
{ value: "vscode-light", label: "VS Light+", dark: false },
|
|
{ value: "nord", label: "Nord", dark: true },
|
|
{ value: "okaidia", label: "Okaidia", dark: true },
|
|
{ value: "material", label: "Material", dark: true },
|
|
{ value: "duotone-light", label: "Duotone Light", dark: false },
|
|
{ value: "duotone-dark", label: "Duotone Dark", dark: true },
|
|
{ value: "xcode", label: "Xcode", dark: false },
|
|
];
|
|
|
|
const EDITOR_THEME_VALUES = new Set<EditorTheme>(EDITOR_THEMES.map((theme) => theme.value));
|
|
|
|
export const FONT_FAMILIES: { value: string; label: string }[] = [
|
|
{ value: "'JetBrains Mono', 'Fira Code', monospace", label: "JetBrains Mono" },
|
|
{ value: "'Fira Code', monospace", label: "Fira Code" },
|
|
{ value: "'Cascadia Code', monospace", label: "Cascadia Code" },
|
|
{ value: "'Source Code Pro', monospace", label: "Source Code Pro" },
|
|
{ value: "'SF Mono', 'Menlo', monospace", label: "SF Mono / Menlo" },
|
|
{ value: "'Consolas', 'Courier New', monospace", label: "Consolas" },
|
|
{ value: "monospace", label: "System Monospace" },
|
|
];
|
|
|
|
export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
|
fontSize: 13,
|
|
theme: "app",
|
|
executeMode: "all",
|
|
wordWrap: false,
|
|
compactTabTitle: false,
|
|
appLayout: "classic",
|
|
pageSize: 100,
|
|
redisScanPageSize: 1000,
|
|
mongoViewMode: "document",
|
|
shortcuts: normalizeShortcutSettings(),
|
|
sidebarActivation: "single",
|
|
autoSelectActiveSidebarNode: false,
|
|
sidebarHiddenTablePrefixes: [],
|
|
columnFormatters: {},
|
|
customColumnFormatters: {},
|
|
snippets: DEFAULT_SQL_SNIPPETS,
|
|
};
|
|
|
|
export const STORAGE_KEY = "dbx-editor-settings";
|
|
const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size";
|
|
|
|
function normalizeColumnFormatters(value: unknown): Record<string, ColumnFormatterConfig> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
const formatters: Record<string, ColumnFormatterConfig> = {};
|
|
for (const [key, formatter] of Object.entries(value as Record<string, unknown>)) {
|
|
const normalized = normalizeColumnFormatter(formatter);
|
|
if (normalized) formatters[key] = normalized;
|
|
}
|
|
return formatters;
|
|
}
|
|
|
|
function normalizeCustomColumnFormatters(value: unknown): Record<string, CustomColumnFormatterConfig> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
const formatters: Record<string, CustomColumnFormatterConfig> = {};
|
|
for (const formatter of Object.values(value as Record<string, unknown>)) {
|
|
const normalized = normalizeCustomColumnFormatter(formatter);
|
|
if (normalized) formatters[normalized.id] = normalized;
|
|
}
|
|
return formatters;
|
|
}
|
|
|
|
function normalizeSqlSnippets(value: unknown, existing?: SqlSnippet[]): SqlSnippet[] {
|
|
if (!Array.isArray(value)) return existing ?? DEFAULT_SQL_SNIPPETS;
|
|
const valid: SqlSnippet[] = [];
|
|
const seenPrefixes = new Set<string>();
|
|
for (const item of value) {
|
|
if (
|
|
!item ||
|
|
typeof item !== "object" ||
|
|
typeof item.id !== "string" ||
|
|
!item.id ||
|
|
typeof item.label !== "string" ||
|
|
!item.label ||
|
|
typeof item.prefix !== "string" ||
|
|
!item.prefix ||
|
|
typeof item.body !== "string"
|
|
) {
|
|
continue;
|
|
}
|
|
if (seenPrefixes.has(item.prefix)) continue;
|
|
seenPrefixes.add(item.prefix);
|
|
valid.push({ id: item.id, label: item.label, prefix: item.prefix, body: item.body });
|
|
}
|
|
if (valid.length === 0) return existing ?? DEFAULT_SQL_SNIPPETS;
|
|
return valid;
|
|
}
|
|
|
|
export function normalizeEditorSettings(settings: Partial<EditorSettings>, existing?: EditorSettings): EditorSettings {
|
|
return {
|
|
fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily,
|
|
fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
|
|
theme: settings.theme && EDITOR_THEME_VALUES.has(settings.theme) ? settings.theme : DEFAULT_EDITOR_SETTINGS.theme,
|
|
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
|
|
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
|
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
|
|
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
|
|
pageSize: normalizeResultPageSize(settings.pageSize),
|
|
redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize,
|
|
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
|
|
shortcuts: normalizeShortcutSettings(settings.shortcuts),
|
|
sidebarActivation:
|
|
settings.sidebarActivation === "single" || settings.sidebarActivation === "double"
|
|
? settings.sidebarActivation
|
|
: DEFAULT_EDITOR_SETTINGS.sidebarActivation,
|
|
autoSelectActiveSidebarNode:
|
|
settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
|
|
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
|
|
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
|
|
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
|
|
snippets: normalizeSqlSnippets(settings.snippets, existing?.snippets),
|
|
};
|
|
}
|
|
|
|
function loadEditorSettings(): EditorSettings {
|
|
// Try new format first
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw) as Partial<EditorSettings>;
|
|
return normalizeEditorSettings(parsed);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Migrate old font-size key if new settings don't exist
|
|
try {
|
|
const oldSize = localStorage.getItem(OLD_FONT_SIZE_KEY);
|
|
if (oldSize) {
|
|
const parsed = parseInt(oldSize, 10);
|
|
if (!isNaN(parsed)) {
|
|
const migrated: EditorSettings = {
|
|
...DEFAULT_EDITOR_SETTINGS,
|
|
fontSize: parsed,
|
|
};
|
|
saveEditorSettings(migrated);
|
|
localStorage.removeItem(OLD_FONT_SIZE_KEY);
|
|
return migrated;
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
return { ...DEFAULT_EDITOR_SETTINGS };
|
|
}
|
|
|
|
function saveEditorSettings(settings: EditorSettings) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
}
|
|
|
|
export const useSettingsStore = defineStore("settings", () => {
|
|
const aiConfig = ref<AiConfig>(normalizeAiConfig({ provider: "claude" }));
|
|
const isAiConfigLoaded = ref(false);
|
|
const desktopSettings = ref<DesktopSettings>({ ...DEFAULT_DESKTOP_SETTINGS });
|
|
const isDesktopSettingsLoaded = ref(false);
|
|
|
|
const editorSettings = ref<EditorSettings>(loadEditorSettings());
|
|
|
|
async function initDesktopSettings() {
|
|
if (isDesktopSettingsLoaded.value) return;
|
|
desktopSettings.value = await api.loadDesktopSettings().catch(() => ({ ...DEFAULT_DESKTOP_SETTINGS }));
|
|
isDesktopSettingsLoaded.value = true;
|
|
}
|
|
|
|
async function updateDesktopSettings(partial: Partial<DesktopSettings>) {
|
|
const previous = desktopSettings.value;
|
|
const next = {
|
|
...desktopSettings.value,
|
|
...partial,
|
|
};
|
|
desktopSettings.value = next;
|
|
try {
|
|
await api.saveDesktopSettings(next);
|
|
} catch (error) {
|
|
desktopSettings.value = previous;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function initAiConfig() {
|
|
if (isAiConfigLoaded.value) return;
|
|
const legacy = localStorage.getItem("dbx-ai-config");
|
|
const saved = await api.loadAiConfig().catch(() => null);
|
|
if (saved) {
|
|
aiConfig.value = normalizeAiConfig(saved);
|
|
} else if (legacy) {
|
|
aiConfig.value = normalizeAiConfig(JSON.parse(legacy));
|
|
await api.saveAiConfig(aiConfig.value).catch(() => {});
|
|
localStorage.removeItem("dbx-ai-config");
|
|
}
|
|
isAiConfigLoaded.value = true;
|
|
}
|
|
|
|
function updateAiConfig(config: Partial<AiConfig>) {
|
|
const previousProvider = aiConfig.value.provider;
|
|
if (config.provider && config.provider !== previousProvider) {
|
|
Object.assign(aiConfig.value, defaultConfigs[config.provider]);
|
|
}
|
|
Object.assign(aiConfig.value, config);
|
|
api.saveAiConfig(aiConfig.value).catch(() => {});
|
|
}
|
|
|
|
function isConfigured(): boolean {
|
|
const preset = AI_PROVIDER_PRESETS[aiConfig.value.provider];
|
|
return !!aiConfig.value.endpoint && !!aiConfig.value.model && (!preset.requiresApiKey || !!aiConfig.value.apiKey);
|
|
}
|
|
|
|
function updateEditorSettings(partial: Partial<EditorSettings>) {
|
|
const normalizedPartial = {
|
|
...partial,
|
|
...(partial.pageSize !== undefined ? { pageSize: normalizeResultPageSize(partial.pageSize) } : {}),
|
|
...(partial.sidebarHiddenTablePrefixes !== undefined
|
|
? { sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes) }
|
|
: {}),
|
|
};
|
|
Object.assign(editorSettings.value, normalizedPartial);
|
|
saveEditorSettings(editorSettings.value);
|
|
}
|
|
|
|
function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) {
|
|
const columnFormatters = { ...editorSettings.value.columnFormatters };
|
|
const normalized = normalizeColumnFormatter(formatter);
|
|
if (normalized) {
|
|
columnFormatters[key] = normalized;
|
|
} else {
|
|
delete columnFormatters[key];
|
|
}
|
|
updateEditorSettings({ columnFormatters });
|
|
}
|
|
|
|
function upsertCustomColumnFormatter(
|
|
formatter: CustomColumnFormatterConfig,
|
|
): CustomColumnFormatterConfig | undefined {
|
|
const normalized = normalizeCustomColumnFormatter(formatter);
|
|
if (!normalized) return undefined;
|
|
updateEditorSettings({
|
|
customColumnFormatters: {
|
|
...editorSettings.value.customColumnFormatters,
|
|
[normalized.id]: normalized,
|
|
},
|
|
});
|
|
return normalized;
|
|
}
|
|
|
|
function deleteCustomColumnFormatter(id: string) {
|
|
const customColumnFormatters = { ...editorSettings.value.customColumnFormatters };
|
|
delete customColumnFormatters[id];
|
|
const columnFormatters = Object.fromEntries(
|
|
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
|
|
return formatter.kind !== "custom-ref" || formatter.formatterId !== id;
|
|
}),
|
|
);
|
|
updateEditorSettings({ customColumnFormatters, columnFormatters });
|
|
}
|
|
|
|
return {
|
|
aiConfig,
|
|
isAiConfigLoaded,
|
|
initAiConfig,
|
|
updateAiConfig,
|
|
isConfigured,
|
|
editorSettings,
|
|
desktopSettings,
|
|
updateEditorSettings,
|
|
initDesktopSettings,
|
|
updateDesktopSettings,
|
|
updateColumnFormatter,
|
|
upsertCustomColumnFormatter,
|
|
deleteCustomColumnFormatter,
|
|
};
|
|
});
|