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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## [Unreleased]

### 修复

- 设置面板内检查到新版本后直接显示「立即更新」按钮,安装完成后可在同一位置重启;此前更新入口只在底部横幅,被设置面板挡住,必须关闭设置才能操作。

### 安全

- 自动 Git 提交改用私有索引,并禁用仓库 hooks、fsmonitor、GPG 签名与外部 Markdown filter;共享/网络笔记目录里的仓库配置不能再借保存操作执行命令,也不会覆盖用户自己的暂存区。
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
<div class="row">
<span class="tip">当前版本 <span id="app-version">—</span></span>
<button id="check-update" class="btn" type="button">检查更新</button>
<button id="update-now" class="btn primary" type="button" hidden>立即更新</button>
</div>
<p id="update-status" class="tip" aria-live="polite"></p>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const ui = {
syncFootStatus: el<HTMLSpanElement>("sync-foot-status"),
syncConflicts: el<HTMLButtonElement>("sync-conflicts"),
checkUpdateButton: el<HTMLButtonElement>("check-update"),
updateNowButton: el<HTMLButtonElement>("update-now"),
updateStatus: el<HTMLParagraphElement>("update-status"),
appVersionLabel: el<HTMLSpanElement>("app-version"),
updateBanner: el<HTMLDivElement>("update-banner"),
Expand Down
83 changes: 82 additions & 1 deletion src/main.dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const mocks = vi.hoisted(() => ({
enable: vi.fn(async () => {}),
disable: vi.fn(async () => {}),
openDialog: vi.fn(async () => null),
check: vi.fn(async () => null),
check: vi.fn<() => Promise<unknown>>(async () => null),
relaunch: vi.fn(async () => {}),
isVisible: vi.fn(async () => false),
onFocusChanged: vi.fn(async () => () => {}),
Expand Down Expand Up @@ -367,6 +367,87 @@ describe("设置", () => {
});
});

describe("更新", () => {
beforeEach(() => {
mocks.check.mockReset();
mocks.check.mockResolvedValue(null);
mocks.relaunch.mockReset();
});

it("检查到新版本后可在设置面板内直接更新并重启", async () => {
await boot({ flush_sync_state: () => null });
node<HTMLButtonElement>("#settings-btn").click();
await flush();

const inline = node<HTMLButtonElement>("#update-now");
expect(inline.hidden).toBe(true);

const downloadAndInstall = vi.fn(async (onEvent: (event: unknown) => void) => {
onEvent({ event: "Started", data: { contentLength: 100 } });
onEvent({ event: "Progress", data: { chunkLength: 100 } });
});
mocks.check.mockResolvedValueOnce({ version: "0.6.0", downloadAndInstall });

node<HTMLButtonElement>("#check-update").click();
await flush();

expect(inline.hidden).toBe(false);
expect(inline.textContent).toBe("立即更新");
expect(node<HTMLElement>("#update-status").textContent).toBe("发现新版本 0.6.0");

inline.click();
await flush();

expect(downloadAndInstall).toHaveBeenCalledTimes(1);
expect(node<HTMLElement>("#update-status").textContent).toContain("已安装 0.6.0");
expect(inline.textContent).toBe("重启");

inline.click();
await flush();

expect(mocks.invoke).toHaveBeenCalledWith("flush_sync_state");
expect(mocks.relaunch).toHaveBeenCalledTimes(1);
});

it("已是最新版本时不显示「立即更新」", async () => {
await boot();
node<HTMLButtonElement>("#settings-btn").click();
await flush();

node<HTMLButtonElement>("#check-update").click();
await flush();

expect(node<HTMLButtonElement>("#update-now").hidden).toBe(true);
expect(node<HTMLElement>("#update-status").textContent).toBe("已是最新版本");
});

it("安装失败时按钮变成「重试」,再点一次重新下载", async () => {
await boot();
node<HTMLButtonElement>("#settings-btn").click();
await flush();

const downloadAndInstall = vi.fn(async () => {
throw new Error("网络中断");
});
mocks.check.mockResolvedValueOnce({ version: "0.6.0", downloadAndInstall });

node<HTMLButtonElement>("#check-update").click();
await flush();

const inline = node<HTMLButtonElement>("#update-now");
inline.click();
await flush();

expect(inline.textContent).toBe("重试");
expect(node<HTMLElement>("#update-status").textContent).toBe("更新失败");

inline.click();
await flush();

expect(downloadAndInstall).toHaveBeenCalledTimes(2);
});
});

describe("Git 自动提交", () => {
async function openGitSettings(): Promise<void> {
node<HTMLButtonElement>("#settings-btn").click();
Expand Down
63 changes: 45 additions & 18 deletions src/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ export function hideUpdateBanner(): void {
ui.updateBanner.classList.add("hidden");
}

/**
* 设置面板里的更新入口。面板打开时底部的更新横幅会被遮住,
* 手动检查发现新版本后必须有一个能点的按钮。
*/
function setInlineUpdateAction(label: string | null, disabled = false): void {
ui.updateNowButton.hidden = label === null;
ui.updateNowButton.disabled = disabled;
if (label !== null) ui.updateNowButton.textContent = label;
}

let pendingUpdate: Update | null = null;
let updateReady = false;
let updating = false;
Expand Down Expand Up @@ -51,6 +61,7 @@ async function checkForUpdate(manual: boolean): Promise<void> {
if (manual) {
ui.updateStatus.textContent = "更新已安装,重启后生效";
showUpdateBanner("更新已安装,重启后生效", "重启");
setInlineUpdateAction("重启");
}
return;
}
Expand All @@ -62,6 +73,8 @@ async function checkForUpdate(manual: boolean): Promise<void> {
try {
const update = await check();
if (!update) {
pendingUpdate = null;
setInlineUpdateAction(null);
if (manual) {
ui.updateStatus.textContent = "已是最新版本";
toast("已是最新版本");
Expand All @@ -72,6 +85,7 @@ async function checkForUpdate(manual: boolean): Promise<void> {
pendingUpdate = update;
ui.updateStatus.textContent = `发现新版本 ${update.version}`;
showUpdateBanner(`发现新版本 ${update.version}`, "更新");
setInlineUpdateAction("立即更新");
} catch (error) {
// 启动时的静默检查失败不打扰用户;手动检查才提示
if (manual) {
Expand All @@ -88,7 +102,10 @@ async function installUpdate(): Promise<void> {
if (!update || updating) return;

updating = true;
ui.checkUpdateButton.disabled = true;
ui.updateBannerAction.disabled = true;
setInlineUpdateAction("正在更新…", true);
ui.updateStatus.textContent = "正在下载更新…";
showUpdateBanner("正在下载更新…", null);

try {
Expand All @@ -101,47 +118,57 @@ async function installUpdate(): Promise<void> {
received += event.data.chunkLength;
const percent =
total > 0 ? Math.min(100, Math.round((received / total) * 100)) : null;
showUpdateBanner(
percent === null ? "正在下载更新…" : `正在下载更新… ${percent}%`,
null,
);
const message =
percent === null ? "正在下载更新…" : `正在下载更新… ${percent}%`;
ui.updateStatus.textContent = message;
showUpdateBanner(message, null);
}
});

updateReady = true;
ui.updateStatus.textContent = `已安装 ${update.version},重启后生效`;
showUpdateBanner(`新版本 ${update.version} 已安装,重启后生效`, "重启");
setInlineUpdateAction("重启");
} catch (error) {
showUpdateBanner("更新失败,请稍后重试", "重试");
ui.updateStatus.textContent = "更新失败";
setInlineUpdateAction("重试");
toast(String(error), true);
} finally {
updating = false;
ui.checkUpdateButton.disabled = false;
ui.updateBannerAction.disabled = false;
}
}

/** 重启前把设置与同步状态落盘,避免更新后重复合并或重复上传。 */
function restartToApplyUpdate(): void {
// 落盘失败也要重启,但必须告诉用户这次「尽力了」。
void invoke("flush_sync_state")
.catch(async (error) => {
console.error("保存同步状态失败", error);
toast("同步状态未能落盘,仍将重启", true);
// 留一点时间让提示画出来,否则立刻重启等于没提示。
await new Promise((resolve) => window.setTimeout(resolve, 1500));
})
.finally(() => void relaunch());
}

/** 模块加载时绑定更新相关按钮。 */
export function initUpdater(): void {
ui.checkUpdateButton.addEventListener("click", () => {
void checkForUpdate(true);
});

ui.updateNowButton.addEventListener("click", () => {
if (updating) return;
if (updateReady) restartToApplyUpdate();
else void installUpdate();
});

ui.updateBannerAction.addEventListener("click", () => {
if (updateReady) {
// 重启前把设置与同步状态落盘,避免更新后重复合并或重复上传。
// 落盘失败也要重启,但必须告诉用户这次「尽力了」。
void invoke("flush_sync_state")
.catch(async (error) => {
console.error("保存同步状态失败", error);
toast("同步状态未能落盘,仍将重启", true);
// 留一点时间让提示画出来,否则立刻重启等于没提示。
await new Promise((resolve) => window.setTimeout(resolve, 1500));
})
.finally(() => void relaunch());
} else {
void installUpdate();
}
if (updateReady) restartToApplyUpdate();
else void installUpdate();
});

ui.updateBannerDismiss.addEventListener("click", hideUpdateBanner);
Expand Down