From 4b367c9f321ea55abddaf3e40b24dc1a0737c5e6 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 3 Sep 2025 13:17:29 +0700 Subject: [PATCH 01/13] fix dev tools dialog not working because mini lcm api might not be created yet --- frontend/viewer/src/lib/layout/DevToolsDialog.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/viewer/src/lib/layout/DevToolsDialog.svelte b/frontend/viewer/src/lib/layout/DevToolsDialog.svelte index 46906053b5..c21d902be4 100644 --- a/frontend/viewer/src/lib/layout/DevToolsDialog.svelte +++ b/frontend/viewer/src/lib/layout/DevToolsDialog.svelte @@ -2,13 +2,13 @@ import { Button } from '$lib/components/ui/button'; import * as Dialog from '$lib/components/ui/dialog'; import { Icon } from '$lib/components/ui/icon'; - import { useMiniLcmApi } from '$lib/services/service-provider'; import { defaultEntry } from '$lib/utils'; import { useWritingSystemService } from '$lib/writing-system-service.svelte'; import type { Snippet } from 'svelte'; import DevContent from './DevContent.svelte'; + import {useProjectContext} from '$lib/project-context.svelte'; - const api = useMiniLcmApi(); + const projectContext = useProjectContext(); const writingSystems = useWritingSystemService(); type Props = { @@ -22,7 +22,7 @@ const entry = defaultEntry(); const vWsId = writingSystems.defaultVernacular?.wsId; if (vWsId) entry.citationForm[vWsId] = `*Test ${Math.random().toString(36).substring(2, 7)}`; - await api.createEntry(entry); + await projectContext.api.createEntry(entry); } } From 00a2e582e37a2b53784a359f91e65116c562fdee Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 3 Sep 2025 13:49:47 +0700 Subject: [PATCH 02/13] don't crash when loading the browse view with an entry selected --- frontend/viewer/src/project/browse/BrowseView.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/viewer/src/project/browse/BrowseView.svelte b/frontend/viewer/src/project/browse/BrowseView.svelte index eda060de17..07aa47a100 100644 --- a/frontend/viewer/src/project/browse/BrowseView.svelte +++ b/frontend/viewer/src/project/browse/BrowseView.svelte @@ -18,7 +18,9 @@ import IfOnce from '$lib/components/if-once/if-once.svelte'; import {SortField} from '$lib/dotnet-types'; import SortMenu, {type SortConfig} from './SortMenu.svelte'; + import {useProjectContext} from '$lib/project-context.svelte'; + const projectContext = useProjectContext(); const currentView = useCurrentView(); const dialogsService = useDialogsService(); const selectedEntryId = new QueryParamState({key: 'entryId', allowBack: true, replaceOnDefaultValue: true}); @@ -101,7 +103,7 @@

{$t`Select ${pt($t`an entry`, $t`a word`, $currentView)} to view details`}

