mirror of
https://github.com/priyanshujain/margin-docs.git
synced 2026-10-02 11:07:05 +00:00
feature fixes
This commit is contained in:
1 parent
586ee946d0
commit
c1c47bc513
63 files changed
+12083
-229
No files matched your search
+171
@@ -0,0 +1,171 @@
|
||||
// The updater, from the frontend's side: asking whether there is one, downloading it with progress
|
||||
// somebody can watch, and relaunching without losing an edit made half a second ago.
|
||||
//
|
||||
// `relaunch()` does not go through the window close handler that protects a dirty buffer, so
|
||||
// anything here that relaunches flushes a pending save first.
|
||||
//
|
||||
// Everything Tauri-shaped lives here and the state it produces lives in src/store/useUpdate.ts,
|
||||
// which is what the dialog and the settings panel read. The one thing that cannot go in that store
|
||||
// is `offered` below: the value `check()` hands back is a resource handle, a number with a download
|
||||
// behind it on the Rust side, so it is held here for as long as the dialog is open and closed when
|
||||
// the dialog is not.
|
||||
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { check, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { flushPendingSave } from "./document";
|
||||
import { isDesktop, isTauri } from "./ipc";
|
||||
import { useUpdate } from "./store/useUpdate";
|
||||
import { notify } from "./store/useToast";
|
||||
|
||||
/**
|
||||
* A dev build has no `plugins.updater` in tauri.conf.json, so the plugin is never registered and
|
||||
* `check()` fails with a raw plugin error. That is a sentence about the build, not a fault, and it
|
||||
* reads as one.
|
||||
*/
|
||||
const NOT_ENABLED = "Updates are not enabled in this build.";
|
||||
|
||||
/** How stale the last check has to be before an automatic one is worth making on launch. */
|
||||
const AUTOMATIC_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* How long after launch the automatic check waits. The first paint, the session restore and the
|
||||
* first index pass all want the main thread and the network before anything asks GitHub a question
|
||||
* nobody has asked for.
|
||||
*/
|
||||
const LAUNCH_DELAY_MS = 6_000;
|
||||
|
||||
let offered: Update | null = null;
|
||||
|
||||
/**
|
||||
* Whether a rejection means "this build has no updater in it" rather than "the check failed".
|
||||
*
|
||||
* src-tauri/src/lib.rs registers the plugin only when the config it needs is present, so in a dev
|
||||
* build there is nothing behind `plugin:updater|check` and Tauri rejects it from its own plugin
|
||||
* store: `PluginStore::extend_api` formats exactly `plugin updater not found`. The ACL arrives at
|
||||
* the same fact from the other side, ending its longer sentence with `Plugin not found`, for a
|
||||
* build whose capability no longer grants `updater:default`. Both mean the same thing to somebody
|
||||
* looking at the screen and neither is theirs to act on, so both become the one sentence above.
|
||||
*
|
||||
* Matched against the exact strings rather than against the word "updater", which also appears in
|
||||
* every ordinary failure this function has to let through: a bad signature, an unreachable endpoint
|
||||
* and a malformed manifest all name the plugin in their message.
|
||||
*/
|
||||
function updaterMissing(e: unknown): boolean {
|
||||
const message = String(e);
|
||||
return message === "plugin updater not found" || message.endsWith("Plugin not found");
|
||||
}
|
||||
|
||||
async function runCheck(quiet: boolean): Promise<void> {
|
||||
// The updater and the process plugin are both desktop-only, in lib.rs and in the capability, so
|
||||
// on a phone and in a browser this is a fact about the build rather than a command that failed.
|
||||
if (!isDesktop) {
|
||||
if (!quiet) notify(NOT_ENABLED);
|
||||
return;
|
||||
}
|
||||
// Anything other than idle is a check already running or a dialog already answering the last one.
|
||||
if (useUpdate.getState().phase !== "idle") return;
|
||||
|
||||
useUpdate.getState().begin();
|
||||
try {
|
||||
const update = await check();
|
||||
useUpdate.getState().markChecked();
|
||||
if (!update) {
|
||||
useUpdate.getState().dismiss();
|
||||
if (!quiet) notify("Margin Docs is up to date");
|
||||
return;
|
||||
}
|
||||
offered = update;
|
||||
useUpdate.getState().offer(update.version, update.body ?? null);
|
||||
} catch (e) {
|
||||
useUpdate.getState().dismiss();
|
||||
if (quiet) return;
|
||||
notify(updaterMissing(e) ? NOT_ENABLED : `Could not check for updates: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `check-updates` command, and the button in the settings panel. Says something either way:
|
||||
* somebody who pressed it is owed an answer even when the answer is that there is nothing to do.
|
||||
*/
|
||||
export async function checkForUpdates(): Promise<void> {
|
||||
await runCheck(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The same check, made on the app's own initiative and saying nothing unless there is an update to
|
||||
* show. Off entirely when the user has turned the preference off, and skipped when the last check
|
||||
* is recent enough that another one would only be noise.
|
||||
*
|
||||
* Returns its own cancel, so a shell that unmounts before the delay is up does not leave a check
|
||||
* behind it.
|
||||
*/
|
||||
export function startUpdateChecks(): () => void {
|
||||
const { automatic, lastChecked } = useUpdate.getState();
|
||||
if (!isDesktop || !automatic) return () => {};
|
||||
if (lastChecked !== null && Date.now() - lastChecked < AUTOMATIC_INTERVAL_MS) return () => {};
|
||||
const timer = window.setTimeout(() => void runCheck(true), LAUNCH_DELAY_MS);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the update the dialog is showing and restarts into it.
|
||||
*
|
||||
* The progress callback is the whole reason this is not one line: `downloadAndInstall` reports
|
||||
* every chunk and the dialog draws them, so a hundred megabytes over a slow connection is a bar
|
||||
* moving rather than an app that has stopped answering.
|
||||
*
|
||||
* Nothing runs after `relaunch()`, which is why the flush is in front of it. The window close
|
||||
* handler in src/App.tsx is what normally saves a buffer half a second old, and a relaunch does not
|
||||
* go anywhere near it.
|
||||
*/
|
||||
export async function installUpdate(): Promise<void> {
|
||||
const update = offered;
|
||||
if (update === null) return;
|
||||
|
||||
let downloaded = 0;
|
||||
let total: number | null = null;
|
||||
useUpdate.getState().progress(0, null);
|
||||
try {
|
||||
await update.downloadAndInstall((event) => {
|
||||
if (event.event === "Started") {
|
||||
total = event.data.contentLength ?? null;
|
||||
useUpdate.getState().progress(0, total);
|
||||
} else if (event.event === "Progress") {
|
||||
downloaded += event.data.chunkLength;
|
||||
useUpdate.getState().progress(downloaded, total);
|
||||
} else {
|
||||
// The bytes are in and the bundle is being swapped over. There is no number for this part
|
||||
// and it is not instant, so it is a phase rather than a bar sitting full.
|
||||
useUpdate.getState().installing();
|
||||
}
|
||||
});
|
||||
offered = null;
|
||||
await flushPendingSave();
|
||||
await relaunch();
|
||||
} catch (e) {
|
||||
useUpdate.getState().failed(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Later, Escape, or the close button. The handle goes with the dialog. */
|
||||
export function dismissUpdate(): void {
|
||||
const update = offered;
|
||||
offered = null;
|
||||
useUpdate.getState().dismiss();
|
||||
if (update !== null) void update.close().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* What the running bundle says its version is, which is the number the updater compares against the
|
||||
* manifest. Asked of Tauri rather than read out of package.json so that the settings panel cannot
|
||||
* disagree with the app it is inside. Null in a browser, where there is no bundle to ask.
|
||||
*/
|
||||
export async function appVersion(): Promise<string | null> {
|
||||
if (!isTauri) return null;
|
||||
try {
|
||||
return await getVersion();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user