Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions static/css/daw.css
Original file line number Diff line number Diff line change
Expand Up @@ -3728,6 +3728,7 @@ input, textarea { font-family: inherit; }
}
.release-inapp-progress-text { margin: 0; font-size: 11px; color: var(--muted); }
.release-inapp-error { margin: 0; font-size: 12px; color: #e54e4e; }
.release-full-download { margin: 0 0 12px; font-size: 13px; line-height: 1.45; text-align: left; color: var(--fg); }

/* The release dialog lives outside `.daw`, so the scoped `.daw .hidden` rule
does not reach its inner elements and base.css's global `.hidden` is not
Expand All @@ -3738,6 +3739,7 @@ input, textarea { font-family: inherit; }
.release-inapp.hidden,
.release-inapp-progress.hidden,
.release-inapp-error.hidden,
.release-full-download.hidden,
.release-card .about-link.hidden {
display: none !important;
}
Expand Down
1 change: 1 addition & 0 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,7 @@ <h2 id="releaseTitle" data-i18n="release.title">New release available</h2>
</div>
<p class="release-inapp-error hidden" id="releaseInappError"></p>
</div>
<p class="release-full-download hidden" id="releaseFullDownload" data-i18n="release.fullDownload">This version is a full download rather than an update: press Download, unzip it, and run it. Your library stays as it is.</p>
<div class="about-primary-links">
<a class="about-link about-link-primary" id="releaseDownload" href="#" target="_blank" rel="noopener noreferrer" data-i18n="release.download">Download</a>
<button class="about-link about-link-accent hidden" id="releaseDownloadApp" type="button" data-i18n="release.updateNow">Update now</button>
Expand Down
10 changes: 10 additions & 0 deletions static/js/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2942,6 +2942,12 @@ function updaterAssetNames(target) {
// objects.githubusercontent.com hosts that serve release *assets*. Fetching
// them from JS is blocked outright; Rust's HTTP client is not bound by the page
// CSP, so the policy stays as tight as it is today.
// check_app_update's reason when the release needs Python packages this
// install's runtime does not have (main.rs). The download is then a full
// package to unzip and run rather than an update, and the dialog says so
// (#720): without it, a missing "Update now" read as a broken update.
const RUNTIME_CHANGED = /^python dependencies changed\b/;

async function resolveInAppUpdatePlan(release, target) {
const names = updaterAssetNames(target);
if (!names) return null;
Expand All @@ -2958,6 +2964,8 @@ async function resolveInAppUpdatePlan(release, target) {
});
if (!check?.supported) {
console.info("[catalog] in-app update unavailable:", check?.reason || "unknown");
document.getElementById("releaseFullDownload")
?.classList.toggle("hidden", !RUNTIME_CHANGED.test(String(check?.reason || "")));
return null;
}

Expand Down Expand Up @@ -3053,6 +3061,8 @@ async function openReleaseDialog() {
const dockerCmd = document.getElementById("releaseDockerCmd");

if (version) version.textContent = `v${normalizeVersion(latestRelease.tag_name)}`;
// Shown again only when this release is found to need a full download.
document.getElementById("releaseFullDownload")?.classList.add("hidden");
if (notes) {
const body = (latestRelease.body || "").trim();
notes.innerHTML = body
Expand Down
11 changes: 11 additions & 0 deletions static/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ const en = {
"release.restartUpdate": "Restart to update",
"release.applying": "Applying update…",
"release.updateFailed": "Update failed",
"release.fullDownload": "This version is a full download rather than an update: press Download, unzip it, and run it. Your library stays as it is.",

"failure.title": "Something failed",
"failure.closeAria": "Close failure dialog",
Expand Down Expand Up @@ -1464,6 +1465,7 @@ const pl = {
"release.restartUpdate": "Uruchom ponownie, aby zaktualizować",
"release.applying": "Instalowanie aktualizacji…",
"release.updateFailed": "Aktualizacja nie powiodła się",
"release.fullDownload": "Ta wersja to pełne pobranie, a nie aktualizacja: kliknij Pobierz, rozpakuj plik i uruchom go. Twoja biblioteka zostaje bez zmian.",

"failure.title": "Coś poszło nie tak",
"failure.closeAria": "Zamknij okno błędu",
Expand Down Expand Up @@ -2250,6 +2252,7 @@ const ja = {
"release.restartUpdate": "再起動して更新",
"release.applying": "アップデートを適用中…",
"release.updateFailed": "アップデートに失敗しました",
"release.fullDownload": "このバージョンはアップデートではなく、フルダウンロードです。「ダウンロード」を押して展開し、実行してください。ライブラリはそのまま残ります。",

"failure.title": "エラーが発生しました",
"failure.closeAria": "エラーダイアログを閉じる",
Expand Down Expand Up @@ -3012,6 +3015,7 @@ const zhHans = {
"release.restartUpdate": "重启以更新",
"release.applying": "正在应用更新…",
"release.updateFailed": "更新失败",
"release.fullDownload": "此版本需要完整下载,而不是更新:点击“下载”,解压后运行即可。你的曲库保持不变。",

"failure.title": "出现故障",
"failure.closeAria": "关闭故障对话框",
Expand Down Expand Up @@ -3775,6 +3779,7 @@ const de = {
"release.restartUpdate": "Zum Aktualisieren neu starten",
"release.applying": "Update wird angewendet…",
"release.updateFailed": "Update fehlgeschlagen",
"release.fullDownload": "Diese Version ist ein vollständiger Download statt eines Updates: Klicke auf Herunterladen, entpacke die Datei und starte StemDeck. Deine Bibliothek bleibt, wie sie ist.",

"failure.title": "Etwas ist fehlgeschlagen",
"failure.closeAria": "Fehlerdialog schließen",
Expand Down Expand Up @@ -4548,6 +4553,7 @@ const pt = {
"release.restartUpdate": "Reiniciar para atualizar",
"release.applying": "Aplicando atualização…",
"release.updateFailed": "Falha na atualização",
"release.fullDownload": "Esta versão é um download completo, não uma atualização: clique em Baixar, descompacte e execute. Sua biblioteca continua como está.",

"failure.title": "Algo falhou",
"failure.closeAria": "Fechar diálogo de falha",
Expand Down Expand Up @@ -5322,6 +5328,7 @@ const id = {
"release.restartUpdate": "Mulai ulang untuk memperbarui",
"release.applying": "Menerapkan pembaruan…",
"release.updateFailed": "Pembaruan gagal",
"release.fullDownload": "Versi ini berupa unduhan lengkap, bukan pembaruan: tekan Unduh, ekstrak, lalu jalankan. Pustaka Anda tetap seperti semula.",

"failure.title": "Terjadi kesalahan",
"failure.closeAria": "Tutup dialog kesalahan",
Expand Down Expand Up @@ -6085,6 +6092,7 @@ const fr = {
"release.restartUpdate": "Redémarrer pour mettre à jour",
"release.applying": "Application de la mise à jour…",
"release.updateFailed": "Échec de la mise à jour",
"release.fullDownload": "Cette version est un téléchargement complet plutôt qu'une mise à jour : cliquez sur Télécharger, décompressez-la et lancez-la. Votre bibliothèque reste telle quelle.",
"release.viewDownload": "Voir le téléchargement",

"failure.title": "Un problème est survenu",
Expand Down Expand Up @@ -6543,6 +6551,7 @@ const ptPT = {
"sections.clearAria": "Limpar todas as secções",
"aria.download": "Transferir {name}",
"release.download": "Transferir",
"release.fullDownload": "Esta versão é uma transferência completa, não uma atualização: clique em Transferir, descomprima e execute. A sua biblioteca fica como está.",
"release.downloading": "A transferir atualização…",
"release.pullImage": "Transfira a nova imagem e reinicie o contentor:",
"settings.exportLogs.desc": "Transfira todos os ficheiros de log num único zip, o que anexar a um relatório de erro. Veja o separador Logs para saber onde ficam.",
Expand Down Expand Up @@ -7029,6 +7038,7 @@ const es = {
"release.restartUpdate": "Reinicia para actualizar",
"release.applying": "Aplicando la actualización…",
"release.updateFailed": "La actualización falló",
"release.fullDownload": "Esta versión es una descarga completa, no una actualización: pulsa Descargar, descomprímela y ejecútala. Tu biblioteca se queda como está.",

"failure.title": "Algo falló",
"failure.closeAria": "Cerrar el diálogo de error",
Expand Down Expand Up @@ -7824,6 +7834,7 @@ const ko = {
"release.restartUpdate": "다시 시작해서 업데이트",
"release.applying": "업데이트 적용 중…",
"release.updateFailed": "업데이트에 실패했어요",
"release.fullDownload": "이번 버전은 업데이트가 아니라 전체 다운로드예요. 다운로드를 누르고 압축을 푼 다음 실행하세요. 라이브러리는 그대로 유지돼요.",

"failure.title": "무언가 실패했어요",
"failure.closeAria": "오류 창 닫기",
Expand Down
81 changes: 81 additions & 0 deletions tests/e2e/release-full-download.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// The update dialog when a release cannot be applied in place (#720).
//
// A release that changes the Python dependencies cannot be applied by the
// one-click update, which keeps the installed runtime. The dialog then offers
// only Download, and has to say that this download is a full package rather
// than an update, or a missing "Update now" reads as a broken update.
import { expect, test } from "@playwright/test";
import { stubTauri } from "./helpers.mjs";

const ASSETS = [
"StemDeck-Windows-x64.NVIDIA.zip",
"StemDeck-Windows-x64-app.zip",
"StemDeck-Windows-x64-app.zip.sha256",
"StemDeck-Windows-x64-runtime-version.json",
].map((name) => ({ name, browser_download_url: `https://example.invalid/${name}` }));

async function installedAppOffered(page, check) {
await stubTauri(page);
// After stubTauri's script: a Windows NVIDIA install, and the updater's
// answer for this release.
await page.addInitScript((answer) => {
const invoke = window.__TAURI__.core.invoke;
window.__TAURI__.core.invoke = (cmd, args) => {
if (cmd === "build_target") return Promise.resolve({ os: "windows", arch: "x64", gpu: "nvidia" });
if (cmd === "check_app_update") return Promise.resolve(answer);
return invoke(cmd, args);
};
}, check);
await page.route("**/api/health**", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ name: "StemDeck", status: "ok", version: "0.18.1", ffmpeg_configured: true }),
}));
await page.route("https://api.github.com/**", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
tag_name: "v0.19.0",
draft: false,
prerelease: false,
body: "notes",
html_url: "https://example.invalid",
assets: ASSETS,
}),
}));
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.locator("#notifBtn").click();
await page.locator("#notifReleaseCard").click();
await expect(page.locator("#releaseDialog")).toBeVisible();
}

