Skip to content
Draft
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
1 change: 1 addition & 0 deletions backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service
services.AddSingleton<UpdateChecker>();
services.AddSingleton<IHostedService>(s => s.GetRequiredService<UpdateChecker>());
services.TryAddSingleton<IPlatformUpdateService, CorePlatformUpdateService>();
services.AddSingleton<UpdateService>();
services.AddSingleton<TestingService>();
services.AddOptions<FwLiteConfig>().BindConfiguration("FwLite");
services.DecorateConstructor<IJSRuntime>((provider, runtime) =>
Expand Down
3 changes: 3 additions & 0 deletions backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ IServiceProvider services
DotnetService.MultiWindowService,
DotnetService.JsEventListener,
DotnetService.JsInvokableLogger,
DotnetService.UpdateService,
];

public static Type GetServiceType(DotnetService service) => service switch
Expand All @@ -53,6 +54,7 @@ IServiceProvider services
DotnetService.MultiWindowService => typeof(IMultiWindowService),
DotnetService.JsEventListener => typeof(JsEventListener),
DotnetService.JsInvokableLogger => typeof(JsInvokableLogger),
DotnetService.UpdateService => typeof(UpdateService),
_ => throw new ArgumentOutOfRangeException(nameof(service), service, null)
};

Expand Down Expand Up @@ -111,4 +113,5 @@ public enum DotnetService
MultiWindowService,
JsEventListener,
JsInvokableLogger,
UpdateService,
}
13 changes: 13 additions & 0 deletions backend/FwLite/FwLiteShared/Services/UpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using FwLiteShared.AppUpdate;
using Microsoft.JSInterop;

namespace FwLiteShared.Services;

public class UpdateService(UpdateChecker updateChecker)
{
[JSInvokable]
public Task CheckForUpdates()
{
return updateChecker.TryUpdate(forceCheck: true);
}
}
6 changes: 6 additions & 0 deletions frontend/viewer/src/home/HomeView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import {transitionContext} from './transitions';
import Anchor from '$lib/components/ui/anchor/anchor.svelte';
import FeedbackDialog from '$lib/about/FeedbackDialog.svelte';
import UpdateDialog from '$lib/about/UpdateDialog.svelte';
import DeleteDialog from '$lib/entry-editor/DeleteDialog.svelte';
import {SYNC_DIALOG_QUERY_PARAM} from '../project/SyncDialog.svelte';

Expand Down Expand Up @@ -132,6 +133,7 @@
}

let feedbackOpen = $state(false);
let updateDialogOpen = $state(false);

let deleteDialog = $state<DeleteDialog>();
</script>
Expand Down Expand Up @@ -160,6 +162,9 @@
<ResponsiveMenu.Root>
<ResponsiveMenu.Trigger/>
<ResponsiveMenu.Content>
<ResponsiveMenu.Item onSelect={() => updateDialogOpen = true} icon="i-mdi-update">
{$t`Check for Updates`}
</ResponsiveMenu.Item>
<ResponsiveMenu.Item onSelect={() => feedbackOpen = true} icon="i-mdi-message">
{$t`Feedback & Support`}
</ResponsiveMenu.Item>
Expand All @@ -170,6 +175,7 @@
</ResponsiveMenu.Item>
</ResponsiveMenu.Content>
</ResponsiveMenu.Root>
<UpdateDialog bind:open={updateDialogOpen}/>
<FeedbackDialog bind:open={feedbackOpen}/>
<TroubleshootDialog bind:this={troubleshootDialog}/>
</div>
Expand Down
180 changes: 180 additions & 0 deletions frontend/viewer/src/lib/about/UpdateDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<script lang="ts">
import {t} from 'svelte-i18n-lingui';
import {Icon} from '$lib/components/ui/icon';
import {useFwLiteConfig, useUpdateService, useService} from '$lib/services/service-provider';
import {Button} from '$lib/components/ui/button';
import ResponsiveDialog from '$lib/components/responsive-dialog/responsive-dialog.svelte';
import {UpdateResult} from '$lib/dotnet-types/generated-types/FwLiteShared/AppUpdate/UpdateResult';
import type {IAppUpdateEvent} from '$lib/dotnet-types/generated-types/FwLiteShared/Events/IAppUpdateEvent';
import {FwEventType} from '$lib/dotnet-types/generated-types/FwLiteShared/Events/FwEventType';
import {DotnetService} from '$lib/dotnet-types';

let {open = $bindable()}: { open: boolean } = $props();
const config = useFwLiteConfig();
const updateService = useUpdateService();
const jsEventListener = useService(DotnetService.JsEventListener);

let checking = $state(false);
let updateResult = $state<UpdateResult | null>(null);
let errorMessage = $state<string | null>(null);
let eventSubscription: Promise<void> | null = null;

async function subscribeToUpdateEvents() {
try {
while (checking) {
const event = await jsEventListener.nextEventAsync();
if (event && event.type === FwEventType.AppUpdate) {
const updateEvent = event as IAppUpdateEvent;
updateResult = updateEvent.result;
break;
}
}
} catch (error) {
console.error('Error subscribing to update events:', error);
}
}

async function checkForUpdates() {
checking = true;
updateResult = null;
errorMessage = null;

try {
// Start listening for update events before triggering the check
eventSubscription = subscribeToUpdateEvents();

// Trigger the update check
await updateService.checkForUpdates();

// Wait for the event (with a timeout)
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 30000); // 30 second timeout
});

