mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
Refactor app updater a bit
- fixes things discovered in manual testing - avoid $effect loops
This commit is contained in:
parent
87b5590161
commit
c9b5aa7c26
@ -14,13 +14,21 @@ import posthog from 'posthog-js';
|
||||
import { derived, writable, type Readable } from 'svelte/store';
|
||||
|
||||
// TODO: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
|
||||
export type Update =
|
||||
| { version?: string; status?: UpdateStatus | 'DOWNLOADED'; body?: string }
|
||||
| undefined;
|
||||
export type Update = {
|
||||
version?: string;
|
||||
status?: UpdateStatus | 'DOWNLOADED';
|
||||
body?: string;
|
||||
manual: boolean;
|
||||
};
|
||||
|
||||
export class UpdaterService {
|
||||
private status = writable<UpdateStatus>(undefined);
|
||||
private result = writable<UpdateManifest | undefined>(undefined, () => {
|
||||
// True if manually initiated check.
|
||||
private manual = writable(false);
|
||||
|
||||
// An object rather than string to prevent unique deduplication.
|
||||
private status = writable<{ status: UpdateStatus } | undefined>(undefined);
|
||||
|
||||
private manifest = writable<UpdateManifest | undefined>(undefined, () => {
|
||||
this.start();
|
||||
return () => {
|
||||
this.stop();
|
||||
@ -28,41 +36,42 @@ export class UpdaterService {
|
||||
});
|
||||
|
||||
update: Readable<Update | undefined> = derived(
|
||||
[this.status, this.result],
|
||||
([status, update]) => {
|
||||
return { ...update, status };
|
||||
[this.status, this.manifest, this.manual],
|
||||
([status, result, manual]) => {
|
||||
// Do not emit when up-to-date unless manually initiated.
|
||||
if (status?.status === 'UPTODATE' && result && !manual) {
|
||||
return;
|
||||
}
|
||||
return { ...result, ...status, manual };
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
// Needed to reset dismissed modal when version changes.
|
||||
currentVersion = writable<string | undefined>(undefined);
|
||||
readonly version = derived(this.update, (update) => update?.version);
|
||||
|
||||
prev: Update | undefined;
|
||||
intervalId: any;
|
||||
unlistenStatusFn: any;
|
||||
unlistenManualCheckFn: any;
|
||||
intervalId: any;
|
||||
|
||||
constructor() {}
|
||||
|
||||
private async start() {
|
||||
const currentVersion = await getVersion();
|
||||
this.currentVersion.set(currentVersion);
|
||||
this.currentVersion.set(await getVersion());
|
||||
this.unlistenManualCheckFn = listen<string>('menu://global/update/clicked', () => {
|
||||
this.checkForUpdate(true);
|
||||
});
|
||||
|
||||
this.unlistenStatusFn = await onUpdaterEvent((status) => {
|
||||
const err = status.error;
|
||||
if (err) {
|
||||
showErrorToast(err);
|
||||
posthog.capture('App Update Status Error', { error: err });
|
||||
this.unlistenStatusFn = await onUpdaterEvent((event) => {
|
||||
const { error, status } = event;
|
||||
if (error) {
|
||||
showErrorToast(error);
|
||||
posthog.capture('App Update Status Error', { error });
|
||||
}
|
||||
this.status.set(status.status);
|
||||
this.status.set({ status });
|
||||
});
|
||||
|
||||
await this.checkForUpdate();
|
||||
setInterval(async () => await this.checkForUpdate(), 3600000); // hourly
|
||||
this.checkForUpdate();
|
||||
}
|
||||
|
||||
private async stop() {
|
||||
@ -74,17 +83,18 @@ export class UpdaterService {
|
||||
}
|
||||
}
|
||||
|
||||
async checkForUpdate(isManual = false) {
|
||||
async checkForUpdate(manual = false) {
|
||||
const update = await Promise.race([
|
||||
checkUpdate(), // In DEV mode this never returns.
|
||||
new Promise<UpdateResult>((resolve) =>
|
||||
setTimeout(() => resolve({ shouldUpdate: false }), 30000)
|
||||
)
|
||||
]);
|
||||
if (!update.shouldUpdate && isManual) {
|
||||
this.status.set('UPTODATE');
|
||||
this.manual.set(manual);
|
||||
if (!update.shouldUpdate && manual) {
|
||||
this.status.set({ status: 'UPTODATE' });
|
||||
} else if (update.manifest) {
|
||||
this.result.set(update.manifest);
|
||||
this.manifest.set(update.manifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,50 +2,58 @@
|
||||
import Link from '../shared/Link.svelte';
|
||||
import newVersionSvg from '$lib/assets/empty-state/app-new-version.svg?raw';
|
||||
import upToDateSvg from '$lib/assets/empty-state/app-up-to-date.svg?raw';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { UpdaterService, type Update } from '$lib/backend/updater';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import Button from '@gitbutler/ui/inputs/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/modal/Modal.svelte';
|
||||
|
||||
const updaterService = getContext(UpdaterService);
|
||||
const update = updaterService.update;
|
||||
const currentVersion = updaterService.currentVersion;
|
||||
|
||||
const status = $derived($update?.status);
|
||||
const version = $derived($update?.version);
|
||||
const update = updaterService.update;
|
||||
|
||||
let dismissed = $state(false);
|
||||
let open = $state(false);
|
||||
|
||||
let modalRef: Modal;
|
||||
let status = $state<Update['status']>();
|
||||
let version = $state<Update['version']>();
|
||||
let lastVersion: string | undefined;
|
||||
let dismissed = false;
|
||||
let modal: Modal;
|
||||
|
||||
$effect(() => {
|
||||
if (version || (status && status !== 'ERROR' && !dismissed && !open)) {
|
||||
open = true;
|
||||
if ($update) {
|
||||
console.log($update);
|
||||
handleUpdate($update);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (status === 'ERROR') {
|
||||
open = false;
|
||||
}
|
||||
});
|
||||
function handleUpdate(update: Update) {
|
||||
version = update?.version;
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
modalRef.show();
|
||||
} else {
|
||||
modalRef.close();
|
||||
if (version !== lastVersion) {
|
||||
dismissed = false;
|
||||
}
|
||||
});
|
||||
|
||||
status = update?.status;
|
||||
const manual = update?.manual;
|
||||
|
||||
if (manual) {
|
||||
modal.show();
|
||||
} else if (status === 'ERROR') {
|
||||
modal.close();
|
||||
} else if (status && status !== 'UPTODATE' && !dismissed) {
|
||||
modal.show();
|
||||
} else if (version && !dismissed) {
|
||||
modal.show();
|
||||
}
|
||||
lastVersion = version;
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
dismissed = true;
|
||||
open = false;
|
||||
modal.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal width="xsmall" bind:this={modalRef}>
|
||||
<Modal width="xsmall" bind:this={modal}>
|
||||
<div class="modal-illustration">
|
||||
{#if status === 'UPTODATE' || status === 'DONE'}
|
||||
{@html upToDateSvg}
|
||||
@ -88,22 +96,32 @@
|
||||
|
||||
{#if status === 'UPTODATE'}
|
||||
<Button style="pop" kind="solid" wide outline onclick={handleDismiss}>Got it!</Button>
|
||||
{:else if status === 'DONE'}
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
wide
|
||||
outline
|
||||
onclick={() => {
|
||||
updaterService.relaunchApp();
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
wide
|
||||
loading={status === 'PENDING' || status === 'DOWNLOADED'}
|
||||
onclick={async () => {
|
||||
await updaterService.installUpdate();
|
||||
onclick={() => {
|
||||
updaterService.installUpdate();
|
||||
}}
|
||||
>
|
||||
{#if status === 'PENDING'}
|
||||
Downloading update...
|
||||
{:else if status === 'DOWNLOADED'}
|
||||
Installing update...
|
||||
{:else if status === 'DONE'}
|
||||
Restart
|
||||
{:else}
|
||||
Download {version}
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user