diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index f8ecf6c962..a4f35ce99b 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -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);

{{ t("settings.supportInfoLoadFailed", { message: appSupportInfoError }) }}

+ +
diff --git a/apps/desktop/src/components/settings/ChangelogPanel.vue b/apps/desktop/src/components/settings/ChangelogPanel.vue new file mode 100644 index 0000000000..ec9ae9c8e4 --- /dev/null +++ b/apps/desktop/src/components/settings/ChangelogPanel.vue @@ -0,0 +1,202 @@ + + + diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 31debdf7cd..56d2c8d2e0 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index b56f88b196..58a9c20d3f 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index f59ddc2183..05209b8082 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 2ee097296a..4ee32ee6eb 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -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条件として貼り付け", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 3d90e77b27..9a23ba2373 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index a02b10f860..c09067a5d3 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -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...", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 805069df16..a61c69298c 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -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 安裝與版本狀態。", diff --git a/apps/desktop/src/lib/app/changelog.ts b/apps/desktop/src/lib/app/changelog.ts new file mode 100644 index 0000000000..51ace0eaff --- /dev/null +++ b/apps/desktop/src/lib/app/changelog.ts @@ -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>(); + +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 { + 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 { + 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"), + }; +} diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index a33b09a0ba..e3f3dfeb81 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -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"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 8a09cac17a..a0a3216b21 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -2166,6 +2166,11 @@ export async function checkForUpdates(locale?: string): Promise { return get(`/api/update/check${query}`); } +export async function fetchChangelog(lang?: string): Promise { + const query = lang ? `?lang=${encodeURIComponent(lang)}` : ""; + return get(`/api/changelog${query}`); +} + export async function checkMcpServerStatus(): Promise { return { installed: false, diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 79a5e1be52..57297e2bf4 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -1392,6 +1392,10 @@ export async function checkForUpdates(locale?: string): Promise { return invoke("check_for_updates", { locale }); } +export async function fetchChangelog(lang?: string): Promise { + return invoke("fetch_changelog", { lang }); +} + export async function getSystemProxyUrl(): Promise { return invoke("get_system_proxy_url"); } diff --git a/crates/dbx-core/src/changelog.rs b/crates/dbx-core/src/changelog.rs new file mode 100644 index 0000000000..6fc108367a --- /dev/null +++ b/crates/dbx-core/src/changelog.rs @@ -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, +} + +#[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, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangelogData { + #[serde(default)] + pub updated_at: String, + #[serde(default)] + pub releases: Vec, +} + +/// 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 { + 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 { + 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"); + } +} diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs index 407143b89a..f153fe0575 100644 --- a/crates/dbx-core/src/lib.rs +++ b/crates/dbx-core/src/lib.rs @@ -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; diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index 28a615d099..502f004951 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -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 diff --git a/crates/dbx-web/src/routes/update.rs b/crates/dbx-web/src/routes/update.rs index e3ca0bd4ee..1a3f261201 100644 --- a/crates/dbx-web/src/routes/update.rs +++ b/crates/dbx-web/src/routes/update.rs @@ -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) -> 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, +} + +pub async fn fetch_changelog( + Query(params): Query, +) -> Result, AppError> { + let lang = params.lang.unwrap_or_else(|| "en".to_string()); + let data = changelog::fetch_changelog(&lang).await.map_err(AppError)?; + Ok(Json(data)) +} diff --git a/packages/app-tests/changelog.test.ts b/packages/app-tests/changelog.test.ts new file mode 100644 index 0000000000..00a9431cd2 --- /dev/null +++ b/packages/app-tests/changelog.test.ts @@ -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); + }); +}); diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs index 222e8b444b..223394daa0 100644 --- a/src-tauri/src/commands/update.rs +++ b/src-tauri/src/commands/update.rs @@ -118,6 +118,12 @@ pub async fn check_for_updates(locale: Option) -> Result) -> Result { + 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 { tauri::async_runtime::spawn_blocking(dbx_core::update::system_proxy_url).await.ok().flatten() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6af956c05e..542af0a21d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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,