mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 18:49:11 +03:00
Simplify and test updater service
- reverts to old UI - moves more business logic into .ts file - add tests for a few scenarios
This commit is contained in:
parent
086d0cb0fb
commit
4a2e947c46
11
apps/desktop/src/lib/backend/tauri.ts
Normal file
11
apps/desktop/src/lib/backend/tauri.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { invoke as invokeIpc, listen as listenIpc } from './ipc';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { checkUpdate, onUpdaterEvent } from '@tauri-apps/api/updater';
|
||||
|
||||
export class Tauri {
|
||||
invoke = invokeIpc;
|
||||
listen = listenIpc;
|
||||
checkUpdate = checkUpdate;
|
||||
onUpdaterEvent = onUpdaterEvent;
|
||||
currentVersion = getVersion;
|
||||
}
|
101
apps/desktop/src/lib/backend/updater.test.ts
Normal file
101
apps/desktop/src/lib/backend/updater.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Tauri } from './tauri';
|
||||
import { UpdaterService } from './updater';
|
||||
import { get } from 'svelte/store';
|
||||
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
/**
|
||||
* It is important to understand the sync `get` method performs a store subscription
|
||||
* under the hood.
|
||||
*/
|
||||
describe('Updater', () => {
|
||||
let tauri: Tauri;
|
||||
let updater: UpdaterService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
tauri = new Tauri();
|
||||
updater = new UpdaterService(tauri);
|
||||
vi.spyOn(tauri, 'listen').mockReturnValue(async () => {});
|
||||
vi.spyOn(tauri, 'onUpdaterEvent').mockReturnValue(Promise.resolve(() => {}));
|
||||
vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should not show up-to-date on interval check', async () => {
|
||||
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
|
||||
Promise.resolve({
|
||||
shouldUpdate: false
|
||||
})
|
||||
);
|
||||
|
||||
await updater.checkForUpdate();
|
||||
expect(get(updater.update)).toHaveProperty('status', undefined);
|
||||
});
|
||||
|
||||
test('should show up-to-date on manual check', async () => {
|
||||
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
|
||||
Promise.resolve({
|
||||
shouldUpdate: false
|
||||
})
|
||||
);
|
||||
await updater.checkForUpdate(true); // manual = true;
|
||||
expect(get(updater.update)).toHaveProperty('status', 'UPTODATE');
|
||||
});
|
||||
|
||||
test('should prompt again on new version', async () => {
|
||||
const body = 'release notes';
|
||||
const date = '2024-01-01';
|
||||
|
||||
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
|
||||
Promise.resolve({
|
||||
shouldUpdate: true,
|
||||
manifest: { version: '1', body, date }
|
||||
})
|
||||
);
|
||||
|
||||
await updater.checkForUpdate();
|
||||
const update1 = get(updater.update);
|
||||
expect(update1).toHaveProperty('version', '1');
|
||||
expect(update1).toHaveProperty('releaseNotes', body);
|
||||
updater.dismiss();
|
||||
|
||||
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
|
||||
Promise.resolve({
|
||||
shouldUpdate: true,
|
||||
manifest: { version: '2', body, date }
|
||||
})
|
||||
);
|
||||
await updater.checkForUpdate();
|
||||
const update2 = get(updater.update);
|
||||
expect(update2).toHaveProperty('version', '2');
|
||||
expect(update2).toHaveProperty('releaseNotes', body);
|
||||
});
|
||||
|
||||
test('should not prompt download for seen version', async () => {
|
||||
const version = '1';
|
||||
const body = 'release notes';
|
||||
const date = '2024-01-01';
|
||||
|
||||
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
|
||||
Promise.resolve({
|
||||
shouldUpdate: true,
|
||||
manifest: { version, body, date }
|
||||
})
|
||||
);
|
||||
const updater = new UpdaterService(tauri);
|
||||
await updater.checkForUpdate();
|
||||
|
||||
const update1 = get(updater.update);
|
||||
expect(update1).toHaveProperty('version', version);
|
||||
expect(update1).toHaveProperty('releaseNotes', body);
|
||||
|
||||
updater.dismiss();
|
||||
await updater.checkForUpdate();
|
||||
const update2 = get(updater.update);
|
||||
expect(update2).toHaveProperty('version', undefined);
|
||||
expect(update2).toHaveProperty('releaseNotes', undefined);
|
||||
});
|
||||
});
|
@ -1,33 +1,29 @@
|
||||
import { listen } from './ipc';
|
||||
import { Tauri } from './tauri';
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { relaunch } from '@tauri-apps/api/process';
|
||||
import {
|
||||
checkUpdate,
|
||||
installUpdate,
|
||||
onUpdaterEvent,
|
||||
type UpdateResult,
|
||||
type UpdateManifest,
|
||||
type UpdateStatus
|
||||
} from '@tauri-apps/api/updater';
|
||||
import posthog from 'posthog-js';
|
||||
import { derived, writable, type Readable } from 'svelte/store';
|
||||
import { derived, readable, writable } 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;
|
||||
manual: boolean;
|
||||
};
|
||||
type Status = UpdateStatus | 'DOWNLOADED';
|
||||
const TIMEOUT_SECONDS = 30;
|
||||
|
||||
/**
|
||||
* Note that the Tauri API `checkUpdate` hangs indefinitely in dev mode, build
|
||||
* a nightly if you want to test the updater manually.
|
||||
*
|
||||
* export TAURI_PRIVATE_KEY=doesnot
|
||||
* export TAURI_KEY_PASSWORD=matter
|
||||
* ./scripts/release.sh --channel nightly --version "0.5.678"
|
||||
*/
|
||||
export class UpdaterService {
|
||||
// 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);
|
||||
|
||||
readonly loading = writable(false);
|
||||
readonly status = writable<Status | undefined>();
|
||||
private manifest = writable<UpdateManifest | undefined>(undefined, () => {
|
||||
this.start();
|
||||
return () => {
|
||||
@ -35,48 +31,52 @@ export class UpdaterService {
|
||||
};
|
||||
});
|
||||
|
||||
update: Readable<Update | undefined> = derived(
|
||||
[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;
|
||||
private currentVersion = readable<string | undefined>(undefined, (set) => {
|
||||
this.tauri.currentVersion().then((version) => set(version));
|
||||
});
|
||||
|
||||
readonly update = derived(
|
||||
[this.manifest, this.status, this.currentVersion],
|
||||
([manifest, status, currentVersion]) => {
|
||||
return {
|
||||
version: manifest?.version,
|
||||
releaseNotes: manifest?.body,
|
||||
status,
|
||||
currentVersion
|
||||
};
|
||||
}
|
||||
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);
|
||||
private intervalId: any;
|
||||
private seenVersion: string | undefined;
|
||||
|
||||
intervalId: any;
|
||||
unlistenStatusFn: any;
|
||||
unlistenManualCheckFn: any;
|
||||
unlistenStatus?: () => void;
|
||||
unlistenMenu?: () => void;
|
||||
|
||||
constructor() {}
|
||||
constructor(private tauri: Tauri) {}
|
||||
|
||||
private async start() {
|
||||
this.currentVersion.set(await getVersion());
|
||||
this.unlistenManualCheckFn = listen<string>('menu://global/update/clicked', () => {
|
||||
this.unlistenMenu = this.tauri.listen<string>('menu://global/update/clicked', () => {
|
||||
this.checkForUpdate(true);
|
||||
});
|
||||
this.unlistenStatusFn = await onUpdaterEvent((event) => {
|
||||
|
||||
this.unlistenStatus = await this.tauri.onUpdaterEvent((event) => {
|
||||
const { error, status } = event;
|
||||
this.status.set(status);
|
||||
if (error) {
|
||||
showErrorToast(error);
|
||||
handleError(error, false);
|
||||
posthog.capture('App Update Status Error', { error });
|
||||
}
|
||||
this.status.set({ status });
|
||||
});
|
||||
|
||||
setInterval(async () => await this.checkForUpdate(), 3600000); // hourly
|
||||
this.checkForUpdate();
|
||||
}
|
||||
|
||||
private async stop() {
|
||||
this.unlistenStatusFn?.();
|
||||
this.unlistenManualCheckFn?.();
|
||||
this.unlistenStatus?.();
|
||||
this.unlistenMenu?.();
|
||||
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = undefined;
|
||||
@ -84,41 +84,78 @@ export class UpdaterService {
|
||||
}
|
||||
|
||||
async checkForUpdate(manual = false) {
|
||||
this.loading.set(true);
|
||||
try {
|
||||
const update = await Promise.race([
|
||||
checkUpdate(), // In DEV mode this never returns.
|
||||
new Promise<UpdateResult>((resolve) =>
|
||||
setTimeout(() => resolve({ shouldUpdate: false }), 30000)
|
||||
this.tauri.checkUpdate(), // In DEV mode this never returns.
|
||||
new Promise<UpdateResult>((_resolve, reject) =>
|
||||
// For manual testing use resolve instead of reject here.
|
||||
setTimeout(
|
||||
() => reject(`Timed out after ${TIMEOUT_SECONDS} seconds.`),
|
||||
TIMEOUT_SECONDS * 1000
|
||||
)
|
||||
)
|
||||
]);
|
||||
this.manual.set(manual);
|
||||
if (!update.shouldUpdate && manual) {
|
||||
this.status.set({ status: 'UPTODATE' });
|
||||
} else if (update.manifest) {
|
||||
this.manifest.set(update.manifest);
|
||||
await this.processUpdate(update, manual);
|
||||
} catch (err: unknown) {
|
||||
// No toast unless manually invoked.
|
||||
if (manual) {
|
||||
handleError(err, true);
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async processUpdate(update: UpdateResult, manual: boolean) {
|
||||
const { shouldUpdate, manifest } = update;
|
||||
if (shouldUpdate === false && manual) {
|
||||
this.status.set('UPTODATE');
|
||||
}
|
||||
if (manifest && manifest.version !== this.seenVersion) {
|
||||
this.manifest.set(manifest);
|
||||
this.seenVersion = manifest.version;
|
||||
}
|
||||
}
|
||||
|
||||
async installUpdate() {
|
||||
this.loading.set(true);
|
||||
try {
|
||||
await installUpdate();
|
||||
posthog.capture('App Update Successful');
|
||||
} catch (err: any) {
|
||||
// We expect toast to be shown by error handling in `onUpdaterEvent`
|
||||
posthog.capture('App Update Install Error', { error: err });
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
relaunchApp() {
|
||||
relaunch();
|
||||
async relaunchApp() {
|
||||
try {
|
||||
await relaunch();
|
||||
} catch (err: unknown) {
|
||||
handleError(err, true);
|
||||
}
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.manifest.set(undefined);
|
||||
this.status.set(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function isOffline(err: any): boolean {
|
||||
return typeof err === 'string' && err.includes('Could not fetch a valid release');
|
||||
return (
|
||||
typeof err === 'string' &&
|
||||
(err.includes('Could not fetch a valid release') || err.includes('Network Error'))
|
||||
);
|
||||
}
|
||||
|
||||
function showErrorToast(err: any) {
|
||||
if (isOffline(err)) return;
|
||||
function handleError(err: any, manual: boolean) {
|
||||
if (!manual && isOffline(err)) return;
|
||||
showToast({
|
||||
title: 'App update failed',
|
||||
message: `
|
||||
|
@ -1,153 +1,341 @@
|
||||
<script lang="ts">
|
||||
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, type Update } from '$lib/backend/updater';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const updaterService = getContext(UpdaterService);
|
||||
const currentVersion = updaterService.currentVersion;
|
||||
|
||||
const update = updaterService.update;
|
||||
const loading = updaterService.loading;
|
||||
|
||||
let status = $state<Update['status']>();
|
||||
let version = $state<Update['version']>();
|
||||
let lastVersion: string | undefined;
|
||||
let dismissed = false;
|
||||
let modal: Modal;
|
||||
let version = $state<string | undefined>();
|
||||
let releaseNotes = $state<string | undefined>();
|
||||
let status = $state<string | undefined>();
|
||||
|
||||
$effect(() => {
|
||||
if ($update) {
|
||||
console.log($update);
|
||||
handleUpdate($update);
|
||||
}
|
||||
({ version, releaseNotes, status } = $update || {});
|
||||
});
|
||||
|
||||
function handleUpdate(update: Update) {
|
||||
version = update?.version;
|
||||
|
||||
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;
|
||||
modal.close();
|
||||
updaterService.dismiss();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal width="xsmall" bind:this={modal}>
|
||||
<div class="modal-illustration">
|
||||
{#if status === 'UPTODATE' || status === 'DONE'}
|
||||
{@html upToDateSvg}
|
||||
{#if version || status === 'UPTODATE'}
|
||||
<div class="update-banner" class:busy={$loading}>
|
||||
<div class="floating-button">
|
||||
<Button icon="cross-small" style="ghost" onclick={handleDismiss} />
|
||||
</div>
|
||||
<div class="img">
|
||||
<div class="circle-img">
|
||||
{#if status !== 'DONE'}
|
||||
<svg
|
||||
class="arrow-img"
|
||||
width="12"
|
||||
height="34"
|
||||
viewBox="0 0 12 34"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 21V32.5M6 32.5L1 27.5M6 32.5L11 27.5"
|
||||
stroke="var(--clr-scale-ntrl-100)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M6 0V11.5M6 11.5L1 6.5M6 11.5L11 6.5"
|
||||
stroke="var(--clr-scale-ntrl-100)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
{@html newVersionSvg}
|
||||
<svg
|
||||
class="tick-img"
|
||||
width="14"
|
||||
height="11"
|
||||
viewBox="0 0 14 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 4.07692L5.57143 9L13 1"
|
||||
stroke="var(--clr-scale-ntrl-100)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h3 class="text-serif-32 modal-title">
|
||||
<svg
|
||||
width="60"
|
||||
height="36"
|
||||
viewBox="0 0 60 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M31.5605 35.5069C31.4488 35.5097 31.3368 35.5112 31.2245 35.5112H12.8571C5.75633 35.5112 0 29.7548 0 22.654C0 15.5532 5.75634 9.79688 12.8571 9.79688H16.123C18.7012 4.02354 24.493 0 31.2245 0C39.7331 0 46.7402 6.42839 47.6541 14.6934H49.5918C55.3401 14.6934 60 19.3533 60 25.1015C60 30.8498 55.3401 35.5097 49.5918 35.5097H32.4489C32.2692 35.5097 32.0906 35.5051 31.913 35.4961C31.7958 35.5009 31.6783 35.5045 31.5605 35.5069Z"
|
||||
fill="var(--clr-scale-pop-70)"
|
||||
/>
|
||||
<g opacity="0.4">
|
||||
<path
|
||||
d="M39 35.5102V29.2505H29.25V9.75049H39V19.5005H48.75V29.2505H58.5V30.4877C56.676 33.4983 53.3688 35.5102 49.5918 35.5102H39Z"
|
||||
fill="var(--clr-scale-pop-50)"
|
||||
/>
|
||||
<path
|
||||
d="M46.3049 9.75049H39V1.93967C42.2175 3.65783 44.8002 6.4091 46.3049 9.75049Z"
|
||||
fill="var(--clr-scale-pop-50)"
|
||||
/>
|
||||
<path
|
||||
d="M9.75 35.1337C10.745 35.3806 11.7858 35.5117 12.8571 35.5117H29.25V29.2505H9.75V19.5005H19.5V9.75049H29.25V0.117188C25.4568 0.568673 22.0577 2.30464 19.5 4.87786V9.75049H16.144C16.137 9.7661 16.13 9.78173 16.123 9.79737H12.8571C11.7858 9.79737 10.745 9.92841 9.75 10.1753V19.5005H0.389701C0.135193 20.5097 0 21.5663 0 22.6545C0 25.0658 0.663785 27.322 1.81859 29.2505H9.75V35.1337Z"
|
||||
fill="var(--clr-scale-pop-50)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h4 class="text-13 label">
|
||||
{#if status === 'UPTODATE'}
|
||||
You are up-to-date
|
||||
{:else if status === 'DONE'}
|
||||
Install complete!
|
||||
{:else}
|
||||
New {version} version
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
<p class="text-13 text-body modal-caption">
|
||||
{#if status === 'UPTODATE'}
|
||||
You're on GitButler {$currentVersion}, which is the most up-to-date version.
|
||||
{:else}
|
||||
Upgrade now for the latest features.
|
||||
<br />
|
||||
You can find release notes <Link href="https://discord.gg/WnTgfnmS">here</Link>.
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<div class="modal-actions">
|
||||
{#if status !== 'UPTODATE' && status !== 'DONE'}
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
disabled={status === 'PENDING' || status === 'DOWNLOADED'}
|
||||
onclick={handleDismiss}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#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={() => {
|
||||
updaterService.installUpdate();
|
||||
}}
|
||||
>
|
||||
{#if status === 'PENDING'}
|
||||
{:else if status === 'PENDING'}
|
||||
Downloading update...
|
||||
{:else if status === 'DOWNLOADED'}
|
||||
Installing update...
|
||||
{:else}
|
||||
Download {version}
|
||||
{:else if status === 'DONE'}
|
||||
Install complete
|
||||
{:else if status === 'CHECKING'}
|
||||
Checking for update
|
||||
{:else if status === 'ERROR'}
|
||||
Error occurred...
|
||||
{:else if version}
|
||||
New version available
|
||||
{/if}
|
||||
</h4>
|
||||
|
||||
<div class="buttons">
|
||||
{#if releaseNotes}
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
onmousedown={() => {
|
||||
showToast({
|
||||
id: 'release-notes',
|
||||
title: `Release notes for ${version}`,
|
||||
message: releaseNotes || 'no release notes available'
|
||||
});
|
||||
}}
|
||||
>
|
||||
Release notes
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="status-section">
|
||||
<div class="sliding-gradient"></div>
|
||||
<div class="cta-btn" transition:fade={{ duration: 100 }}>
|
||||
{#if !status}
|
||||
<Button
|
||||
wide
|
||||
style="pop"
|
||||
kind="solid"
|
||||
onmousedown={async () => {
|
||||
await updaterService.installUpdate();
|
||||
}}
|
||||
>
|
||||
Download {version}
|
||||
</Button>
|
||||
{:else if status === 'UPTODATE'}
|
||||
<Button
|
||||
wide
|
||||
style="pop"
|
||||
kind="solid"
|
||||
onmousedown={async () => {
|
||||
updaterService.dismiss();
|
||||
}}
|
||||
>
|
||||
Got it!
|
||||
</Button>
|
||||
{:else if status === 'DONE'}
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
wide
|
||||
onclick={async () => await updaterService.relaunchApp()}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.modal-illustration {
|
||||
.update-banner {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
|
||||
position: fixed;
|
||||
z-index: var(--z-blocker);
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
padding: 24px;
|
||||
background-color: var(--clr-bg-1);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
.label {
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
}
|
||||
|
||||
.modal-caption {
|
||||
color: var(--clr-text-2);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
.buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* STATUS SECTION */
|
||||
|
||||
.status-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
border-radius: var(--radius-m);
|
||||
|
||||
transition:
|
||||
transform 0.15s ease-in-out,
|
||||
height 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.sliding-gradient {
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
|
||||
mix-blend-mode: overlay;
|
||||
|
||||
background: linear-gradient(
|
||||
80deg,
|
||||
rgba(255, 255, 255, 0) 9%,
|
||||
rgba(255, 255, 255, 0.5) 31%,
|
||||
rgba(255, 255, 255, 0) 75%
|
||||
);
|
||||
animation: slide 3s ease-in-out infinite;
|
||||
|
||||
transition: width 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.cta-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.busy {
|
||||
& .status-section {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
& .sliding-gradient {
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
80deg,
|
||||
rgba(255, 255, 255, 0) 9%,
|
||||
rgba(255, 255, 255, 0.9) 31%,
|
||||
rgba(255, 255, 255, 0) 75%
|
||||
);
|
||||
animation: slide 1.6s ease-in infinite;
|
||||
}
|
||||
|
||||
& .arrow-img {
|
||||
transform: rotate(180deg);
|
||||
animation: moving-arrow 1s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* IMAGE */
|
||||
|
||||
.img {
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.circle-img {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
bottom: -8px;
|
||||
left: 17px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--clr-scale-pop-40);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
box-shadow: inset 0 0 4px 4px var(--clr-scale-pop-40);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-img {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
left: 7px;
|
||||
/* transform: translateY(20px); */
|
||||
}
|
||||
|
||||
.tick-img {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
@keyframes moving-arrow {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(21px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -6,6 +6,7 @@ import { GitConfigService } from '$lib/backend/gitConfigService';
|
||||
import { HttpClient } from '$lib/backend/httpClient';
|
||||
import { ProjectService } from '$lib/backend/projects';
|
||||
import { PromptService } from '$lib/backend/prompt';
|
||||
import { Tauri } from '$lib/backend/tauri';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import { RustSecretService } from '$lib/secrets/secretsService';
|
||||
@ -32,7 +33,7 @@ export const load: LayoutLoad = async () => {
|
||||
const httpClient = new HttpClient();
|
||||
const authService = new AuthService();
|
||||
const projectService = new ProjectService(defaultPath, httpClient);
|
||||
const updaterService = new UpdaterService();
|
||||
const updaterService = new UpdaterService(new Tauri());
|
||||
const promptService = new PromptService();
|
||||
const userService = new UserService(httpClient);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user