From bcbbc80fbc5403ed1031040245e80fb57fd5baea Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 8 Jan 2025 15:38:59 +0700 Subject: [PATCH 01/20] enable FwDataBridge in FwLiteWeb --- backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs index 27b957e92..ad0352435 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs @@ -1,4 +1,6 @@ -using SIL.Harmony; +using FwDataMiniLcmBridge; +using FwLiteProjectSync; +using SIL.Harmony; using FwLiteShared; using FwLiteShared.Auth; using LcmCrdt; @@ -16,6 +18,8 @@ public static IServiceCollection AddFwLiteWebServices(this IServiceCollection se services.AddHttpContextAccessor(); services.AddSingleton(); services.AddSingleton(); + services.AddFwDataBridge(); + services.AddFwLiteProjectSync(); services.AddFwLiteShared(environment); services.AddOptions().BindConfiguration("FwLiteWeb"); From 330cf2355f5c587854f9d9b73dbe9cb2690b9886 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 8 Jan 2025 15:39:37 +0700 Subject: [PATCH 02/20] reword home page for mobile --- frontend/viewer/src/HomeView.svelte | 280 +++++++++++----------------- 1 file changed, 104 insertions(+), 176 deletions(-) diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 9da9937f0..5e7c6005b 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -4,13 +4,21 @@ mdiBookArrowLeftOutline, mdiBookEditOutline, mdiBookPlusOutline, - mdiBookSyncOutline, mdiCloudSync, - mdiLogin, - mdiLogout, + mdiBookSyncOutline, mdiChevronRight, mdiCloud, mdiCloudSync, mdiTestTube, } from '@mdi/js'; - import {links} from 'svelte-routing'; - import {Button, Card, type ColumnDef, Table, TextField, tableCell, Icon, ProgressCircle} from 'svelte-ux'; + import { + Button, + Card, + type ColumnDef, + Table, + TextField, + tableCell, + Icon, + ProgressCircle, + ListItem, + cls + } from 'svelte-ux'; import flexLogo from './lib/assets/flex-logo.png'; import DevContent, {isDev} from './lib/layout/DevContent.svelte'; import {type Project} from './lib/services/projects-service'; @@ -22,14 +30,10 @@ const projectsService = useProjectsService(); const authService = useAuthService(); const importFwdataService = useImportFwdataService(); + const exampleProjectName = 'Example-Project'; - let newProjectName = ''; - - let createError: string; - - async function createProject() { - await projectsService.createProject(newProjectName); - newProjectName = ''; + async function createProject(projectName: string) { + await projectsService.createProject(projectName); await refreshProjects(); } @@ -59,11 +63,11 @@ } } - let projectsPromise = projectsService.localProjects().then(p => projects = p); + let projectsPromise = projectsService.localProjects().then(p => projects = p.sort((p1, p2) => p1.name.localeCompare(p2.name))); let projects: Project[] = []; async function refreshProjects() { - let promise = projectsService.localProjects(); + let promise = projectsService.localProjects().then(p => p.sort((p1, p2) => p1.name.localeCompare(p2.name))); projects = await promise;//avoids clearing out the list until the new list is fetched projectsPromise = promise; } @@ -77,6 +81,7 @@ for (let serverProjects of result) { remoteProjects[serverProjects.server.authority] = serverProjects.projects; } + remoteProjects = remoteProjects; } finally { loadingRemoteProjects = false; } @@ -95,34 +100,9 @@ let serversStatus: IServerStatus[] = []; onMount(async () => { - supportsFwData = await projectsService.supportsFwData(); serversStatus = await authService.servers(); }); - let supportsFwData = false; - $: columns = [ - { - name: 'name', - header: 'Name', - }, - { - name: 'fwdata', - header: 'FieldWorks', - hidden: !supportsFwData, - }, - { - name: 'crdt', - header: 'CRDT', - }, - ...(serversStatus.find(s => s.loggedIn) - ? [ - { - name: 'lexbox', - header: 'Lexbox', - }, - ] - : []), - ] satisfies ColumnDef[]; function matchesProject(projects: Project[], project: Project): Project | undefined { let matches: Project | undefined = undefined; @@ -132,7 +112,7 @@ return matches; } - function syncedServer(serversProjects: { [server: string]: Project[] }, project: Project): ILexboxServer | undefined { + function syncedServer(serversProjects: { [server: string]: Project[] }, project: Project, serversStatus: IServerStatus[]): ILexboxServer | undefined { //this may be null, even if the project is synced, when the project info isn't cached on the server yet. if (project.serverAuthority) { return serversStatus.find(s => s.server.id == project.serverAuthority)?.server ?? { @@ -146,150 +126,98 @@ return authority ? serversStatus.find(s => s.server.authority == authority)?.server : undefined; } - -
- -
- - - - -
-
-
-
-
-
My projects
- -
- {#await projectsPromise} -

loading...

- {:then projects} - p.fwdata || p.crdt).sort((p1, p2) => p1.name.localeCompare(p2.name))} - classes={{ th: 'p-4' }}> - - {#each data ?? [] as project, rowIndex} - - {#each columns as column (column.name)} - - {/each} - - {/each} - - - - - - - - -
- {#if column.name === 'fwdata'} - {#if project.fwdata} - - {/if} - {:else if column.name === 'lexbox'} - {@const server = syncedServer(remoteProjects, project)} - {#if project.crdt && server} - - {/if} - {:else if column.name === 'crdt'} - {#if project.crdt} - - {:else if project.fwdata} - - {/if} - {:else} - {getCellContent(column, project, rowIndex)} - {/if} -
- Test project - - -
- {:catch error} -

Error: {error.message}

- {/await} -
-
Remote projects - {#if loadingRemoteProjects} - - {/if} +
+ +
+ {#await projectsPromise} +

loading...

+ {:then projects} +
+ {#each projects.filter(p => p.crdt) as project (project.id)} + {@const server = syncedServer(remoteProjects, project, serversStatus)} + + +
+
+
+
+ {/each} + + +
+
- {#each serversStatus as status} - {@const server = status.server} -
-
-

{server.displayName}

-
- {#if status.loggedInAs} -

{status.loggedInAs}

- {/if} - refreshProjectsAndServers()} /> + +
+ {#if !projects.some(p => p.name === exampleProjectName)} + createProject(exampleProjectName)}> +
+
+
+ {/if} + {#each serversStatus as status} + {@const server = status.server} +
+

{server.displayName}

+
+ {#if status.loggedInAs} +

{status.loggedInAs}

+ {/if} + refreshProjectsAndServers()}/> +
+ {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []} + {#each serverProjects as project} + {@const localProject = matchesProject(projects, project)} + { if (!localProject?.crdt) {void downloadCrdtProject(project, server);} }} + loading={downloading === project.name}> +
+
- {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []} - {#each serverProjects as project} - {@const localProject = matchesProject(projects, project)} -
-

{project.name}

-
- {#if localProject?.crdt} - - {:else} + + {/each} + {/each} + + {#if projects.some(p => p.fwdata)} +

FieldWorks projects

+ {#each projects.filter(p => p.fwdata) as project (project.id ?? project.name)} + + + FieldWorks logo +
+ - {/if} +
- {/each} - {/each} -
-
- -
-
+ +
+ {/each} + {/if} +
+ {:catch error} +

Error: {error.message}

+ {/await}
+ +
From 1b6039adb26fe46313847edbde89f8568384bf4a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 8 Jan 2025 16:33:03 +0700 Subject: [PATCH 03/20] use dev assets when running a dev build on windows only in maui or fwlite web --- backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs | 2 +- .../FwLite/FwLiteMaui/Platforms/Windows/WindowsKernel.cs | 8 +++++++- backend/FwLite/FwLiteShared/FwLiteConfig.cs | 6 ++++++ backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs | 1 + backend/FwLite/FwLiteShared/Layout/SvelteLayout.razor | 5 +++-- backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs | 4 ++++ 6 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 backend/FwLite/FwLiteShared/FwLiteConfig.cs diff --git a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs index c1438d643..ece6f83aa 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs +++ b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs @@ -41,7 +41,7 @@ public static void AddFwLiteMauiServices(this IServiceCollection services, FwLiteProjectSync.FwLiteProjectSyncKernel.AddFwLiteProjectSync(services); #endif #if WINDOWS - services.AddFwLiteWindows(); + services.AddFwLiteWindows(env); #endif #if ANDROID services.Configure(config => config.ParentActivityOrWindow = Platform.CurrentActivity); diff --git a/backend/FwLite/FwLiteMaui/Platforms/Windows/WindowsKernel.cs b/backend/FwLite/FwLiteMaui/Platforms/Windows/WindowsKernel.cs index c04fa867c..ddf92c248 100644 --- a/backend/FwLite/FwLiteMaui/Platforms/Windows/WindowsKernel.cs +++ b/backend/FwLite/FwLiteMaui/Platforms/Windows/WindowsKernel.cs @@ -1,10 +1,12 @@ using System.Runtime.InteropServices; +using FwLiteShared; +using Microsoft.Extensions.Hosting; namespace FwLiteMaui; public static class WindowsKernel { - public static void AddFwLiteWindows(this IServiceCollection services) + public static void AddFwLiteWindows(this IServiceCollection services, IHostEnvironment environment) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; if (!FwLiteMauiKernel.IsPortableApp) @@ -12,5 +14,9 @@ public static void AddFwLiteWindows(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); } + services.Configure(config => + { + config.UseDevAssets = environment.IsDevelopment(); + }); } } diff --git a/backend/FwLite/FwLiteShared/FwLiteConfig.cs b/backend/FwLite/FwLiteShared/FwLiteConfig.cs new file mode 100644 index 000000000..b7279f20c --- /dev/null +++ b/backend/FwLite/FwLiteShared/FwLiteConfig.cs @@ -0,0 +1,6 @@ +namespace FwLiteShared; + +public class FwLiteConfig +{ + public bool UseDevAssets { get; set; } = false; +} diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs index ef7fe8eec..9c0af4153 100644 --- a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -25,6 +25,7 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service services.AddSingleton(); services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); + services.AddOptions(); return services; } diff --git a/backend/FwLite/FwLiteShared/Layout/SvelteLayout.razor b/backend/FwLite/FwLiteShared/Layout/SvelteLayout.razor index 6b35a9ab6..5f0f58fde 100644 --- a/backend/FwLite/FwLiteShared/Layout/SvelteLayout.razor +++ b/backend/FwLite/FwLiteShared/Layout/SvelteLayout.razor @@ -2,10 +2,11 @@ @using FwLiteShared.Services @using Microsoft.Extensions.Hosting @using Microsoft.Extensions.Logging +@using Microsoft.Extensions.Options @inject IJSRuntime JS @inject ILogger Logger @inject FwLiteProvider FwLiteProvider -@inject IHostEnvironment Environment; +@inject IOptions Config; @implements IAsyncDisposable @if (useDevAssets) { @@ -44,7 +45,7 @@ else @Body @code { - private bool useDevAssets => Environment.IsDevelopment(); + private bool useDevAssets => Config.Value.UseDevAssets; // private bool useDevAssets => false; private IJSObjectReference? module; diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs index ad0352435..133c36a3c 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs @@ -21,6 +21,10 @@ public static IServiceCollection AddFwLiteWebServices(this IServiceCollection se services.AddFwDataBridge(); services.AddFwLiteProjectSync(); services.AddFwLiteShared(environment); + if (environment.IsDevelopment()) + { + services.Configure(config => config.UseDevAssets = true); + } services.AddOptions().BindConfiguration("FwLiteWeb"); From 5334aa812d0e7aa56046df1b2c4b57026825dde5 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 8 Jan 2025 16:48:21 +0700 Subject: [PATCH 04/20] add an app bar to the home page --- frontend/viewer/src/HomeView.svelte | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 5e7c6005b..d1ead74ba 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -9,15 +9,8 @@ } from '@mdi/js'; import { Button, - Card, - type ColumnDef, - Table, - TextField, - tableCell, - Icon, - ProgressCircle, ListItem, - cls + AppBar } from 'svelte-ux'; import flexLogo from './lib/assets/flex-logo.png'; import DevContent, {isDev} from './lib/layout/DevContent.svelte'; @@ -126,6 +119,8 @@ return authority ? serversStatus.find(s => s.server.authority == authority)?.server : undefined; } + +
From 1c391205fc6413aea22c0f3442822a5566dfbc5f Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 8 Jan 2025 17:31:02 +0100 Subject: [PATCH 05/20] Touch up Home Page esp. ListItems --- frontend/viewer/src/HomeView.svelte | 128 ++++++++++-------- .../src/lib/utils/AnchorListItem.svelte | 20 +++ 2 files changed, 90 insertions(+), 58 deletions(-) create mode 100644 frontend/viewer/src/lib/utils/AnchorListItem.svelte diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index d1ead74ba..82b8db27d 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -19,6 +19,7 @@ import {useAuthService, useImportFwdataService, useProjectsService} from './lib/services/service-provider'; import type {ILexboxServer, IServerStatus} from '$lib/dotnet-types'; import LoginButton from '$lib/auth/LoginButton.svelte'; + import AnchorListItem from '$lib/utils/AnchorListItem.svelte'; const projectsService = useProjectsService(); const authService = useAuthService(); @@ -121,85 +122,96 @@ -
+
{#await projectsPromise}

loading...

{:then projects}
- {#each projects.filter(p => p.crdt) as project (project.id)} - {@const server = syncedServer(remoteProjects, project, serversStatus)} - - +
+ {#each projects.filter(p => p.crdt) as project (project.id)} + {@const server = syncedServer(remoteProjects, project, serversStatus)} + + +
+
+
+
+ {/each} + +
-
- {/each} - - -
-
-
-
- {#if !projects.some(p => p.name === exampleProjectName)} - createProject(exampleProjectName)}> -
-
-
- {/if} + + {#if !projects.some(p => p.name === exampleProjectName)} + createProject(exampleProjectName)}> +
+
+
+ {/if} +
{#each serversStatus as status} {@const server = status.server} -
-

{server.displayName}

+ {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []} +
+

Projects on {server.displayName}

{#if status.loggedInAs}

{status.loggedInAs}

{/if} refreshProjectsAndServers()}/>
- {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []} - {#each serverProjects as project} - {@const localProject = matchesProject(projects, project)} - { if (!localProject?.crdt) {void downloadCrdtProject(project, server);} }} - loading={downloading === project.name}> -
- -
-
- {/each} - {/each} - - {#if projects.some(p => p.fwdata)} -

FieldWorks projects

- {#each projects.filter(p => p.fwdata) as project (project.id ?? project.name)} - - - FieldWorks logo + + {/each} + + {#if projects.some(p => p.fwdata)} +

FieldWorks Projects

+
+ {#each projects.filter(p => p.fwdata) as project (project.id ?? project.name)} + + + FieldWorks logo +
+ + + +
+
+
+ {/each} +
{/if}
{:catch error} diff --git a/frontend/viewer/src/lib/utils/AnchorListItem.svelte b/frontend/viewer/src/lib/utils/AnchorListItem.svelte new file mode 100644 index 000000000..7aefc164a --- /dev/null +++ b/frontend/viewer/src/lib/utils/AnchorListItem.svelte @@ -0,0 +1,20 @@ + + + +
+ +
+
+ + From b4339c7f5f1546bf07222d4f771667b2dd4f55c2 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 9 Jan 2025 10:29:25 +0700 Subject: [PATCH 06/20] make home sections more obvious and style the sub-titles --- frontend/viewer/src/HomeView.svelte | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 82b8db27d..5b0ef287a 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -121,14 +121,16 @@ } +
-
+
{#await projectsPromise}

loading...

{:then projects}
+

Local

{#each projects.filter(p => p.crdt) as project (project.id)} {@const server = syncedServer(remoteProjects, project, serversStatus)} @@ -161,7 +163,7 @@ {@const server = status.server} {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []}
-

Projects on {server.displayName}

+

{server.displayName} Server

{#if status.loggedInAs}

{status.loggedInAs}

@@ -170,7 +172,7 @@
{#if status.loggedIn && !serverProjects.length} -

No projects

+

No projects

{/if} {#each serverProjects as project} {@const localProject = matchesProject(projects, project)} @@ -191,7 +193,7 @@ {/each} {#if projects.some(p => p.fwdata)} -

FieldWorks Projects

+

Legacy FieldWorks Projects

{#each projects.filter(p => p.fwdata) as project (project.id ?? project.name)} @@ -227,4 +229,8 @@ display: flex; flex-direction: column; } + .sub-title { + @apply pl-2 mt-5 mb-4; + @apply text-surface-content/50 text-sm; + } From da0cc1c707bf97789594a3d3f7985717cb3c5c4a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 9 Jan 2025 10:31:37 +0700 Subject: [PATCH 07/20] enable injecting FwLiteConfig to pass data into the frontend --- backend/FwLite/FwLiteShared/FwLiteConfig.cs | 12 ++++++++- .../FwLiteShared/Services/FwLiteProvider.cs | 27 ++++++++++++++----- .../TypeGen/ReinforcedFwLiteTypingConfig.cs | 1 + .../FwLiteShared/IFwLiteConfig.ts | 13 +++++++++ .../FwLiteShared/Services/DotnetService.ts | 3 ++- .../lib/services/service-provider-dotnet.ts | 12 ++++++--- .../src/lib/services/service-provider.ts | 6 +++++ 7 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts diff --git a/backend/FwLite/FwLiteShared/FwLiteConfig.cs b/backend/FwLite/FwLiteShared/FwLiteConfig.cs index b7279f20c..7e1db82a9 100644 --- a/backend/FwLite/FwLiteShared/FwLiteConfig.cs +++ b/backend/FwLite/FwLiteShared/FwLiteConfig.cs @@ -1,6 +1,16 @@ -namespace FwLiteShared; +using System.Runtime.InteropServices; + +namespace FwLiteShared; public class FwLiteConfig { public bool UseDevAssets { get; set; } = false; + public string AppVersion { get; set; } = "Unknown"; + public string Os { get; set; } = Environment.OSVersion.Platform switch { + PlatformID.Win32NT => "Windows", + PlatformID.Unix => "Linux", + PlatformID.MacOSX => "Mac", + _ => "Other" + }; + public string FeedbackUrl => $"https://lexbox.org/api/feedback/fw-lite?version={AppVersion}&os={Os}"; } diff --git a/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs b/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs index cfd2ad81a..f862d15bb 100644 --- a/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs +++ b/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs @@ -6,6 +6,7 @@ using LexCore.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; using MiniLcm.Models; using MiniLcm.Project; @@ -20,7 +21,8 @@ public class FwLiteProvider( LexboxProjectService lexboxProjectService, ChangeEventBus changeEventBus, IEnumerable projectProviders, - ILogger logger + ILogger logger, + IOptions config ) : IDisposable { public const string OverrideServiceFunctionName = "setOverrideService"; @@ -50,6 +52,7 @@ public object GetService(DotnetService service) DotnetService.CombinedProjectsService => projectService, DotnetService.AuthService => authService, DotnetService.ImportFwdataService => importFwdataService, + DotnetService.FwLiteConfig => config.Value, _ => throw new ArgumentOutOfRangeException(nameof(service), service, null) }; } @@ -57,19 +60,30 @@ public object GetService(DotnetService service) public async Task SetService(IJSRuntime jsRuntime, DotnetService service, object? serviceInstance) { DotNetObjectReference? reference = null; - if (serviceInstance is not null) + if (serviceInstance is null) { - reference = DotNetObjectReference.Create(serviceInstance); + logger.LogInformation("Clearing Service {Service}", service); } - else + if (ShouldConvertToDotnetObject(service, serviceInstance)) { - logger.LogInformation("Clearing Service {Service}", service); + reference = DotNetObjectReference.Create(serviceInstance); + serviceInstance = reference; } - await jsRuntime.DurableInvokeVoidAsync(OverrideServiceFunctionName, service.ToString(), reference); + await jsRuntime.DurableInvokeVoidAsync(OverrideServiceFunctionName, service.ToString(), serviceInstance); return reference; } + + private bool ShouldConvertToDotnetObject(DotnetService service, [NotNullWhen(true)] object? serviceInstance) + { + return serviceInstance is not null && service switch + { + DotnetService.FwLiteConfig => false, + _ => true + }; + } + public async Task InjectCrdtProject(IJSRuntime jsRuntime, IServiceProvider scopedServices, string projectName) @@ -114,4 +128,5 @@ public enum DotnetService CombinedProjectsService, AuthService, ImportFwdataService, + FwLiteConfig, } diff --git a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs index 13e805cd4..12c2fd76f 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs @@ -77,6 +77,7 @@ public static void Configure(ConfigurationBuilder builder) builder.ExportAsInterface().WithPublicProperties(); builder.ExportAsInterface().WithPublicProperties(); builder.ExportAsInterface().WithPublicProperties(); + builder.ExportAsInterface().WithPublicProperties(); builder.ExportAsEnum(); } diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts new file mode 100644 index 000000000..bd2e12d77 --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts @@ -0,0 +1,13 @@ +/* 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 IFwLiteConfig +{ + useDevAssets: boolean; + appVersion: string; + os: string; + feedbackUrl: string; +} +/* eslint-enable */ diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts index 4edb057a7..14d613cae 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts @@ -7,6 +7,7 @@ export enum DotnetService { MiniLcmApi = "MiniLcmApi", CombinedProjectsService = "CombinedProjectsService", AuthService = "AuthService", - ImportFwdataService = "ImportFwdataService" + ImportFwdataService = "ImportFwdataService", + FwLiteConfig = "FwLiteConfig" } /* eslint-enable */ diff --git a/frontend/viewer/src/lib/services/service-provider-dotnet.ts b/frontend/viewer/src/lib/services/service-provider-dotnet.ts index 264f4c04f..7bdafcf89 100644 --- a/frontend/viewer/src/lib/services/service-provider-dotnet.ts +++ b/frontend/viewer/src/lib/services/service-provider-dotnet.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import './service-declaration'; -import { type DotNet } from '@microsoft/dotnet-js-interop'; +import { DotNet } from '@microsoft/dotnet-js-interop'; import {type LexboxServiceRegistry, SERVICE_KEYS, type ServiceKey} from './service-provider'; export class DotNetServiceProvider { private services: LexboxServiceRegistry; @@ -19,11 +19,17 @@ export class DotNetServiceProvider { public getService(key: K): LexboxServiceRegistry[K] | undefined { this.validateAllServices(); - const service = this.services[key] as unknown as DotNet.DotNetObject; + const service = this.services[key] as LexboxServiceRegistry[K] | DotNet.DotNetObject | undefined; //todo maybe don't return undefined if (!service) return undefined; - return wrapInProxy(service) as LexboxServiceRegistry[K]; + if (this.isDotnetObject(service)) return wrapInProxy(service) as LexboxServiceRegistry[K]; + return service; } + + private isDotnetObject(service: object): service is DotNet.DotNetObject { + return service instanceof DotNet.DotNetObject || 'invokeMethodAsync' in service; + } + private validateAllServices() { const validServiceKeys = SERVICE_KEYS; for (const [key, value] of Object.entries(this.services)) { diff --git a/frontend/viewer/src/lib/services/service-provider.ts b/frontend/viewer/src/lib/services/service-provider.ts index 1f6cbf591..d0deedec4 100644 --- a/frontend/viewer/src/lib/services/service-provider.ts +++ b/frontend/viewer/src/lib/services/service-provider.ts @@ -4,6 +4,7 @@ import {DotnetService, type ICombinedProjectsService, type IAuthService} from '. import type {IImportFwdataService} from '$lib/dotnet-types/generated-types/FwLiteShared/Projects/IImportFwdataService'; import type {IMiniLcmJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMiniLcmJsInvokable'; import {useEventBus} from './event-bus'; +import type {IFwLiteConfig} from '$lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig'; export enum LexboxService { LexboxApi = 'LexboxApi' @@ -14,6 +15,7 @@ export type LexboxServiceRegistry = { [DotnetService.CombinedProjectsService]: ICombinedProjectsService, [DotnetService.AuthService]: IAuthService, [DotnetService.ImportFwdataService]: IImportFwdataService, + [DotnetService.FwLiteConfig]: IFwLiteConfig }; export const SERVICE_KEYS = [...Object.values(LexboxService), ...Object.values(DotnetService)]; @@ -63,3 +65,7 @@ export function useAuthService(): IAuthService { export function useImportFwdataService(): IImportFwdataService { return window.lexbox.ServiceProvider.getService(DotnetService.ImportFwdataService); } + +export function useFwLiteConfig(): IFwLiteConfig { + return window.lexbox.ServiceProvider.getService(DotnetService.FwLiteConfig); +} From 75e040145420f37ce37cd4541b250622670677d4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 9 Jan 2025 10:57:33 +0700 Subject: [PATCH 08/20] provide app version and os to FwLiteConfig --- backend/FwLite/FwLiteMaui/AppVersion.cs | 15 ++---------- backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs | 24 +++++++++++++++++++ backend/FwLite/FwLiteShared/FwLiteConfig.cs | 24 ++++++++++++++----- .../FwLiteShared/Services/VersionHelper.cs | 18 ++++++++++++++ .../TypeGen/ReinforcedFwLiteTypingConfig.cs | 1 + backend/FwLite/FwLiteWeb/FwLiteWebServer.cs | 8 ++++++- .../FwLiteShared/FwLitePlatform.ts | 15 ++++++++++++ .../FwLiteShared/IFwLiteConfig.ts | 4 +++- 8 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 backend/FwLite/FwLiteShared/Services/VersionHelper.cs create mode 100644 frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/FwLitePlatform.ts diff --git a/backend/FwLite/FwLiteMaui/AppVersion.cs b/backend/FwLite/FwLiteMaui/AppVersion.cs index a220d5997..f95cc5c8c 100644 --- a/backend/FwLite/FwLiteMaui/AppVersion.cs +++ b/backend/FwLite/FwLiteMaui/AppVersion.cs @@ -1,20 +1,9 @@ using System.Reflection; +using FwLiteShared.Services; namespace FwLiteMaui; public class AppVersion { - static AppVersion() - { - var infoVersion = typeof(AppVersion).Assembly - .GetCustomAttribute()?.InformationalVersion; - //info version may look like v2024-12-12-3073dd1c+3073dd1ce2ff5510f54a9411366f55c958b9ea45. We want to strip off everything after the +, so we can compare versions - if (infoVersion is not null && infoVersion.Contains('+')) - { - infoVersion = infoVersion[..infoVersion.IndexOf('+')]; - } - Version = infoVersion ?? "dev"; - } - - public static readonly string Version; + public static readonly string Version = VersionHelper.DisplayVersion(typeof(AppVersion).Assembly); } diff --git a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs index ece6f83aa..61dc04d22 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs +++ b/backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs @@ -51,6 +51,30 @@ public static void AddFwLiteMauiServices(this IServiceCollection services, var window = Application.Current?.Windows.FirstOrDefault(); if (window is not null) Application.Current?.ActivateWindow(window); }); + services.Configure(config => + { + config.AppVersion = AppVersion.Version; + if (DeviceInfo.Current.Platform == DevicePlatform.Android) + { + config.Os = FwLitePlatform.Android; + } + else if (DeviceInfo.Current.Platform == DevicePlatform.iOS) + { + config.Os = FwLitePlatform.iOS; + } + else if (DeviceInfo.Current.Platform == DevicePlatform.macOS) + { + config.Os = FwLitePlatform.Mac; + } + else if (DeviceInfo.Current.Platform == DevicePlatform.WinUI) + { + config.Os = FwLitePlatform.Windows; + } + else + { + config.Os = FwLitePlatform.Other; + } + }); var defaultDataPath = IsPortableApp ? Directory.GetCurrentDirectory() : FileSystem.AppDataDirectory; var baseDataPath = Path.GetFullPath(configuration.GetSection("FwLiteMaui").GetValue("BaseDataDir") ?? diff --git a/backend/FwLite/FwLiteShared/FwLiteConfig.cs b/backend/FwLite/FwLiteShared/FwLiteConfig.cs index 7e1db82a9..880cd4d74 100644 --- a/backend/FwLite/FwLiteShared/FwLiteConfig.cs +++ b/backend/FwLite/FwLiteShared/FwLiteConfig.cs @@ -6,11 +6,23 @@ public class FwLiteConfig { public bool UseDevAssets { get; set; } = false; public string AppVersion { get; set; } = "Unknown"; - public string Os { get; set; } = Environment.OSVersion.Platform switch { - PlatformID.Win32NT => "Windows", - PlatformID.Unix => "Linux", - PlatformID.MacOSX => "Mac", - _ => "Other" + public FwLitePlatform Os { get; set; } = Environment.OSVersion.Platform switch { + PlatformID.Win32NT => FwLitePlatform.Windows, + PlatformID.Unix => FwLitePlatform.Linux, + PlatformID.MacOSX => FwLitePlatform.Mac, + _ => FwLitePlatform.Other }; - public string FeedbackUrl => $"https://lexbox.org/api/feedback/fw-lite?version={AppVersion}&os={Os}"; + public string FeedbackUrl => $"https://docs.google.com/forms/d/e/1FAIpQLSdUdNufT3sdoBscY7vixguYnvtgpaw-hjX-z54BKi9KlYv4vw/viewform?usp=pp_url&entry.2102942583={AppVersion}&entry.1772086822={Os}"; +} + +public enum FwLitePlatform +{ + Windows, + Linux, + Mac, + Other, + Android, + // ReSharper disable once InconsistentNaming + iOS, + Web } diff --git a/backend/FwLite/FwLiteShared/Services/VersionHelper.cs b/backend/FwLite/FwLiteShared/Services/VersionHelper.cs new file mode 100644 index 000000000..ae3307aa8 --- /dev/null +++ b/backend/FwLite/FwLiteShared/Services/VersionHelper.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace FwLiteShared.Services; + +public class VersionHelper +{ + public static string DisplayVersion(Assembly assembly) + { + var infoVersion = assembly.GetCustomAttribute()?.InformationalVersion; + //info version may look like v2024-12-12-3073dd1c+3073dd1ce2ff5510f54a9411366f55c958b9ea45. We want to strip off everything after the +, so we can compare versions + if (infoVersion is not null && infoVersion.Contains('+')) + { + infoVersion = infoVersion[..infoVersion.IndexOf('+')]; + } + + return infoVersion ?? "dev"; + } +} diff --git a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs index 12c2fd76f..f7962ebda 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs @@ -78,6 +78,7 @@ public static void Configure(ConfigurationBuilder builder) builder.ExportAsInterface().WithPublicProperties(); builder.ExportAsInterface().WithPublicProperties(); builder.ExportAsInterface().WithPublicProperties(); + builder.ExportAsEnum().UseString(); builder.ExportAsEnum(); } diff --git a/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs b/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs index 34ae69475..1d73fbf24 100644 --- a/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs +++ b/backend/FwLite/FwLiteWeb/FwLiteWebServer.cs @@ -1,6 +1,8 @@ using FwDataMiniLcmBridge; using FwDataMiniLcmBridge.LcmUtils; +using FwLiteShared; using FwLiteShared.Auth; +using FwLiteShared.Services; using LcmCrdt; using FwLiteWeb; using FwLiteWeb.Components; @@ -36,7 +38,11 @@ public static WebApplication SetupAppServer(WebApplicationOptions options, Actio ]); builder.ConfigureProd(config => config.LexboxServers = [new(new("https://staging.languagedepot.org"), "Lexbox Staging")]); - builder.Services.Configure(c => c.ClientId = "becf2856-0690-434b-b192-a4032b72067f"); + builder.Services.Configure(config => + { + config.AppVersion = VersionHelper.DisplayVersion(typeof(FwLiteWebServer).Assembly); + //todo os should be web, when the server is running remotely to the client, but linux runs the server locally so we will default using the OS to determine the platform (the default value) + }); builder.Logging.AddDebug(); builder.Services.AddRazorComponents().AddInteractiveServerComponents(circuitOptions => circuitOptions.DetailedErrors = true); if (builder.Configuration.GetValue("FwLiteWeb:LogFileName") is { Length: > 0 } logFileName) diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/FwLitePlatform.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/FwLitePlatform.ts new file mode 100644 index 000000000..20152bd06 --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/FwLitePlatform.ts @@ -0,0 +1,15 @@ +/* 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 enum FwLitePlatform { + Windows = "Windows", + Linux = "Linux", + Mac = "Mac", + Other = "Other", + Android = "Android", + iOS = "iOS", + Web = "Web" +} +/* eslint-enable */ diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts index bd2e12d77..6dad532af 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/IFwLiteConfig.ts @@ -3,11 +3,13 @@ // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. +import type {FwLitePlatform} from './FwLitePlatform'; + export interface IFwLiteConfig { useDevAssets: boolean; appVersion: string; - os: string; + os: FwLitePlatform; feedbackUrl: string; } /* eslint-enable */ From ee24af3d95f27d11e14ccac08d11764d2936413d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 9 Jan 2025 10:58:51 +0700 Subject: [PATCH 09/20] show feedback button on the home page --- frontend/viewer/src/HomeView.svelte | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 5b0ef287a..2dfcd1b14 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -4,7 +4,7 @@ mdiBookArrowLeftOutline, mdiBookEditOutline, mdiBookPlusOutline, - mdiBookSyncOutline, mdiChevronRight, mdiCloud, mdiCloudSync, + mdiBookSyncOutline, mdiChatQuestion, mdiChevronRight, mdiCloud, mdiCloudSync, mdiTestTube, } from '@mdi/js'; import { @@ -16,7 +16,12 @@ import DevContent, {isDev} from './lib/layout/DevContent.svelte'; import {type Project} from './lib/services/projects-service'; import {onMount} from 'svelte'; - import {useAuthService, useImportFwdataService, useProjectsService} from './lib/services/service-provider'; + import { + useAuthService, + useFwLiteConfig, + useImportFwdataService, + useProjectsService + } from './lib/services/service-provider'; import type {ILexboxServer, IServerStatus} from '$lib/dotnet-types'; import LoginButton from '$lib/auth/LoginButton.svelte'; import AnchorListItem from '$lib/utils/AnchorListItem.svelte'; @@ -24,6 +29,7 @@ const projectsService = useProjectsService(); const authService = useAuthService(); const importFwdataService = useImportFwdataService(); + const fwLiteConfig = useFwLiteConfig(); const exampleProjectName = 'Example-Project'; async function createProject(projectName: string) { @@ -120,8 +126,17 @@ return authority ? serversStatus.find(s => s.server.authority == authority)?.server : undefined; } - +
+ +
From aaee7980a0b5daee5ad8140f37766829b1cb07d8 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 9 Jan 2025 11:06:12 +0700 Subject: [PATCH 10/20] tweak server project styles, instruct users to login to see projects --- frontend/viewer/src/HomeView.svelte | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 2dfcd1b14..a296e8743 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -177,7 +177,7 @@ {#each serversStatus as status} {@const server = status.server} {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []} -
+

{server.displayName} Server

{#if status.loggedInAs} @@ -186,8 +186,14 @@ refreshProjectsAndServers()}/>
- {#if status.loggedIn && !serverProjects.length} -

No projects

+ {#if !serverProjects.length} +

+ {#if status.loggedIn} + No projects + {:else} + Login to see projects + {/if} +

{/if} {#each serverProjects as project} {@const localProject = matchesProject(projects, project)} @@ -203,7 +209,6 @@
{/each} -
{/each} From c273fe87645d6f45d955f8c023d89a5fc9a01f52 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 9 Jan 2025 11:08:58 +0700 Subject: [PATCH 11/20] update feedback button in project view --- frontend/viewer/src/ProjectView.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index d3d5cbab4..df1563ae4 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -11,7 +11,7 @@ import Editor from './lib/Editor.svelte'; import {navigate, useLocation} from 'svelte-routing'; import {headword} from './lib/utils'; - import {useLexboxApi} from './lib/services/service-provider'; + import {useFwLiteConfig, useLexboxApi} from './lib/services/service-provider'; import type {IEntry} from './lib/dotnet-types'; import {onDestroy, onMount, setContext} from 'svelte'; import {derived, type Readable, writable} from 'svelte/store'; @@ -65,6 +65,7 @@ }); })); + const fwLiteConfig = useFwLiteConfig(); const lexboxApi = useLexboxApi(); void lexboxApi.supportedFeatures().then(f => { features.set(f); @@ -340,7 +341,7 @@ {/if} {#if $features.feedback} +{#if status.loggedIn} + + + {:else} {#if $shouldUseSystemWebView}