feat(settings): embed version changelog in About page

This commit is contained in:
Freedom
2026-07-14 12:54:07 +08:00
committed by GitHub
parent e9e59113a3
commit aff9c8515d
20 changed files with 546 additions and 1 deletions
@@ -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>
+17
View File
@@ -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",
+17
View File
@@ -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",
+17
View File
@@ -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",
+17
View File
@@ -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条件として貼り付け",
+17
View File
@@ -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",
+17
View File
@@ -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...",
+17
View File
@@ -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 安裝與版本狀態。",
+76
View File
@@ -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"),
};
}
+1
View File
@@ -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");
+5
View File
@@ -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,
+4
View File
@@ -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");
}
+99
View File
@@ -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");
}
}
+1
View File
@@ -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;
+1
View File
@@ -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
+15 -1
View File
@@ -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))
}
+13
View File
@@ -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);
});
});
+6
View File
@@ -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()
+1
View File
@@ -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,