mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(settings): embed version changelog in About page
This commit is contained in:
@@ -99,6 +99,7 @@ import { uuid } from "@/lib/common/utils";
|
||||
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
|
||||
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
|
||||
import AppLogo from "@/components/icons/AppLogo.vue";
|
||||
import ChangelogPanel from "@/components/settings/ChangelogPanel.vue";
|
||||
import SqlFormatterSettingsPanel from "./SqlFormatterSettingsPanel.vue";
|
||||
import { APP_THEME_PALETTES, type AppThemeAppearance, type AppThemeMode, type AppThemePalette } from "@/lib/app/appTheme";
|
||||
import { editorSettingsDraftChanged, editorSettingsDraftFromSettings, editorSettingsPatchFromDraft, type EditorSettingsDraft } from "@/lib/settings/editorSettingsDraft";
|
||||
@@ -4573,6 +4574,8 @@ onUnmounted(cleanupPreviewEditor);
|
||||
<p v-if="appSupportInfoError" class="mt-3 text-xs text-destructive">{{ t("settings.supportInfoLoadFailed", { message: appSupportInfoError }) }}</p>
|
||||
</div>
|
||||
|
||||
<ChangelogPanel />
|
||||
|
||||
<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">
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ChevronDown, ExternalLink, Loader2, RefreshCw, Tag } from "@lucide/vue";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { currentLocale } from "@/i18n";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { changelogLangFromLocale, changelogReleaseUrl, changelogWebsiteUrl, createLatestRequestGuard, fetchChangelog, type ChangelogLang, type ChangelogRelease } from "@/lib/app/changelog";
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const releases = ref<ChangelogRelease[]>([]);
|
||||
const visibleCount = ref(PAGE_SIZE);
|
||||
const expandedTags = ref<Set<string>>(new Set());
|
||||
const loadedLang = ref<ChangelogLang | null>(null);
|
||||
const loadRequest = createLatestRequestGuard();
|
||||
|
||||
const changelogLang = computed(() => changelogLangFromLocale(locale.value || currentLocale()));
|
||||
const visibleReleases = computed(() => releases.value.slice(0, visibleCount.value));
|
||||
const hasMore = computed(() => visibleCount.value < releases.value.length);
|
||||
const latestTag = computed(() => releases.value[0]?.tag ?? "");
|
||||
|
||||
function isLatestRelease(tag: string) {
|
||||
return !!latestTag.value && tag === latestTag.value;
|
||||
}
|
||||
|
||||
function sectionLabel(type: string, fallback: string) {
|
||||
const keyByType: Record<string, string> = {
|
||||
added: "settings.changelogSectionAdded",
|
||||
improved: "settings.changelogSectionImproved",
|
||||
fixed: "settings.changelogSectionFixed",
|
||||
changed: "settings.changelogSectionChanged",
|
||||
removed: "settings.changelogSectionRemoved",
|
||||
};
|
||||
const key = keyByType[type];
|
||||
if (!key) return fallback;
|
||||
const translated = t(key);
|
||||
return translated === key ? fallback : translated;
|
||||
}
|
||||
|
||||
function formatReleaseDate(dateStr: string) {
|
||||
if (!dateStr) return "";
|
||||
const date = new Date(dateStr);
|
||||
if (Number.isNaN(date.getTime())) return dateStr;
|
||||
try {
|
||||
return new Intl.DateTimeFormat(locale.value || currentLocale(), {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
function openExternalUrl(url: string) {
|
||||
if (isTauriRuntime()) {
|
||||
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
|
||||
} else {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}
|
||||
|
||||
function isExpanded(tag: string) {
|
||||
return expandedTags.value.has(tag);
|
||||
}
|
||||
|
||||
function toggleRelease(tag: string) {
|
||||
const next = new Set(expandedTags.value);
|
||||
if (next.has(tag)) next.delete(tag);
|
||||
else next.add(tag);
|
||||
expandedTags.value = next;
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
visibleCount.value = Math.min(visibleCount.value + PAGE_SIZE, releases.value.length);
|
||||
}
|
||||
|
||||
async function load(force = false) {
|
||||
const lang = changelogLang.value;
|
||||
const requestId = loadRequest.begin();
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const data = await fetchChangelog(lang, { force });
|
||||
if (!loadRequest.isCurrent(requestId)) return;
|
||||
releases.value = data.releases;
|
||||
loadedLang.value = lang;
|
||||
visibleCount.value = PAGE_SIZE;
|
||||
expandedTags.value = new Set();
|
||||
} catch (e: any) {
|
||||
if (!loadRequest.isCurrent(requestId)) return;
|
||||
releases.value = [];
|
||||
loadedLang.value = null;
|
||||
error.value = e?.message || String(e);
|
||||
} finally {
|
||||
if (loadRequest.isCurrent(requestId)) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
|
||||
watch(changelogLang, (lang) => {
|
||||
if (lang !== loadedLang.value) {
|
||||
void load();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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.changelogTitle") }}</Label>
|
||||
<p class="text-sm text-muted-foreground">{{ t("settings.changelogDescription") }}</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" :disabled="loading" @click="openExternalUrl(changelogWebsiteUrl(changelogLang))">
|
||||
<ExternalLink class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t("settings.changelogOpenWebsite") }}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" :disabled="loading" :title="t('settings.changelogRetry')" :aria-label="t('settings.changelogRetry')" @click="load(true)">
|
||||
<Loader2 v-if="loading" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCw v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !releases.length" class="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("settings.changelogLoading") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="error && !releases.length" class="mt-4 space-y-3">
|
||||
<p class="text-sm text-destructive">{{ t("settings.changelogLoadFailed") }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="outline" size="sm" @click="load(true)">{{ t("settings.changelogRetry") }}</Button>
|
||||
<Button type="button" variant="outline" size="sm" @click="openExternalUrl(changelogWebsiteUrl(changelogLang))">
|
||||
<ExternalLink class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t("settings.changelogOpenWebsite") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-else-if="!releases.length" class="mt-4 text-sm text-muted-foreground">{{ t("settings.changelogEmpty") }}</p>
|
||||
|
||||
<div v-else class="mt-4 space-y-2">
|
||||
<div v-for="release in visibleReleases" :key="release.tag" class="relative overflow-hidden rounded-md border bg-muted/20" :class="isLatestRelease(release.tag) ? 'border-primary/35' : ''">
|
||||
<button type="button" class="flex w-full items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" :aria-expanded="isExpanded(release.tag)" @click="toggleRelease(release.tag)">
|
||||
<Badge variant="secondary" class="shrink-0 gap-1 font-mono text-[11px]">
|
||||
<Tag class="h-3 w-3" />
|
||||
{{ release.tag.replace(/^v/, "") }}
|
||||
</Badge>
|
||||
<span v-if="isLatestRelease(release.tag)" class="shrink-0 rounded-sm bg-primary px-1.5 py-0.5 text-[10px] font-semibold leading-none tracking-wide text-primary-foreground">
|
||||
{{ t("settings.changelogNew") }}
|
||||
</span>
|
||||
<span class="min-w-0 flex-1 truncate text-xs text-muted-foreground">
|
||||
{{ t("settings.changelogPublishedOn", { date: formatReleaseDate(release.date) }) }}
|
||||
</span>
|
||||
<ChevronDown class="h-4 w-4 shrink-0 text-muted-foreground transition-transform" :class="isExpanded(release.tag) ? 'rotate-180' : ''" />
|
||||
</button>
|
||||
|
||||
<div v-if="isExpanded(release.tag)" class="border-t border-border/60 px-3 py-3">
|
||||
<template v-if="release.sections.length">
|
||||
<div v-for="(section, sectionIndex) in release.sections" :key="`${release.tag}-${section.type}-${sectionIndex}`" :class="sectionIndex > 0 ? 'mt-3' : ''">
|
||||
<div class="mb-1.5 text-xs font-semibold text-foreground">{{ sectionLabel(section.type, section.title) }}</div>
|
||||
<ul class="space-y-1.5">
|
||||
<li v-for="(item, itemIndex) in section.items" :key="itemIndex" class="flex gap-2 text-xs leading-relaxed text-muted-foreground">
|
||||
<span class="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-muted-foreground/60" />
|
||||
<span>
|
||||
<template v-if="item.desc">{{ item.title }} — {{ item.desc }}</template>
|
||||
<template v-else>{{ item.title }}</template>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="text-xs italic text-muted-foreground">{{ t("settings.changelogNoDetails") }}</p>
|
||||
|
||||
<Button type="button" variant="ghost" size="sm" class="mt-3 h-7 px-2 text-xs" @click.stop="openExternalUrl(changelogReleaseUrl(release.tag))">
|
||||
<ExternalLink class="mr-1 h-3 w-3" />
|
||||
{{ t("settings.changelogOpenGitHub") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasMore" class="pt-1">
|
||||
<Button type="button" variant="outline" size="sm" class="w-full" @click="loadMore">
|
||||
{{ t("settings.changelogLoadMore") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3632,6 +3632,23 @@ export default {
|
||||
project: "Project",
|
||||
openSource: "Open-source repository",
|
||||
officialDocs: "Official docs",
|
||||
changelogTitle: "Changelog",
|
||||
changelogDescription: "Browse what's new in each release.",
|
||||
changelogLoading: "Loading changelog...",
|
||||
changelogLoadFailed: "Failed to load changelog.",
|
||||
changelogEmpty: "No changelog entries yet.",
|
||||
changelogPublishedOn: "Published on {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "Load more",
|
||||
changelogOpenWebsite: "View on website",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "See GitHub Release for details.",
|
||||
changelogRetry: "Retry",
|
||||
changelogSectionAdded: "New Features",
|
||||
changelogSectionImproved: "Improvements",
|
||||
changelogSectionFixed: "Bug Fixes",
|
||||
changelogSectionChanged: "Changes",
|
||||
changelogSectionRemoved: "Removed",
|
||||
shortcutUppercaseSelection: "Convert selection to uppercase",
|
||||
shortcutLowercaseSelection: "Convert selection to lowercase",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: paste as IN condition",
|
||||
|
||||
@@ -3422,6 +3422,23 @@ export default withEnglishFallback({
|
||||
project: "Proyecto",
|
||||
openSource: "Repositorio de código abierto",
|
||||
officialDocs: "Documentación oficial",
|
||||
changelogTitle: "Registro de cambios",
|
||||
changelogDescription: "Consulta las novedades de cada versión.",
|
||||
changelogLoading: "Cargando el registro de cambios...",
|
||||
changelogLoadFailed: "No se pudo cargar el registro de cambios.",
|
||||
changelogEmpty: "Aún no hay entradas en el registro de cambios.",
|
||||
changelogPublishedOn: "Publicado el {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "Cargar más",
|
||||
changelogOpenWebsite: "Ver en el sitio web",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "Consulta GitHub Release para más detalles.",
|
||||
changelogRetry: "Reintentar",
|
||||
changelogSectionAdded: "Novedades",
|
||||
changelogSectionImproved: "Mejoras",
|
||||
changelogSectionFixed: "Correcciones",
|
||||
changelogSectionChanged: "Cambios",
|
||||
changelogSectionRemoved: "Eliminado",
|
||||
shortcutUppercaseSelection: "Convertir selección a mayúsculas",
|
||||
shortcutLowercaseSelection: "Convertir selección a minúsculas",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: pegar como condición IN",
|
||||
|
||||
@@ -3420,6 +3420,23 @@ export default withEnglishFallback({
|
||||
project: "Progetto",
|
||||
openSource: "Repository open-source",
|
||||
officialDocs: "Documenti ufficiali",
|
||||
changelogTitle: "Registro modifiche",
|
||||
changelogDescription: "Consulta le novità di ogni versione.",
|
||||
changelogLoading: "Caricamento del registro modifiche...",
|
||||
changelogLoadFailed: "Impossibile caricare il registro modifiche.",
|
||||
changelogEmpty: "Nessuna voce nel registro modifiche.",
|
||||
changelogPublishedOn: "Pubblicato il {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "Carica altro",
|
||||
changelogOpenWebsite: "Apri sul sito",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "Vedi GitHub Release per i dettagli.",
|
||||
changelogRetry: "Riprova",
|
||||
changelogSectionAdded: "Nuove funzionalità",
|
||||
changelogSectionImproved: "Miglioramenti",
|
||||
changelogSectionFixed: "Correzioni",
|
||||
changelogSectionChanged: "Modifiche",
|
||||
changelogSectionRemoved: "Rimossi",
|
||||
shortcutUppercaseSelection: "Converti selezione in maiuscolo",
|
||||
shortcutLowercaseSelection: "Converti selezione in minuscolo",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: incolla come condizione IN",
|
||||
|
||||
@@ -3421,6 +3421,23 @@ export default withEnglishFallback({
|
||||
project: "プロジェクト",
|
||||
openSource: "オープンソースリポジトリ",
|
||||
officialDocs: "公式ドキュメント",
|
||||
changelogTitle: "更新履歴",
|
||||
changelogDescription: "各バージョンの更新内容を確認できます。",
|
||||
changelogLoading: "更新履歴を読み込み中...",
|
||||
changelogLoadFailed: "更新履歴の読み込みに失敗しました。",
|
||||
changelogEmpty: "更新履歴はまだありません。",
|
||||
changelogPublishedOn: "公開日 {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "さらに読み込む",
|
||||
changelogOpenWebsite: "公式サイトで見る",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "詳細は GitHub Release を参照してください。",
|
||||
changelogRetry: "再試行",
|
||||
changelogSectionAdded: "新機能",
|
||||
changelogSectionImproved: "改善",
|
||||
changelogSectionFixed: "不具合修正",
|
||||
changelogSectionChanged: "変更",
|
||||
changelogSectionRemoved: "削除",
|
||||
shortcutUppercaseSelection: "選択範囲を大文字に変換",
|
||||
shortcutLowercaseSelection: "選択範囲を小文字に変換",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: IN条件として貼り付け",
|
||||
|
||||
@@ -3422,6 +3422,23 @@ export default withEnglishFallback({
|
||||
project: "Projeto",
|
||||
openSource: "Repositório de código aberto",
|
||||
officialDocs: "Documentação oficial",
|
||||
changelogTitle: "Registro de alterações",
|
||||
changelogDescription: "Veja o que há de novo em cada versão.",
|
||||
changelogLoading: "Carregando o registro de alterações...",
|
||||
changelogLoadFailed: "Falha ao carregar o registro de alterações.",
|
||||
changelogEmpty: "Ainda não há entradas no registro de alterações.",
|
||||
changelogPublishedOn: "Publicado em {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "Carregar mais",
|
||||
changelogOpenWebsite: "Ver no site",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "Veja o GitHub Release para detalhes.",
|
||||
changelogRetry: "Tentar novamente",
|
||||
changelogSectionAdded: "Novidades",
|
||||
changelogSectionImproved: "Melhorias",
|
||||
changelogSectionFixed: "Correções",
|
||||
changelogSectionChanged: "Alterações",
|
||||
changelogSectionRemoved: "Removido",
|
||||
shortcutUppercaseSelection: "Converter seleção em maiúsculas",
|
||||
shortcutLowercaseSelection: "Converter seleção em minúsculas",
|
||||
shortcutExPasteSqlInCondition: "ExPaste: colar como condição IN",
|
||||
|
||||
@@ -3634,6 +3634,23 @@ export default withEnglishFallback({
|
||||
project: "项目",
|
||||
openSource: "开源仓库",
|
||||
officialDocs: "官方文档",
|
||||
changelogTitle: "更新日志",
|
||||
changelogDescription: "查看各版本的更新内容。",
|
||||
changelogLoading: "正在加载更新日志...",
|
||||
changelogLoadFailed: "加载更新日志失败。",
|
||||
changelogEmpty: "暂无更新日志。",
|
||||
changelogPublishedOn: "发布于 {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "加载更多",
|
||||
changelogOpenWebsite: "在官网查看",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "详见 GitHub Release。",
|
||||
changelogRetry: "重试",
|
||||
changelogSectionAdded: "新功能",
|
||||
changelogSectionImproved: "改进",
|
||||
changelogSectionFixed: "问题修复",
|
||||
changelogSectionChanged: "变更",
|
||||
changelogSectionRemoved: "移除",
|
||||
},
|
||||
driverStore: {
|
||||
progressJreExtract: "解压 JRE...",
|
||||
|
||||
@@ -3221,6 +3221,23 @@ export default withEnglishFallback({
|
||||
project: "專案",
|
||||
openSource: "開源倉庫",
|
||||
officialDocs: "官方文件",
|
||||
changelogTitle: "更新日誌",
|
||||
changelogDescription: "查看各版本的更新內容。",
|
||||
changelogLoading: "正在載入更新日誌...",
|
||||
changelogLoadFailed: "載入更新日誌失敗。",
|
||||
changelogEmpty: "暫無更新日誌。",
|
||||
changelogPublishedOn: "發佈於 {date}",
|
||||
changelogNew: "NEW",
|
||||
changelogLoadMore: "載入更多",
|
||||
changelogOpenWebsite: "在官網查看",
|
||||
changelogOpenGitHub: "GitHub Release",
|
||||
changelogNoDetails: "詳見 GitHub Release。",
|
||||
changelogRetry: "重試",
|
||||
changelogSectionAdded: "新功能",
|
||||
changelogSectionImproved: "改進",
|
||||
changelogSectionFixed: "問題修復",
|
||||
changelogSectionChanged: "變更",
|
||||
changelogSectionRemoved: "移除",
|
||||
mcpTab: "MCP",
|
||||
mcpTitle: "MCP Server",
|
||||
mcpDescription: "檢查 Claude Code、Cursor 等程式助理使用的 DBX MCP Server 安裝與版本狀態。",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export type ChangelogItem = {
|
||||
title: string;
|
||||
desc: string;
|
||||
};
|
||||
|
||||
export type ChangelogSection = {
|
||||
type: string;
|
||||
title: string;
|
||||
items: ChangelogItem[];
|
||||
};
|
||||
|
||||
export type ChangelogRelease = {
|
||||
tag: string;
|
||||
name: string;
|
||||
date: string;
|
||||
sections: ChangelogSection[];
|
||||
};
|
||||
|
||||
export type ChangelogData = {
|
||||
updatedAt: string;
|
||||
releases: ChangelogRelease[];
|
||||
};
|
||||
|
||||
export type ChangelogLang = "en" | "cn";
|
||||
|
||||
const cache = new Map<ChangelogLang, Promise<ChangelogData>>();
|
||||
|
||||
export function createLatestRequestGuard() {
|
||||
let latestRequestId = 0;
|
||||
return {
|
||||
begin: () => ++latestRequestId,
|
||||
isCurrent: (requestId: number) => requestId === latestRequestId,
|
||||
};
|
||||
}
|
||||
|
||||
export function changelogLangFromLocale(locale: string): ChangelogLang {
|
||||
return locale === "zh-CN" || locale === "zh-TW" ? "cn" : "en";
|
||||
}
|
||||
|
||||
export function changelogWebsiteUrl(lang: ChangelogLang): string {
|
||||
return `https://dbxio.com/${lang}/changelog`;
|
||||
}
|
||||
|
||||
export function changelogReleaseUrl(tag: string): string {
|
||||
return `https://github.com/t8y2/dbx/releases/tag/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
|
||||
export async function fetchChangelog(lang: ChangelogLang, options: { force?: boolean } = {}): Promise<ChangelogData> {
|
||||
if (options.force) {
|
||||
cache.delete(lang);
|
||||
}
|
||||
|
||||
let pending = cache.get(lang);
|
||||
if (!pending) {
|
||||
pending = loadChangelog(lang).catch((error) => {
|
||||
cache.delete(lang);
|
||||
throw error;
|
||||
});
|
||||
cache.set(lang, pending);
|
||||
}
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
async function loadChangelog(lang: ChangelogLang): Promise<ChangelogData> {
|
||||
const { fetchChangelog: fetchChangelogViaBackend } = await import("@/lib/backend/api");
|
||||
const data = await fetchChangelogViaBackend(lang);
|
||||
if (!data || !Array.isArray(data.releases)) {
|
||||
throw new Error("Invalid changelog payload");
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "",
|
||||
releases: data.releases.filter((release) => release && typeof release.tag === "string"),
|
||||
};
|
||||
}
|
||||
@@ -466,6 +466,7 @@ export const deleteHistoryEntry = forward("deleteHistoryEntry");
|
||||
export const checkMcpServerStatus = forward("checkMcpServerStatus");
|
||||
export const installMcpServer = forward("installMcpServer");
|
||||
export const checkForUpdates = forward("checkForUpdates");
|
||||
export const fetchChangelog = forward("fetchChangelog");
|
||||
export const getSystemProxyUrl = forward("getSystemProxyUrl");
|
||||
export const downloadAndInstallUpdate = forward("downloadAndInstallUpdate");
|
||||
export const getAppVersion = forward("getAppVersion");
|
||||
|
||||
@@ -2166,6 +2166,11 @@ export async function checkForUpdates(locale?: string): Promise<UpdateInfo> {
|
||||
return get(`/api/update/check${query}`);
|
||||
}
|
||||
|
||||
export async function fetchChangelog(lang?: string): Promise<import("@/lib/app/changelog").ChangelogData> {
|
||||
const query = lang ? `?lang=${encodeURIComponent(lang)}` : "";
|
||||
return get(`/api/changelog${query}`);
|
||||
}
|
||||
|
||||
export async function checkMcpServerStatus(): Promise<import("@/lib/backend/tauri").McpServerStatus> {
|
||||
return {
|
||||
installed: false,
|
||||
|
||||
@@ -1392,6 +1392,10 @@ export async function checkForUpdates(locale?: string): Promise<UpdateInfo> {
|
||||
return invoke("check_for_updates", { locale });
|
||||
}
|
||||
|
||||
export async function fetchChangelog(lang?: string): Promise<import("@/lib/app/changelog").ChangelogData> {
|
||||
return invoke("fetch_changelog", { lang });
|
||||
}
|
||||
|
||||
export async function getSystemProxyUrl(): Promise<string | null> {
|
||||
return invoke("get_system_proxy_url");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const CHANGELOG_R2_PREFIX: &str = "changelog/releases-";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChangelogItem {
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChangelogSection {
|
||||
#[serde(default)]
|
||||
pub r#type: String,
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub items: Vec<ChangelogItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChangelogRelease {
|
||||
pub tag: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub date: String,
|
||||
#[serde(default)]
|
||||
pub sections: Vec<ChangelogSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChangelogData {
|
||||
#[serde(default)]
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub releases: Vec<ChangelogRelease>,
|
||||
}
|
||||
|
||||
/// Normalize UI locale / short lang into `cn` or `en` (matches R2 file naming).
|
||||
pub fn normalize_changelog_lang(lang: &str) -> &'static str {
|
||||
let trimmed = lang.trim();
|
||||
if trimmed.eq_ignore_ascii_case("cn")
|
||||
|| trimmed.eq_ignore_ascii_case("zh")
|
||||
|| trimmed.eq_ignore_ascii_case("zh-CN")
|
||||
|| trimmed.eq_ignore_ascii_case("zh-TW")
|
||||
{
|
||||
"cn"
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_changelog(lang: &str) -> Result<ChangelogData, String> {
|
||||
let lang = normalize_changelog_lang(lang);
|
||||
let client = build_changelog_http_client()?;
|
||||
let url = format!("{}{CHANGELOG_R2_PREFIX}{lang}.json", crate::R2_CDN_BASE);
|
||||
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.header(reqwest::header::USER_AGENT, "dbx-changelog")
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.map_err(|e| format!("Failed to fetch changelog: {e}"))?;
|
||||
|
||||
let mut data: ChangelogData = resp.json().await.map_err(|e| format!("Failed to parse changelog: {e}"))?;
|
||||
data.releases.retain(|release| !release.tag.trim().is_empty());
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn build_changelog_http_client() -> Result<reqwest::Client, String> {
|
||||
let mut builder =
|
||||
reqwest::Client::builder().timeout(std::time::Duration::from_secs(15)).user_agent("dbx-changelog");
|
||||
|
||||
if let Some(proxy_url) = crate::update::system_proxy_url() {
|
||||
let proxy = reqwest::Proxy::all(&proxy_url).map_err(|e| format!("Invalid system proxy URL: {e}"))?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
builder.build().map_err(|e| format!("Failed to create HTTP client: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_changelog_lang;
|
||||
|
||||
#[test]
|
||||
fn normalizes_changelog_lang() {
|
||||
assert_eq!(normalize_changelog_lang("cn"), "cn");
|
||||
assert_eq!(normalize_changelog_lang("zh-CN"), "cn");
|
||||
assert_eq!(normalize_changelog_lang("zh-TW"), "cn");
|
||||
assert_eq!(normalize_changelog_lang("en"), "en");
|
||||
assert_eq!(normalize_changelog_lang("ja"), "en");
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod agent_tools;
|
||||
pub mod ai;
|
||||
pub mod ai_cli_agent;
|
||||
pub mod ai_codex_cli;
|
||||
pub mod changelog;
|
||||
pub mod cloud_sync;
|
||||
pub mod connection;
|
||||
pub mod connection_secrets;
|
||||
|
||||
@@ -556,6 +556,7 @@ async fn main() {
|
||||
// Update
|
||||
.route("/version", get(routes::update::get_version))
|
||||
.route("/update/check", get(routes::update::check_for_updates))
|
||||
.route("/changelog", get(routes::update::fetch_changelog))
|
||||
// Layout
|
||||
.route("/layout/sidebar", post(routes::layout::save_sidebar_layout).get(routes::layout::load_sidebar_layout))
|
||||
// App settings
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::{extract::Query, Json};
|
||||
use dbx_core::update;
|
||||
use dbx_core::{changelog, update};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
@@ -19,3 +19,17 @@ pub async fn check_for_updates(Query(params): Query<UpdateCheckParams>) -> Resul
|
||||
let info = update::build_update_info(release, env!("CARGO_PKG_VERSION"));
|
||||
Ok(Json(serde_json::to_value(info).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ChangelogParams {
|
||||
#[serde(default)]
|
||||
pub lang: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn fetch_changelog(
|
||||
Query(params): Query<ChangelogParams>,
|
||||
) -> Result<Json<changelog::ChangelogData>, AppError> {
|
||||
let lang = params.lang.unwrap_or_else(|| "en".to_string());
|
||||
let data = changelog::fetch_changelog(&lang).await.map_err(AppError)?;
|
||||
Ok(Json(data))
|
||||
}
|
||||
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { createLatestRequestGuard } from "../../apps/desktop/src/lib/app/changelog";
|
||||
|
||||
describe("latest changelog request guard", () => {
|
||||
test("invalidates older requests when a new load starts", () => {
|
||||
const guard = createLatestRequestGuard();
|
||||
const englishRequest = guard.begin();
|
||||
const chineseRequest = guard.begin();
|
||||
|
||||
expect(guard.isCurrent(englishRequest)).toBe(false);
|
||||
expect(guard.isCurrent(chineseRequest)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -118,6 +118,12 @@ pub async fn check_for_updates(locale: Option<String>) -> Result<UpdateInfo, Str
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_changelog(lang: Option<String>) -> Result<dbx_core::changelog::ChangelogData, String> {
|
||||
let lang = lang.unwrap_or_else(|| "en".to_string());
|
||||
dbx_core::changelog::fetch_changelog(&lang).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_proxy_url() -> Option<String> {
|
||||
tauri::async_runtime::spawn_blocking(dbx_core::update::system_proxy_url).await.ok().flatten()
|
||||
|
||||
@@ -1235,6 +1235,7 @@ pub fn run() {
|
||||
commands::mcp::check_mcp_server_status,
|
||||
commands::mcp::install_mcp_server,
|
||||
commands::update::check_for_updates,
|
||||
commands::update::fetch_changelog,
|
||||
commands::update::get_system_proxy_url,
|
||||
commands::update::download_and_install_update,
|
||||
commands::transfer::start_transfer,
|
||||
|
||||
Reference in New Issue
Block a user