test.describe("update dialog", () => {
test("a release that needs a new runtime says it is a full download", async ({ page }) => {
await installedAppOffered(page, {
supported: false,
reason: "python dependencies changed (py3.12-aaaaaaaaaaaaaaaa -> py3.12-bbbbbbbbbbbbbbbb)",
});
const note = page.locator("#releaseFullDownload");
await expect(note).toBeVisible();
await expect(note).toContainText("full download");
await expect(page.locator("#releaseDownloadApp")).toBeHidden();
await expect(page.locator("#releaseDownload")).toHaveAttribute(
"href",
"https://example.invalid/StemDeck-Windows-x64.NVIDIA.zip",
);
});

test("any other reason for no in-app update says nothing about a full download", async ({ page }) => {
await installedAppOffered(page, { supported: false, reason: "this install is not writable by the current user" });
await expect(page.locator("#releaseDownload")).toBeVisible();
await expect(page.locator("#releaseFullDownload")).toBeHidden();
});

test("a release that updates in place offers Update now and no note", async ({ page }) => {
await installedAppOffered(page, { supported: true, appSha256: "0".repeat(64) });
await expect(page.locator("#releaseDownloadApp")).toBeVisible();
await expect(page.locator("#releaseFullDownload")).toBeHidden();
});
});
Loading