- {:else} + {:else if projectContext.maybeApi}
Date: Wed, 3 Sep 2025 13:55:42 +0700 Subject: [PATCH 03/13] put fieldId and label into `FieldRootState` context, use new fieldId prop on primitive editors. --- .../components/editor/field/field-root.svelte | 33 +++++++++++++------ .../editor/field/field-title.svelte | 3 ++ .../EntryEditorPrimitive.svelte | 14 ++++---- .../ExampleEditorPrimitive.svelte | 6 ++-- .../SenseEditorPrimitive.svelte | 8 ++--- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/frontend/viewer/src/lib/components/editor/field/field-root.svelte b/frontend/viewer/src/lib/components/editor/field/field-root.svelte index 50bfc06336..c6e32a03a0 100644 --- a/frontend/viewer/src/lib/components/editor/field/field-root.svelte +++ b/frontend/viewer/src/lib/components/editor/field/field-root.svelte @@ -1,23 +1,32 @@ -
+
{@render children?.()}
diff --git a/frontend/viewer/src/lib/components/editor/field/field-title.svelte b/frontend/viewer/src/lib/components/editor/field/field-title.svelte index 61d2b0e9c3..56e656853f 100644 --- a/frontend/viewer/src/lib/components/editor/field/field-title.svelte +++ b/frontend/viewer/src/lib/components/editor/field/field-title.svelte @@ -17,6 +17,9 @@ const view = useCurrentView(); const label = $derived(pickViewText(name, $view.type)); + $effect(() => { + stateProps.label = label; + }); const title = $derived(typeof name === 'string' ? undefined : $view.type === 'fw-classic' ? $t`${name.lite} (FieldWorks Lite)` diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte index 75e91bf653..5fea39c93b 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte @@ -39,7 +39,7 @@ - + - + {#if !modalMode} - + onFieldChanged('complexForms')} @@ -73,7 +73,7 @@ - + - + {/if} - + - + - + - + {#if writingSystemService.defaultAnalysis} - + - + - + - + + + +
From 8610d28323a3adec32588ec3317ef397891d7747 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 8 Sep 2025 15:41:42 +0200 Subject: [PATCH 13/13] Only show relevant fields as audio dialog context --- .../MiniLcm.Tests/WritingSystemIdTests.cs | 12 +++++++ .../FwLite/MiniLcm/Models/WritingSystem.cs | 1 + .../FwLite/MiniLcm/Models/WritingSystemId.cs | 15 ++++++++- .../field-editors/audio-input.svelte | 31 ++++++++++++++++--- .../field-editors/multi-ws-input.svelte | 2 +- .../field-editors/rich-multi-ws-input.svelte | 2 +- .../MiniLcm/Models/IWritingSystem.ts | 1 + 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs b/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs index 6ced2a6b15..661453c40c 100644 --- a/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs +++ b/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs @@ -26,6 +26,18 @@ public void DetectsAudioWritingSystems(string code) ws.IsAudio.Should().BeTrue(); } + [Theory] + [InlineData("en-Zxxx-x-audio", "en")] + [InlineData("seh-Zxxx-x-audio-var", "seh-x-var")] + [InlineData("lwl-Zxxx-x-majority-audio", "lwl-x-majority")] + [InlineData("lwl-Latn-x-majority", "lwl-x-majority")] + [InlineData("lwl-Latn", "lwl")] + public void CalculatesScriptlessEquivalent(string audioCode, string expectedScriptlessCode) + { + var ws = new WritingSystemId(audioCode); + ws.CodeWithoutScriptOrAudio.Should().Be(expectedScriptlessCode); + } + [Theory] [InlineData("gx")] [InlineData("oo")] diff --git a/backend/FwLite/MiniLcm/Models/WritingSystem.cs b/backend/FwLite/MiniLcm/Models/WritingSystem.cs index 2efbde8b8c..ffda5af8eb 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystem.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystem.cs @@ -11,6 +11,7 @@ public record WritingSystem: IObjectWithId, IOrderableNoId public required Guid Id { get; set; } public virtual required WritingSystemId WsId { get; set; } public bool IsAudio => WsId.IsAudio; + public string CodeWithoutScriptOrAudio => WsId.CodeWithoutScriptOrAudio ?? WsId.Code; public virtual required string Name { get; set; } public virtual required string Abbreviation { get; set; } public virtual required string Font { get; set; } diff --git a/backend/FwLite/MiniLcm/Models/WritingSystemId.cs b/backend/FwLite/MiniLcm/Models/WritingSystemId.cs index 79b8e65a2c..ec964e9459 100644 --- a/backend/FwLite/MiniLcm/Models/WritingSystemId.cs +++ b/backend/FwLite/MiniLcm/Models/WritingSystemId.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using SIL.WritingSystems; @@ -35,6 +34,7 @@ public override void WriteAsPropertyName(Utf8JsonWriter writer, WritingSystemId { public string Code { get => field ?? "default"; init; } public bool IsAudio { get; } = false; + public string? CodeWithoutScriptOrAudio { get; } public static readonly WritingSystemId Default = "default"; @@ -62,6 +62,19 @@ public WritingSystemId(string code) Code = code; IsAudio = script?.Equals(WellKnownSubtags.AudioScript, StringComparison.OrdinalIgnoreCase) == true && variants?.Split('-').Any(v => v == WellKnownSubtags.AudioPrivateUse) == true; + + if ((script is not null || IsAudio) && IetfLanguageTag.TryGetSubtags(code, + out var langSubtag, + out var scriptSubtag, + out var regionSubtag, + out var variantsSubtags)) + { + CodeWithoutScriptOrAudio = IetfLanguageTag.Create( + langSubtag, + null, // remove script (e.g. Zxxxx (unwritten/audio), Latn, etc.) + regionSubtag, + variantsSubtags.Where(v => v.Code != WellKnownSubtags.AudioPrivateUse)); + } } else { diff --git a/frontend/viewer/src/lib/components/field-editors/audio-input.svelte b/frontend/viewer/src/lib/components/field-editors/audio-input.svelte index 218c88da16..caa0f81be5 100644 --- a/frontend/viewer/src/lib/components/field-editors/audio-input.svelte +++ b/frontend/viewer/src/lib/components/field-editors/audio-input.svelte @@ -56,6 +56,10 @@ import {useSubjectContext} from '$lib/entry-editor/object-editors/subject-context'; import LexiconEditorPrimitive from '$lib/entry-editor/object-editors/LexiconEditorPrimitive.svelte'; import OverrideFields from '$lib/OverrideFields.svelte'; + import {WritingSystemType, type IWritingSystem} from '$lib/dotnet-types'; + import type {ReadonlyDeep} from 'type-fest'; + import {useWritingSystemService} from '$lib/writing-system-service.svelte'; + import type {Overrides} from '$lib/views/view-data'; const handled = Symbol(); let { @@ -63,18 +67,35 @@ audioId = $bindable(), onchange = () => {}, readonly = false, - wsLabel = undefined, + ws = undefined, }: { loader?: (audioId: string) => Promise<{stream: ReadableStream, filename: string} | undefined | typeof handled>, audioId: string | undefined, onchange?: (audioId: string | undefined) => void; readonly?: boolean; - wsLabel?: string; + ws?: ReadonlyDeep; } = $props(); const projectContext = useProjectContext(); const api = $derived(projectContext?.maybeApi); const supportsAudio = $derived(projectContext?.features.audio); + const writingSystems = useWritingSystemService(); + const overrides: Overrides = $derived.by(() => { + if (!ws) return {}; + if (ws.type === WritingSystemType.Analysis) { + return { + analysisWritingSystems: writingSystems.analysisNoAudio + .filter(w => w.codeWithoutScriptOrAudio === ws.codeWithoutScriptOrAudio) + .map(w => w.wsId), + }; + } else { + return { + vernacularWritingSystems: writingSystems.vernacularNoAudio + .filter(w => w.codeWithoutScriptOrAudio === ws.codeWithoutScriptOrAudio) + .map(w => w.wsId), + }; + } + }); const fieldProps = tryUseFieldBody(); async function defaultLoader(audioId: string) { @@ -268,15 +289,15 @@ return audio.error.code === MediaError.MEDIA_ERR_NETWORK && audio.error.message?.includes('demuxer seek failed'); } - let dialogTitle = $derived(fieldProps?.label && wsLabel ? `${fieldProps.label}: ${wsLabel}` : fieldProps?.label || wsLabel); + let dialogTitle = $derived(fieldProps?.label && ws?.abbreviation ? `${fieldProps.label}: ${ws.abbreviation}` : fieldProps?.label || ws?.abbreviation); let subject = useSubjectContext(); {#if supportsAudio} {#if !readonly} {#if subject?.current} - - + + {/if} diff --git a/frontend/viewer/src/lib/components/field-editors/multi-ws-input.svelte b/frontend/viewer/src/lib/components/field-editors/multi-ws-input.svelte index b88bb3a33c..57cc9e628f 100644 --- a/frontend/viewer/src/lib/components/field-editors/multi-ws-input.svelte +++ b/frontend/viewer/src/lib/components/field-editors/multi-ws-input.svelte @@ -52,7 +52,7 @@ {:else} onchange?.(ws.wsId, value[ws.wsId], value)} - wsLabel={ws.abbreviation} + {ws} {readonly} /> {/if}
diff --git a/frontend/viewer/src/lib/components/field-editors/rich-multi-ws-input.svelte b/frontend/viewer/src/lib/components/field-editors/rich-multi-ws-input.svelte index 51678e15d0..ada0d485b9 100644 --- a/frontend/viewer/src/lib/components/field-editors/rich-multi-ws-input.svelte +++ b/frontend/viewer/src/lib/components/field-editors/rich-multi-ws-input.svelte @@ -77,7 +77,7 @@ {:else} getAudioId(value[ws.wsId]), audioId => setAudioId(audioId, ws.wsId)} - wsLabel={ws.abbreviation} + {ws} {readonly} /> {/if} diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts index 735af80459..36da9d7ef8 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IWritingSystem.ts @@ -11,6 +11,7 @@ export interface IWritingSystem extends IObjectWithId id: string; wsId: string; isAudio: boolean; + codeWithoutScriptOrAudio: string; name: string; abbreviation: string; font: string;