await Promise.race([eventSubscription, timeoutPromise]);

// If we still don't have a result after timeout, check lastEvent
if (updateResult === null) {
const event = await jsEventListener.lastEvent(FwEventType.AppUpdate) as IAppUpdateEvent | null;
if (event) {
updateResult = event.result;
} else {
errorMessage = $t`Update check timed out.`;
updateResult = UpdateResult.Failed;
}
}
} catch (error) {
console.error('Error checking for updates:', error);
errorMessage = error instanceof Error ? error.message : String(error);
updateResult = UpdateResult.Failed;
} finally {
checking = false;
eventSubscription = null;
}
}

function getUpdateMessage(): string {
if (checking) {
return $t`Checking for updates...`;
}

if (errorMessage) {
return $t`Error checking for updates: ${errorMessage}`;
}

switch (updateResult) {
case UpdateResult.Success:
return $t`Update installed successfully! Please restart the application.`;
case UpdateResult.Started:
return $t`Update download started. The application will update when you restart it.`;
case UpdateResult.ManualUpdateRequired:
return $t`An update is available! Please visit the download page to update manually.`;
case UpdateResult.Failed:
return $t`Failed to check for updates. Please try again later.`;
case UpdateResult.Unknown:
return $t`No updates available. You are running the latest version.`;
default:
return $t`Check completed. Status unknown.`;
}
}

function getUpdateIcon(): string {
if (checking) {
return 'i-mdi-loading';
}

switch (updateResult) {
case UpdateResult.Success:
case UpdateResult.Started:
return 'i-mdi-check-circle';
case UpdateResult.ManualUpdateRequired:
return 'i-mdi-information';
case UpdateResult.Failed:
return 'i-mdi-alert-circle';
case UpdateResult.Unknown:
default:
return 'i-mdi-check';
}
}

const appVersion = config.appVersion;
const downloadUrl = 'https://github.com/sillsdev/languageforge-lexbox/releases';
</script>

<ResponsiveDialog bind:open title={$t`Check for Updates`}>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="text-sm text-muted-foreground">
{$t`Current version:`} <span class="font-mono font-semibold">{appVersion}</span>
</div>
<div class="text-sm text-muted-foreground">
{$t`Platform:`} <span class="font-semibold">{config.os.toString()}</span>
</div>
</div>

{#if updateResult !== null}
<div class="flex items-start gap-3 p-4 rounded-lg bg-muted">
<Icon
icon={getUpdateIcon()}
class={cn('size-6 flex-shrink-0', checking && 'animate-spin')}
/>
<div class="flex-1 text-sm">
{getUpdateMessage()}
</div>
</div>
{/if}

<div class="flex flex-col gap-2">
<Button
onclick={() => void checkForUpdates()}
disabled={checking}
class="w-full"
>
{#if checking}
<Icon icon="i-mdi-loading" class="animate-spin mr-2" />
{:else}
<Icon icon="i-mdi-update" class="mr-2" />
{/if}
{$t`Check for Updates`}
</Button>

{#if updateResult === UpdateResult.ManualUpdateRequired}
<Button
variant="outline"
href={downloadUrl}
target="_blank"
class="w-full"
>
<Icon icon="i-mdi-download" class="mr-2" />
{$t`Go to Download Page`}
</Button>
{/if}
</div>
</div>
</ResponsiveDialog>

<script context="module" lang="ts">
import {cn} from '$lib/utils';
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum DotnetService {
TestingService = "TestingService",
MultiWindowService = "MultiWindowService",
JsEventListener = "JsEventListener",
JsInvokableLogger = "JsInvokableLogger"
JsInvokableLogger = "JsInvokableLogger",
UpdateService = "UpdateService"
}
/* eslint-enable */
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable */
// This code was generated by a Reinforced.Typings tool.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.

export interface IUpdateService
{
checkForUpdates() : Promise<void>;
}
/* eslint-enable */
7 changes: 7 additions & 0 deletions frontend/viewer/src/lib/services/service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {ISyncServiceJsInvokable} from '$lib/dotnet-types/generated-types/Fw
import {useProjectContext} from '../project-context.svelte';
import type {IJsInvokableLogger} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IJsInvokableLogger';

import type {IUpdateService} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IUpdateService';

export type ServiceKey = keyof LexboxServiceRegistry;
export type LexboxServiceRegistry = {
[DotnetService.MiniLcmApi]: IMiniLcmJsInvokable,
Expand All @@ -35,6 +37,7 @@ export type LexboxServiceRegistry = {
[DotnetService.MultiWindowService]: IMultiWindowService,
[DotnetService.JsEventListener]: IJsEventListener,
[DotnetService.JsInvokableLogger]: IJsInvokableLogger,
[DotnetService.UpdateService]: IUpdateService,
};

export const SERVICE_KEYS = Object.values(DotnetService);
Expand Down Expand Up @@ -122,6 +125,10 @@ export function useTroubleshootingService(): ITroubleshootingService | undefined
return window.lexbox.ServiceProvider.tryGetService(DotnetService.TroubleshootingService);
}

export function useUpdateService(): IUpdateService {
return window.lexbox.ServiceProvider.getService(DotnetService.UpdateService);
}

export function useService<K extends ServiceKey>(key: K): LexboxServiceRegistry[K] {
return window.lexbox.ServiceProvider.getService(key);
}
Expand Down