Skip to content

Commit 99040d8

Browse files
Copilotmyieye
andauthored
Add UI to check for and trigger updates (#2083)
* Initial plan * Add update checking UI with dialog and backend service Co-authored-by: myieye <[email protected]> * Improve event handling to avoid stale events and clarify messages Co-authored-by: myieye <[email protected]> * Expose better api for update checking UI * Refactor UpdateDialog to use new CheckForUpdate/ApplyUpdate API - Removed complex event subscription logic - Use direct API calls: checkForUpdates() returns IAvailableUpdate | null - Use supportsAutoUpdate flag to show Install button vs download link - Simplified state management and UI logic - All functionality contained within dialog (no external notifications) Co-authored-by: myieye <[email protected]> * Simplify UpdateDialog state with promises and implement cache Backend: - Implement 5-minute memory cache for update checks - Cache both "update available" and "no update" results Frontend: - Replace boolean flags (checking, installing, checkComplete, errorMessage) with promise-based state - Use Svelte's {#await} blocks to handle async states declaratively - Remove "Check Again" buttons - main button changes based on state - Use Button's built-in loading and icon props (removed manual mr-2, i-mdi-loading) - Remove unused context="module" script tag and cn import - Fix platform display (removed .toString()) State reduced from 6 variables to 3: - checkPromise: handles check states (pending/fulfilled/rejected) - installPromise: handles install states - installSuccess: tracks successful installation Co-authored-by: myieye <[email protected]> * Use standard .NET MemoryCache instead of custom cache - Replace custom cache fields (_cachedUpdate, _cacheTime) with IMemoryCache - Inject IMemoryCache via constructor dependency injection - Use GetOrCreateAsync pattern for cleaner cache logic - Add AddMemoryCache() to FwLiteSharedKernel service registration - Maintain same 5-minute cache duration behavior Co-authored-by: myieye <[email protected]> * Add FwLiteRelease to AppUpdateEvent * Return UpdateResult * Use actual download URL instead of download page url * Make release downloading reusable * Tweak cache key and duration * Finalize UI and add stories * Add Updates dialog to project view * Add install progress percentage * Fix story naming and location * Fix test failing on demo rejected promise * Prevent showing 0% until we know it works * Add download page link and download update as install fallback * Show platform in troubleshoot dialog * Fix demo page * Remove "or Download Update" It's confusing and there's a link to the app/downloads anyway * Simplify Download page url * Keep po source references sorted * Standardize text anchor styling * PR feedback * Move update calls off of main thread * i18n:extract with added context * Fix lint error --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: myieye <[email protected]> Co-authored-by: Tim Haasdyk <[email protected]>
1 parent a93900b commit 99040d8

44 files changed

Lines changed: 1430 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/FwLite/FwLiteMaui/Platforms/Windows/AppUpdateService.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
using System.Buffers;
2-
using System.Net.Http.Json;
31
using System.Text.Json;
42
using Windows.Management.Deployment;
53
using Windows.Networking.Connectivity;
64
using LexCore.Entities;
75
using Microsoft.Extensions.Logging;
86
using Microsoft.Toolkit.Uwp.Notifications;
9-
using FwLiteMaui.Services;
107
using FwLiteShared.AppUpdate;
8+
using FwLiteShared.Events;
119

1210
namespace FwLiteMaui;
1311

14-
public class AppUpdateService(ILogger<AppUpdateService> logger, IPreferences preferences)
12+
public class AppUpdateService(ILogger<AppUpdateService> logger, IPreferences preferences, GlobalEventBus eventBus)
1513
: IMauiInitializeService, IPlatformUpdateService
1614
{
1715
private const string LastUpdateCheckKey = "lastUpdateChecked";
@@ -118,6 +116,7 @@ private async Task<UpdateResult> ApplyUpdate(FwLiteRelease latestRelease, bool q
118116
});
119117
asyncOperation.Progress = (info, progressInfo) =>
120118
{
119+
NotifyInstallProgress(progressInfo.percentage, latestRelease);
121120
if (progressInfo.state == DeploymentProgressState.Queued)
122121
{
123122
logger.LogInformation("Queued update");
@@ -146,6 +145,11 @@ private async Task<UpdateResult> ApplyUpdate(FwLiteRelease latestRelease, bool q
146145
return UpdateResult.Started;
147146
}
148147

148+
private void NotifyInstallProgress(uint percentage, FwLiteRelease release)
149+
{
150+
eventBus.PublishEvent(new AppUpdateProgressEvent(percentage, release));
151+
}
152+
149153
public DateTime LastUpdateCheck
150154
{
151155
get => preferences.Get(LastUpdateCheckKey, DateTime.MinValue);

backend/FwLite/FwLiteShared/AppUpdate/IPlatformUpdateService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ public enum UpdateResult
1919
Success,
2020
Failed,
2121
Started,
22-
ManualUpdateRequired
22+
ManualUpdateRequired,
23+
Disallowed
2324
}

backend/FwLite/FwLiteShared/AppUpdate/UpdateChecker.cs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,73 @@
11
using System.Net.Http.Json;
22
using FwLiteShared.Events;
33
using LexCore.Entities;
4+
using Microsoft.Extensions.Caching.Memory;
45
using Microsoft.Extensions.Hosting;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.Extensions.Options;
78

89
namespace FwLiteShared.AppUpdate;
910

11+
public record AvailableUpdate(FwLiteRelease Release, bool SupportsAutoUpdate);
12+
1013
public class UpdateChecker(
1114
IHttpClientFactory httpClientFactory,
1215
ILogger<UpdateChecker> logger,
1316
IOptions<FwLiteConfig> config,
1417
GlobalEventBus eventBus,
15-
IPlatformUpdateService platformUpdateService): BackgroundService
18+
IPlatformUpdateService platformUpdateService,
19+
IMemoryCache cache) : BackgroundService
1620
{
21+
private const string CacheKey = "ManualUpdateCheck";
22+
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(2);
23+
1724
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
1825
{
1926
await TryUpdate();
2027
}
2128

22-
public async Task TryUpdate(bool forceCheck = false)
29+
public async Task<UpdateResult?> TryUpdate()
30+
{
31+
if (!ShouldCheckForUpdate()) return null;
32+
var update = await CheckForUpdate();
33+
if (update is null) return null;
34+
return await ApplyUpdate(update.Release);
35+
}
36+
37+
public async Task<AvailableUpdate?> CheckForUpdate()
2338
{
24-
if (!ShouldCheckForUpdate() && !forceCheck) return;
25-
var response = await ShouldUpdateAsync();
39+
return await cache.GetOrCreateAsync(CacheKey, async entry =>
40+
{
41+
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
42+
var response = await ShouldUpdateAsync();
43+
platformUpdateService.LastUpdateCheck = DateTime.UtcNow;
44+
return response.Update
45+
? new AvailableUpdate(response.Release, platformUpdateService.SupportsAutoUpdate)
46+
: null;
47+
});
48+
}
2649

27-
platformUpdateService.LastUpdateCheck = DateTime.UtcNow;
28-
if (!response.Update) return;
50+
public async Task<UpdateResult> ApplyUpdate(FwLiteRelease release)
51+
{
2952
if (ShouldPromptBeforeUpdate() &&
30-
!await platformUpdateService.RequestPermissionToUpdate(response.Release))
53+
!await platformUpdateService.RequestPermissionToUpdate(release))
3154
{
32-
return;
55+
return UpdateResult.Disallowed;
3356
}
3457

35-
UpdateResult updateResult = UpdateResult.ManualUpdateRequired;
58+
var updateResult = UpdateResult.ManualUpdateRequired;
3659
if (platformUpdateService.SupportsAutoUpdate)
3760
{
38-
updateResult = await platformUpdateService.ApplyUpdate(response.Release);
61+
updateResult = await platformUpdateService.ApplyUpdate(release);
3962
}
4063

41-
NotifyResult(updateResult);
64+
NotifyResult(updateResult, release);
65+
return updateResult;
4266
}
4367

44-
private void NotifyResult(UpdateResult result)
68+
private void NotifyResult(UpdateResult result, FwLiteRelease release)
4569
{
46-
eventBus.PublishEvent(new AppUpdateEvent(result));
70+
eventBus.PublishEvent(new AppUpdateEvent(result, release));
4771
}
4872

4973
private bool ShouldCheckForUpdate()
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using FwLiteShared.AppUpdate;
2+
using LexCore.Entities;
23

34
namespace FwLiteShared.Events;
45

5-
public class AppUpdateEvent(UpdateResult result) : IFwEvent
6+
public class AppUpdateEvent(UpdateResult result, FwLiteRelease release) : IFwEvent
67
{
78
public UpdateResult Result { get; } = result;
9+
public FwLiteRelease Release { get; } = release;
810
public FwEventType Type => FwEventType.AppUpdate;
911
public bool IsGlobal => true;
1012
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using LexCore.Entities;
2+
3+
namespace FwLiteShared.Events;
4+
5+
public class AppUpdateProgressEvent(uint percentage, FwLiteRelease release) : IFwEvent
6+
{
7+
public uint Percentage { get; } = percentage;
8+
public FwLiteRelease Release { get; } = release;
9+
public FwEventType Type => FwEventType.AppUpdateProgress;
10+
public bool IsGlobal => true;
11+
}

backend/FwLite/FwLiteShared/Events/IFwEvent.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace FwLiteShared.Events;
99
[JsonDerivedType(typeof(AuthenticationChangedEvent), nameof(AuthenticationChangedEvent))]
1010
[JsonDerivedType(typeof(SyncEvent), nameof(SyncEvent))]
1111
[JsonDerivedType(typeof(AppUpdateEvent), nameof(AppUpdateEvent))]
12+
[JsonDerivedType(typeof(AppUpdateProgressEvent), nameof(AppUpdateProgressEvent))]
1213
public interface IFwEvent
1314
{
1415
FwEventType Type { get; }
@@ -25,4 +26,5 @@ public enum FwEventType
2526
EntryDeleted,
2627
Sync,
2728
AppUpdate,
29+
AppUpdateProgress,
2830
}

backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static class FwLiteSharedKernel
2222
{
2323
public static IServiceCollection AddFwLiteShared(this IServiceCollection services, IHostEnvironment environment)
2424
{
25+
services.AddMemoryCache();
2526
services.AddHttpClient();
2627
services.AddAuthHelpers(environment);
2728
services.AddLcmCrdtClient();
@@ -46,6 +47,7 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service
4647
services.AddSingleton<UpdateChecker>();
4748
services.AddSingleton<IHostedService>(s => s.GetRequiredService<UpdateChecker>());
4849
services.TryAddSingleton<IPlatformUpdateService, CorePlatformUpdateService>();
50+
services.AddSingleton<UpdateService>();
4951
services.AddSingleton<TestingService>();
5052
services.AddOptions<FwLiteConfig>().BindConfiguration("FwLite");
5153
services.DecorateConstructor<IJSRuntime>((provider, runtime) =>

backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ IServiceProvider services
3535
DotnetService.MultiWindowService,
3636
DotnetService.JsEventListener,
3737
DotnetService.JsInvokableLogger,
38+
DotnetService.UpdateService,
3839
];
3940

4041
public static Type GetServiceType(DotnetService service) => service switch
@@ -53,6 +54,7 @@ IServiceProvider services
5354
DotnetService.MultiWindowService => typeof(IMultiWindowService),
5455
DotnetService.JsEventListener => typeof(JsEventListener),
5556
DotnetService.JsInvokableLogger => typeof(JsInvokableLogger),
57+
DotnetService.UpdateService => typeof(UpdateService),
5658
_ => throw new ArgumentOutOfRangeException(nameof(service), service, null)
5759
};
5860

@@ -111,4 +113,5 @@ public enum DotnetService
111113
MultiWindowService,
112114
JsEventListener,
113115
JsInvokableLogger,
116+
UpdateService,
114117
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using FwLiteShared.AppUpdate;
2+
using Microsoft.JSInterop;
3+
using Reinforced.Typings.Attributes;
4+
5+
namespace FwLiteShared.Services;
6+
7+
public class UpdateService(UpdateChecker updateChecker)
8+
{
9+
[JSInvokable]
10+
[TsFunction(Type = "Promise<IAvailableUpdate | null>")]
11+
public Task<AvailableUpdate?> CheckForUpdates()
12+
{
13+
return Task.Run(async () => await updateChecker.CheckForUpdate());
14+
}
15+
16+
[JSInvokable]
17+
public Task<UpdateResult> ApplyUpdate(AvailableUpdate update)
18+
{
19+
return Task.Run(async () => await updateChecker.ApplyUpdate(update.Release));
20+
}
21+
}

backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ private static void ConfigureFwLiteSharedTypes(ConfigurationBuilder builder)
174174
typeof(IChange),
175175
typeof(CommitMetadata),
176176
typeof(ObjectSnapshot),
177-
typeof(ProjectScope)
177+
typeof(ProjectScope),
178+
typeof(FwLiteRelease),
179+
typeof(AvailableUpdate),
178180
], exportBuilder => exportBuilder.WithPublicProperties());
179181

180182
builder.ExportAsEnum<FwEventType>().UseString();

0 commit comments

Comments
 (0)