feat(support-info): add app support information
This commit is contained in:
parent
328147b559
commit
28db8bc1b8
|
|
@ -57,6 +57,7 @@ import {
|
|||
installMcpServer,
|
||||
forgetWebdavSyncSecretsPassphrase,
|
||||
forgetWebdavSavedPassword,
|
||||
getAppSupportInfo,
|
||||
listSystemFonts,
|
||||
saveWebdavSyncSecretsPreference,
|
||||
saveWebdavSavedPassword,
|
||||
|
|
@ -65,6 +66,7 @@ import {
|
|||
webdavSyncSecretsStatus,
|
||||
webdavSyncTest,
|
||||
webdavSyncUpload,
|
||||
type AppSupportInfo,
|
||||
type AiModelInfo,
|
||||
type McpServerStatus,
|
||||
type WebDavConfig,
|
||||
|
|
@ -96,6 +98,7 @@ import { LOCALE_OPTIONS } from "@/lib/app/localeOptions";
|
|||
import { DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, DEFAULT_WEB_DAV_REMOTE_PATH, normalizedWebDavAutoUploadInterval, writeWebDavAutoUploadFields } from "@/lib/webdav/webdavAutoUploadConfig";
|
||||
import { apiUrl } from "@/lib/common/webPath";
|
||||
import { DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
||||
import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
|
|
@ -1195,6 +1198,20 @@ function setSidebarActivation(value: "single" | "double") {
|
|||
const activeSettingsTab = ref("appearance");
|
||||
const isWeb = !isTauriRuntime();
|
||||
const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : ""));
|
||||
const appSupportInfo = ref<AppSupportInfo | null>(null);
|
||||
const appSupportInfoLoading = ref(false);
|
||||
const appSupportInfoError = ref("");
|
||||
const appSupportInfoCopied = ref(false);
|
||||
const appSupportInfoLabels = computed<AppSupportInfoLabels>(() => ({
|
||||
appVersion: t("settings.supportInfoAppVersion"),
|
||||
runtime: t("settings.supportInfoRuntime"),
|
||||
runtimeDesktop: t("settings.supportInfoRuntimeDesktop"),
|
||||
runtimeWeb: t("settings.supportInfoRuntimeWeb"),
|
||||
operatingSystem: t("settings.supportInfoOperatingSystem"),
|
||||
architecture: t("settings.supportInfoArchitecture"),
|
||||
unknown: t("settings.supportInfoUnknown"),
|
||||
}));
|
||||
const appSupportInfoRows = computed(() => (appSupportInfo.value ? buildAppSupportInfoRows(appSupportInfo.value, appSupportInfoLabels.value) : []));
|
||||
type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
|
||||
{ value: "appearance", label: t("settings.appearanceTab") },
|
||||
|
|
@ -1236,6 +1253,44 @@ async function copyDebugLogs() {
|
|||
}, 1500);
|
||||
}
|
||||
|
||||
function fallbackAppSupportInfo(): AppSupportInfo {
|
||||
return {
|
||||
appVersion: props.appVersion || "",
|
||||
runtime: isWeb ? "web" : "desktop",
|
||||
osName: "",
|
||||
osVersion: null,
|
||||
arch: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshAppSupportInfo() {
|
||||
if (appSupportInfoLoading.value) return;
|
||||
appSupportInfoLoading.value = true;
|
||||
appSupportInfoError.value = "";
|
||||
try {
|
||||
appSupportInfo.value = await getAppSupportInfo();
|
||||
} catch (e: any) {
|
||||
appSupportInfo.value = appSupportInfo.value || fallbackAppSupportInfo();
|
||||
appSupportInfoError.value = e?.message || String(e);
|
||||
} finally {
|
||||
appSupportInfoLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyAppSupportInfo() {
|
||||
if (!appSupportInfo.value) await refreshAppSupportInfo();
|
||||
if (!appSupportInfo.value) return;
|
||||
try {
|
||||
await copyToClipboard(formatAppSupportInfoForClipboard(appSupportInfo.value, appSupportInfoLabels.value));
|
||||
appSupportInfoCopied.value = true;
|
||||
window.setTimeout(() => {
|
||||
appSupportInfoCopied.value = false;
|
||||
}, 1500);
|
||||
} catch (e: any) {
|
||||
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function clearDebugLogs() {
|
||||
clearStoredDebugLogs();
|
||||
debugLogCopied.value = false;
|
||||
|
|
@ -1572,6 +1627,7 @@ watch(
|
|||
syncAiEditState();
|
||||
if (!isWeb && activeSettingsTab.value === "mcp") void refreshMcpStatus();
|
||||
if (!isWeb && activeSettingsTab.value === "ai" && aiIsCodexCli.value) void ensureCodexMcpStatus();
|
||||
if (activeSettingsTab.value === "about") void refreshAppSupportInfo();
|
||||
await scrollToInitialSettingsSection();
|
||||
}
|
||||
},
|
||||
|
|
@ -1608,6 +1664,7 @@ watch([webdavAutoUploadEnabled, webdavAutoUploadIntervalMinutes], () => {
|
|||
watch(activeSettingsTab, (tab) => {
|
||||
if (tab === "mcp" && !mcpStatus.value && !mcpStatusLoading.value) void refreshMcpStatus();
|
||||
if (tab === "ai" && aiIsCodexCli.value) void ensureCodexMcpStatus();
|
||||
if (tab === "about" && !appSupportInfo.value) void refreshAppSupportInfo();
|
||||
if (tab === "appearance") {
|
||||
checkLayoutDescTruncation();
|
||||
checkIconThemeDescTruncation();
|
||||
|
|
@ -4144,6 +4201,29 @@ onUnmounted(cleanupPreviewEditor);
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<Label>{{ t("settings.supportInfoTitle") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t("settings.supportInfoDescription") }}</p>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" class="shrink-0" :disabled="appSupportInfoLoading && !appSupportInfo" @click="copyAppSupportInfo">
|
||||
<Loader2 v-if="appSupportInfoLoading && !appSupportInfo" class="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
<CheckCircle2 v-else-if="appSupportInfoCopied" class="mr-1 h-3.5 w-3.5" />
|
||||
<Copy v-else class="mr-1 h-3.5 w-3.5" />
|
||||
{{ appSupportInfoCopied ? t("settings.supportInfoCopied") : t("settings.supportInfoCopy") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="appSupportInfoRows.length" class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div v-for="row in appSupportInfoRows" :key="row.key" class="min-w-0 rounded-md bg-muted/30 px-3 py-2">
|
||||
<div class="text-xs font-medium text-muted-foreground">{{ row.label }}</div>
|
||||
<div class="mt-1 min-w-0 select-text break-words font-mono text-xs text-foreground">{{ row.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="mt-4 text-sm text-muted-foreground">{{ t("settings.supportInfoLoading") }}</p>
|
||||
<p v-if="appSupportInfoError" class="mt-3 text-xs text-destructive">{{ t("settings.supportInfoLoadFailed", { message: appSupportInfoError }) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
|
|
|
|||
|
|
@ -3254,6 +3254,19 @@ export default {
|
|||
mcpRefresh: "Check again",
|
||||
mcpGuide: "MCP guide",
|
||||
aboutDescription: "An open-source, lightweight database management tool.",
|
||||
supportInfoTitle: "Support information",
|
||||
supportInfoDescription: "Copy these environment details when filing issues or asking for help.",
|
||||
supportInfoCopy: "Copy support info",
|
||||
supportInfoCopied: "Copied",
|
||||
supportInfoLoading: "Loading support information...",
|
||||
supportInfoLoadFailed: "Failed to load full support information: {message}",
|
||||
supportInfoAppVersion: "DBX Version",
|
||||
supportInfoRuntime: "Runtime",
|
||||
supportInfoRuntimeDesktop: "Desktop",
|
||||
supportInfoRuntimeWeb: "Web",
|
||||
supportInfoOperatingSystem: "Operating System",
|
||||
supportInfoArchitecture: "Architecture",
|
||||
supportInfoUnknown: "Unknown",
|
||||
community: "Community",
|
||||
qqGroup: "QQ Group",
|
||||
wechatGroup: "WeChat Group",
|
||||
|
|
|
|||
|
|
@ -3155,6 +3155,19 @@ export default withEnglishFallback({
|
|||
mcpRefresh: "Comprobar de nuevo",
|
||||
mcpGuide: "Guía MCP",
|
||||
aboutDescription: "Una herramienta de administración de bases de datos liviana y de código abierto.",
|
||||
supportInfoTitle: "Información de soporte",
|
||||
supportInfoDescription: "Copia estos datos del entorno al crear issues o pedir ayuda.",
|
||||
supportInfoCopy: "Copiar información",
|
||||
supportInfoCopied: "Copiado",
|
||||
supportInfoLoading: "Cargando información de soporte...",
|
||||
supportInfoLoadFailed: "No se pudo cargar toda la información de soporte: {message}",
|
||||
supportInfoAppVersion: "Versión de DBX",
|
||||
supportInfoRuntime: "Entorno",
|
||||
supportInfoRuntimeDesktop: "Escritorio",
|
||||
supportInfoRuntimeWeb: "Web",
|
||||
supportInfoOperatingSystem: "Sistema operativo",
|
||||
supportInfoArchitecture: "Arquitectura",
|
||||
supportInfoUnknown: "Desconocido",
|
||||
community: "Comunidad",
|
||||
qqGroup: "Grupo QQ",
|
||||
wechatGroup: "Grupo WeChat",
|
||||
|
|
|
|||
|
|
@ -3153,6 +3153,19 @@ export default withEnglishFallback({
|
|||
mcpRefresh: "Verifica di nuovo",
|
||||
mcpGuide: "Guida MCP",
|
||||
aboutDescription: "Uno strumento di gestione di database leggero e open-source.",
|
||||
supportInfoTitle: "Informazioni di supporto",
|
||||
supportInfoDescription: "Copia questi dettagli dell'ambiente quando apri issue o chiedi aiuto.",
|
||||
supportInfoCopy: "Copia informazioni",
|
||||
supportInfoCopied: "Copiato",
|
||||
supportInfoLoading: "Caricamento informazioni di supporto...",
|
||||
supportInfoLoadFailed: "Impossibile caricare tutte le informazioni di supporto: {message}",
|
||||
supportInfoAppVersion: "Versione DBX",
|
||||
supportInfoRuntime: "Ambiente",
|
||||
supportInfoRuntimeDesktop: "Desktop",
|
||||
supportInfoRuntimeWeb: "Web",
|
||||
supportInfoOperatingSystem: "Sistema operativo",
|
||||
supportInfoArchitecture: "Architettura",
|
||||
supportInfoUnknown: "Sconosciuto",
|
||||
community: "Community",
|
||||
qqGroup: "Gruppo QQ",
|
||||
wechatGroup: "Gruppo WeChat",
|
||||
|
|
|
|||
|
|
@ -3153,6 +3153,19 @@ export default withEnglishFallback({
|
|||
queryExportKeysetOptimizationEnabled: "キーセット最適化を試行",
|
||||
queryExportKeysetOptimizationEnabledDescription: "安全に認識された単一テーブルクエリにのみ適用。複雑なクエリは自動的にフォールバックします。",
|
||||
aboutDescription: "オープンソースの軽量データベース管理ツール。",
|
||||
supportInfoTitle: "サポート情報",
|
||||
supportInfoDescription: "Issueの作成や問い合わせ時に、この環境情報をコピーできます。",
|
||||
supportInfoCopy: "サポート情報をコピー",
|
||||
supportInfoCopied: "コピーしました",
|
||||
supportInfoLoading: "サポート情報を読み込み中...",
|
||||
supportInfoLoadFailed: "完全なサポート情報の読み込みに失敗しました: {message}",
|
||||
supportInfoAppVersion: "DBXバージョン",
|
||||
supportInfoRuntime: "実行環境",
|
||||
supportInfoRuntimeDesktop: "デスクトップ",
|
||||
supportInfoRuntimeWeb: "Web",
|
||||
supportInfoOperatingSystem: "オペレーティングシステム",
|
||||
supportInfoArchitecture: "アーキテクチャ",
|
||||
supportInfoUnknown: "不明",
|
||||
community: "コミュニティ",
|
||||
qqGroup: "QQグループ",
|
||||
wechatGroup: "WeChatグループ",
|
||||
|
|
|
|||
|
|
@ -3154,6 +3154,19 @@ export default withEnglishFallback({
|
|||
mcpRefresh: "Verificar novamente",
|
||||
mcpGuide: "Guia do MCP",
|
||||
aboutDescription: "Uma ferramenta de gerenciamento de banco de dados leve e de código aberto.",
|
||||
supportInfoTitle: "Informações de suporte",
|
||||
supportInfoDescription: "Copie estes detalhes do ambiente ao abrir issues ou pedir ajuda.",
|
||||
supportInfoCopy: "Copiar informações",
|
||||
supportInfoCopied: "Copiado",
|
||||
supportInfoLoading: "Carregando informações de suporte...",
|
||||
supportInfoLoadFailed: "Falha ao carregar todas as informações de suporte: {message}",
|
||||
supportInfoAppVersion: "Versão do DBX",
|
||||
supportInfoRuntime: "Ambiente",
|
||||
supportInfoRuntimeDesktop: "Desktop",
|
||||
supportInfoRuntimeWeb: "Web",
|
||||
supportInfoOperatingSystem: "Sistema operacional",
|
||||
supportInfoArchitecture: "Arquitetura",
|
||||
supportInfoUnknown: "Desconhecido",
|
||||
community: "Comunidade",
|
||||
qqGroup: "Grupo do QQ",
|
||||
wechatGroup: "Grupo do WeChat",
|
||||
|
|
|
|||
|
|
@ -3257,6 +3257,19 @@ export default withEnglishFallback({
|
|||
mcpRefresh: "重新检查",
|
||||
mcpGuide: "MCP 指南",
|
||||
aboutDescription: "开源、轻量的数据库管理工具。",
|
||||
supportInfoTitle: "支持信息",
|
||||
supportInfoDescription: "提交 issue 或寻求帮助时,可复制这些环境信息。",
|
||||
supportInfoCopy: "复制支持信息",
|
||||
supportInfoCopied: "已复制",
|
||||
supportInfoLoading: "正在加载支持信息...",
|
||||
supportInfoLoadFailed: "完整支持信息加载失败:{message}",
|
||||
supportInfoAppVersion: "DBX 版本",
|
||||
supportInfoRuntime: "运行环境",
|
||||
supportInfoRuntimeDesktop: "桌面端",
|
||||
supportInfoRuntimeWeb: "Web 端",
|
||||
supportInfoOperatingSystem: "操作系统",
|
||||
supportInfoArchitecture: "系统架构",
|
||||
supportInfoUnknown: "未知",
|
||||
community: "社区",
|
||||
qqGroup: "QQ 群",
|
||||
wechatGroup: "微信交流群",
|
||||
|
|
|
|||
|
|
@ -2955,6 +2955,19 @@ export default withEnglishFallback({
|
|||
jdbcDeleteSuccess: "驅動程式已刪除",
|
||||
jdbcNoDrivers: "還沒有匯入 JDBC 驅動程式。",
|
||||
aboutDescription: "開源、輕量的資料庫管理工具。",
|
||||
supportInfoTitle: "支援資訊",
|
||||
supportInfoDescription: "提交 issue 或尋求協助時,可複製這些環境資訊。",
|
||||
supportInfoCopy: "複製支援資訊",
|
||||
supportInfoCopied: "已複製",
|
||||
supportInfoLoading: "正在載入支援資訊...",
|
||||
supportInfoLoadFailed: "完整支援資訊載入失敗:{message}",
|
||||
supportInfoAppVersion: "DBX 版本",
|
||||
supportInfoRuntime: "執行環境",
|
||||
supportInfoRuntimeDesktop: "桌面端",
|
||||
supportInfoRuntimeWeb: "Web 端",
|
||||
supportInfoOperatingSystem: "作業系統",
|
||||
supportInfoArchitecture: "系統架構",
|
||||
supportInfoUnknown: "未知",
|
||||
community: "社群",
|
||||
qqGroup: "QQ 群",
|
||||
wechatGroup: "微信交流群",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import type { AppSupportInfo } from "@/lib/backend/tauri";
|
||||
|
||||
export interface AppSupportInfoLabels {
|
||||
appVersion: string;
|
||||
runtime: string;
|
||||
runtimeDesktop: string;
|
||||
runtimeWeb: string;
|
||||
operatingSystem: string;
|
||||
architecture: string;
|
||||
unknown: string;
|
||||
}
|
||||
|
||||
export interface AppSupportInfoRow {
|
||||
key: keyof Pick<AppSupportInfoLabels, "appVersion" | "runtime" | "operatingSystem" | "architecture">;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function normalizeSupportInfoVersion(version: string | null | undefined, unknownLabel: string): string {
|
||||
const trimmed = version?.trim();
|
||||
if (!trimmed) return unknownLabel;
|
||||
return trimmed.startsWith("v") || trimmed.startsWith("V") ? `v${trimmed.slice(1)}` : `v${trimmed}`;
|
||||
}
|
||||
|
||||
export function formatSupportInfoRuntime(runtime: AppSupportInfo["runtime"], labels: AppSupportInfoLabels): string {
|
||||
if (runtime === "desktop") return labels.runtimeDesktop;
|
||||
if (runtime === "web") return labels.runtimeWeb;
|
||||
return labels.unknown;
|
||||
}
|
||||
|
||||
export function formatSupportInfoOperatingSystem(info: AppSupportInfo, unknownLabel: string): string {
|
||||
const name = info.osName?.trim();
|
||||
const version = info.osVersion?.trim();
|
||||
if (!name && !version) return unknownLabel;
|
||||
return [name, version].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function buildAppSupportInfoRows(info: AppSupportInfo, labels: AppSupportInfoLabels): AppSupportInfoRow[] {
|
||||
return [
|
||||
{
|
||||
key: "appVersion",
|
||||
label: labels.appVersion,
|
||||
value: normalizeSupportInfoVersion(info.appVersion, labels.unknown),
|
||||
},
|
||||
{
|
||||
key: "runtime",
|
||||
label: labels.runtime,
|
||||
value: formatSupportInfoRuntime(info.runtime, labels),
|
||||
},
|
||||
{
|
||||
key: "operatingSystem",
|
||||
label: labels.operatingSystem,
|
||||
value: formatSupportInfoOperatingSystem(info, labels.unknown),
|
||||
},
|
||||
{
|
||||
key: "architecture",
|
||||
label: labels.architecture,
|
||||
value: info.arch?.trim() || labels.unknown,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function formatAppSupportInfoForClipboard(info: AppSupportInfo, labels: AppSupportInfoLabels): string {
|
||||
return buildAppSupportInfoRows(info, labels)
|
||||
.map((row) => `${row.label}: ${row.value}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
|
@ -440,6 +440,7 @@ export const checkForUpdates = forward("checkForUpdates");
|
|||
export const getSystemProxyUrl = forward("getSystemProxyUrl");
|
||||
export const downloadAndInstallUpdate = forward("downloadAndInstallUpdate");
|
||||
export const getAppVersion = forward("getAppVersion");
|
||||
export const getAppSupportInfo = forward("getAppSupportInfo");
|
||||
|
||||
// Layout
|
||||
export const saveSidebarLayout = forward("saveSidebarLayout");
|
||||
|
|
@ -450,6 +451,7 @@ export const loadSidebarLayout = forward("loadSidebarLayout");
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type {
|
||||
AppSupportInfo,
|
||||
AiMessage,
|
||||
AiCompletionRequest,
|
||||
AiTaskContract,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ import type {
|
|||
ExplainSqlBuildResult,
|
||||
DroppedFilePreviewSqlOptions,
|
||||
MongoGridFsFileInfo,
|
||||
AppSupportInfo,
|
||||
} from "@/lib/backend/tauri";
|
||||
import type { QueryEditability } from "@/lib/sql/sqlAnalysis";
|
||||
import type {
|
||||
|
|
@ -2102,6 +2103,17 @@ export async function getAppVersion(): Promise<string> {
|
|||
return res.version;
|
||||
}
|
||||
|
||||
export async function getAppSupportInfo(): Promise<AppSupportInfo> {
|
||||
const appVersion = await getAppVersion();
|
||||
return {
|
||||
appVersion,
|
||||
runtime: "web",
|
||||
osName: navigator.platform || "web",
|
||||
osVersion: null,
|
||||
arch: "",
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -200,6 +200,14 @@ export interface WebDavSyncSecretsStatus {
|
|||
hasSavedPassphrase: boolean;
|
||||
}
|
||||
|
||||
export interface AppSupportInfo {
|
||||
appVersion: string;
|
||||
runtime: "desktop" | "web";
|
||||
osName: string;
|
||||
osVersion?: string | null;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
export interface QueryPagination {
|
||||
limit: number;
|
||||
offset: number;
|
||||
|
|
@ -1301,6 +1309,10 @@ export async function getAppVersion(): Promise<string> {
|
|||
return getVersion();
|
||||
}
|
||||
|
||||
export async function getAppSupportInfo(): Promise<AppSupportInfo> {
|
||||
return invoke<AppSupportInfo>("get_app_support_info");
|
||||
}
|
||||
|
||||
// --- Redis ---
|
||||
export interface RedisKeyInfo {
|
||||
key_display: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, normalizeSupportInfoVersion } from "../../apps/desktop/src/lib/app/supportInfo.ts";
|
||||
import type { AppSupportInfoLabels } from "../../apps/desktop/src/lib/app/supportInfo.ts";
|
||||
import type { AppSupportInfo } from "../../apps/desktop/src/lib/backend/tauri.ts";
|
||||
|
||||
const labels: AppSupportInfoLabels = {
|
||||
appVersion: "DBX Version",
|
||||
runtime: "Runtime",
|
||||
runtimeDesktop: "Desktop",
|
||||
runtimeWeb: "Web",
|
||||
operatingSystem: "Operating System",
|
||||
architecture: "Architecture",
|
||||
unknown: "Unknown",
|
||||
};
|
||||
|
||||
test("normalizes support info versions with a single v prefix", () => {
|
||||
assert.equal(normalizeSupportInfoVersion("0.5.50", labels.unknown), "v0.5.50");
|
||||
assert.equal(normalizeSupportInfoVersion("v0.5.50", labels.unknown), "v0.5.50");
|
||||
assert.equal(normalizeSupportInfoVersion("V0.5.50", labels.unknown), "v0.5.50");
|
||||
});
|
||||
|
||||
test("support info rows use stable fallback values", () => {
|
||||
const info: AppSupportInfo = {
|
||||
appVersion: "",
|
||||
runtime: "desktop",
|
||||
osName: "",
|
||||
osVersion: null,
|
||||
arch: "",
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
buildAppSupportInfoRows(info, labels).map((row) => row.value),
|
||||
["Unknown", "Desktop", "Unknown", "Unknown"],
|
||||
);
|
||||
});
|
||||
|
||||
test("formats support info clipboard text in stable issue-friendly order", () => {
|
||||
const info: AppSupportInfo = {
|
||||
appVersion: "0.5.50",
|
||||
runtime: "desktop",
|
||||
osName: "macOS",
|
||||
osVersion: "15.5",
|
||||
arch: "aarch64",
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
formatAppSupportInfoForClipboard(info, labels),
|
||||
["DBX Version: v0.5.50", "Runtime: Desktop", "Operating System: macOS 15.5", "Architecture: aarch64"].join("\n"),
|
||||
);
|
||||
});
|
||||
|
|
@ -36,6 +36,7 @@ pub mod schema_diff;
|
|||
pub mod sql_file;
|
||||
pub mod sqlite_backup;
|
||||
pub mod ssh_config;
|
||||
pub mod support_info;
|
||||
pub mod system_fonts;
|
||||
pub mod tab_runtime_cache;
|
||||
pub mod table_export;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppSupportInfo {
|
||||
pub app_version: String,
|
||||
pub runtime: &'static str,
|
||||
pub os_name: String,
|
||||
pub os_version: Option<String>,
|
||||
pub arch: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_support_info() -> AppSupportInfo {
|
||||
current_app_support_info()
|
||||
}
|
||||
|
||||
pub(crate) fn current_app_support_info() -> AppSupportInfo {
|
||||
AppSupportInfo {
|
||||
app_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
runtime: "desktop",
|
||||
os_name: os_name(),
|
||||
os_version: os_version(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_support_info_for_native_about() -> String {
|
||||
let info = current_app_support_info();
|
||||
let operating_system = format_operating_system(&info);
|
||||
|
||||
["Desktop".to_string(), unknown_if_empty(&operating_system), unknown_if_empty(&info.arch)].join(" • ")
|
||||
}
|
||||
|
||||
pub(crate) fn format_support_info_for_clipboard() -> String {
|
||||
let info = current_app_support_info();
|
||||
let operating_system = format_operating_system(&info);
|
||||
|
||||
[
|
||||
format!("DBX Version: {}", normalize_app_version(&info.app_version)),
|
||||
"Runtime: Desktop".to_string(),
|
||||
format!("Operating System: {}", unknown_if_empty(&operating_system)),
|
||||
format!("Architecture: {}", unknown_if_empty(&info.arch)),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn format_operating_system(info: &AppSupportInfo) -> String {
|
||||
let operating_system = [Some(info.os_name.as_str()), info.os_version.as_deref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|part| !part.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
operating_system
|
||||
}
|
||||
|
||||
fn normalize_app_version(version: &str) -> String {
|
||||
let trimmed = version.trim();
|
||||
if trimmed.is_empty() {
|
||||
return "Unknown".to_string();
|
||||
}
|
||||
if trimmed.starts_with('v') || trimmed.starts_with('V') {
|
||||
format!("v{}", &trimmed[1..])
|
||||
} else {
|
||||
format!("v{trimmed}")
|
||||
}
|
||||
}
|
||||
|
||||
fn unknown_if_empty(value: &str) -> String {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn os_name() -> String {
|
||||
platform_product_name().unwrap_or_else(|| std::env::consts::OS.to_string())
|
||||
}
|
||||
|
||||
fn os_version() -> Option<String> {
|
||||
platform_product_version()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn platform_product_name() -> Option<String> {
|
||||
command_first_line("sw_vers", &["-productName"])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn platform_product_version() -> Option<String> {
|
||||
command_first_line("sw_vers", &["-productVersion"])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn platform_product_name() -> Option<String> {
|
||||
Some("Windows".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn platform_product_version() -> Option<String> {
|
||||
command_first_line("cmd", &["/C", "ver"])
|
||||
.map(|line| line.trim_matches(|ch| ch == '\r' || ch == '\n').trim().to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn platform_product_name() -> Option<String> {
|
||||
linux_os_release_value("/etc/os-release", "PRETTY_NAME")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn platform_product_version() -> Option<String> {
|
||||
linux_os_release_value("/etc/os-release", "VERSION_ID")
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
fn platform_product_name() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
fn platform_product_version() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
fn command_first_line(command: &str, args: &[&str]) -> Option<String> {
|
||||
let output = std::process::Command::new(command).args(args).output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
String::from_utf8_lossy(&output.stdout).lines().map(str::trim).find(|line| !line.is_empty()).map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_os_release_value(path: &str, key: &str) -> Option<String> {
|
||||
let content = std::fs::read_to_string(path).ok()?;
|
||||
parse_os_release_value(&content, key)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn parse_os_release_value(content: &str, key: &str) -> Option<String> {
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let Some((name, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if name != key {
|
||||
continue;
|
||||
}
|
||||
return Some(unquote_os_release_value(value.trim()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn unquote_os_release_value(value: &str) -> String {
|
||||
let quoted = value
|
||||
.strip_prefix('"')
|
||||
.and_then(|inner| inner.strip_suffix('"'))
|
||||
.or_else(|| value.strip_prefix('\'').and_then(|inner| inner.strip_suffix('\'')));
|
||||
let Some(inner) = quoted else {
|
||||
return value.to_string();
|
||||
};
|
||||
inner.replace("\\\"", "\"").replace("\\'", "'").replace("\\\\", "\\")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
format_support_info_for_clipboard, format_support_info_for_native_about, normalize_app_version,
|
||||
parse_os_release_value, unknown_if_empty, unquote_os_release_value,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn normalizes_app_version_for_support_info() {
|
||||
assert_eq!(normalize_app_version("0.5.50"), "v0.5.50");
|
||||
assert_eq!(normalize_app_version("v0.5.50"), "v0.5.50");
|
||||
assert_eq!(normalize_app_version("V0.5.50"), "v0.5.50");
|
||||
assert_eq!(normalize_app_version(""), "Unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_unknown_for_empty_support_info_values() {
|
||||
assert_eq!(unknown_if_empty(""), "Unknown");
|
||||
assert_eq!(unknown_if_empty(" aarch64 "), "aarch64");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_native_about_support_info_compactly() {
|
||||
let text = format_support_info_for_native_about();
|
||||
assert!(text.contains("Desktop"));
|
||||
assert!(!text.contains("DBX Version:"));
|
||||
assert!(!text.contains('\n'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_clipboard_support_info_with_labels() {
|
||||
let text = format_support_info_for_clipboard();
|
||||
assert!(text.contains("DBX Version:"));
|
||||
assert!(text.contains("Runtime:"));
|
||||
assert!(text.contains("Operating System:"));
|
||||
assert!(text.contains("Architecture:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_linux_os_release_values() {
|
||||
let content = r#"
|
||||
NAME="Ubuntu"
|
||||
VERSION_ID="24.04"
|
||||
PRETTY_NAME="Ubuntu 24.04.1 LTS"
|
||||
"#;
|
||||
|
||||
assert_eq!(parse_os_release_value(content, "PRETTY_NAME").as_deref(), Some("Ubuntu 24.04.1 LTS"));
|
||||
assert_eq!(parse_os_release_value(content, "VERSION_ID").as_deref(), Some("24.04"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_missing_linux_os_release_value() {
|
||||
assert_eq!(parse_os_release_value("NAME=Ubuntu\n", "PRETTY_NAME"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unquotes_linux_os_release_escapes() {
|
||||
assert_eq!(unquote_os_release_value(r#""A \"quoted\" value""#), "A \"quoted\" value");
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@ use tauri::{
|
|||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
use tauri::{Emitter, Manager};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
|
|
@ -28,6 +30,8 @@ const DESKTOP_TRAY_ID: &str = "main-tray";
|
|||
const APP_CLOSE_REQUESTED_EVENT: &str = "dbx-app-close-requested";
|
||||
#[cfg(target_os = "macos")]
|
||||
const APP_MENU_QUIT_ID: &str = "app-menu-quit";
|
||||
#[cfg(target_os = "macos")]
|
||||
const APP_MENU_COPY_SUPPORT_INFO_ID: &str = "app-menu-copy-support-info";
|
||||
|
||||
pub struct CloseBehaviorState {
|
||||
confirmed_exit: AtomicBool,
|
||||
|
|
@ -48,6 +52,8 @@ impl CloseBehaviorState {
|
|||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
const MACOS_TRAY_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/tray-macos-template.png");
|
||||
#[cfg(target_os = "macos")]
|
||||
const ABOUT_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon.png");
|
||||
const BLACK_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon-black.png");
|
||||
|
||||
pub(crate) fn apply_debug_log_level(debug_logging_enabled: bool) {
|
||||
|
|
@ -80,15 +86,16 @@ fn native_window_decorations_override(target_os: &str) -> Option<bool> {
|
|||
#[cfg(target_os = "macos")]
|
||||
fn build_app_menu<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let pkg_info = app_handle.package_info();
|
||||
let config = app_handle.config();
|
||||
let app_name = pkg_info.name.clone();
|
||||
let about_metadata = AboutMetadata {
|
||||
name: Some(app_name.clone()),
|
||||
version: Some(pkg_info.version.to_string()),
|
||||
copyright: config.bundle.copyright.clone(),
|
||||
authors: config.bundle.publisher.clone().map(|p| vec![p]),
|
||||
copyright: Some(commands::support_info::format_support_info_for_native_about()),
|
||||
icon: Some(ABOUT_APP_ICON),
|
||||
..Default::default()
|
||||
};
|
||||
let copy_support_info_item =
|
||||
MenuItem::with_id(app_handle, APP_MENU_COPY_SUPPORT_INFO_ID, "Copy Support Info", true, None::<&str>)?;
|
||||
let quit_item = MenuItem::with_id(app_handle, APP_MENU_QUIT_ID, format!("Quit {app_name}"), true, Some("Cmd+Q"))?;
|
||||
|
||||
Menu::with_items(
|
||||
|
|
@ -100,6 +107,7 @@ fn build_app_menu<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>) -> tauri:
|
|||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
|
||||
©_support_info_item,
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
&PredefinedMenuItem::services(app_handle, None)?,
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
|
|
@ -626,6 +634,10 @@ pub fn run() {
|
|||
let builder = builder.menu(build_app_menu).on_menu_event(|app, event| {
|
||||
if event.id() == APP_MENU_QUIT_ID {
|
||||
request_app_close(app, "quit");
|
||||
} else if event.id() == APP_MENU_COPY_SUPPORT_INFO_ID {
|
||||
if let Err(err) = app.clipboard().write_text(commands::support_info::format_support_info_for_clipboard()) {
|
||||
log::warn!("Failed to copy support info from app menu: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -774,6 +786,7 @@ pub fn run() {
|
|||
commands::app_settings::load_saved_sql_editor_positions,
|
||||
commands::app_settings::save_saved_sql_editor_positions,
|
||||
commands::app_settings::load_native_debug_logs,
|
||||
commands::support_info::get_app_support_info,
|
||||
commands::cloud_sync::webdav_sync_test,
|
||||
commands::cloud_sync::webdav_password_status,
|
||||
commands::cloud_sync::save_webdav_saved_password,
|
||||
|
|
|
|||
Loading…
Reference in New Issue