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/DialogsProvider.svelte b/frontend/viewer/src/lib/DialogsProvider.svelte index 0245dbe065..94cf81e3b2 100644 --- a/frontend/viewer/src/lib/DialogsProvider.svelte +++ b/frontend/viewer/src/lib/DialogsProvider.svelte @@ -1,7 +1,6 @@ -
+
{@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/components/field-editors/audio-input.svelte b/frontend/viewer/src/lib/components/field-editors/audio-input.svelte index 1d77897317..caa0f81be5 100644 --- a/frontend/viewer/src/lib/components/field-editors/audio-input.svelte +++ b/frontend/viewer/src/lib/components/field-editors/audio-input.svelte @@ -50,8 +50,16 @@ import {formatDuration, normalizeDuration} from '$lib/components/ui/format'; import {t} from 'svelte-i18n-lingui'; import {ReadFileResult} from '$lib/dotnet-types/generated-types/MiniLcm/Media/ReadFileResult'; - import {useDialogsService} from '$lib/services/dialogs-service'; import * as ResponsiveMenu from '$lib/components/responsive-menu'; + import AudioDialog from '$lib/components/audio/AudioDialog.svelte'; + import {tryUseFieldBody} from '$lib/components/editor/field/field-root.svelte'; + 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 { @@ -59,17 +67,36 @@ audioId = $bindable(), onchange = () => {}, readonly = false, + ws = undefined, }: { loader?: (audioId: string) => Promise<{stream: ReadableStream, filename: string} | undefined | typeof handled>, audioId: string | undefined, onchange?: (audioId: string | undefined) => void; readonly?: boolean; + ws?: ReadonlyDeep; } = $props(); const projectContext = useProjectContext(); const api = $derived(projectContext?.maybeApi); const supportsAudio = $derived(projectContext?.features.audio); - const dialogService = useDialogsService(); + 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) { if (!api) throw new Error('No api, unable to load audio'); @@ -217,12 +244,10 @@ }); let smallestUnit = $derived(totalLength.minutes > 0 ? 'seconds' as const : 'milliseconds' as const); - async function onGetAudioClick() { - const result = await dialogService.getAudio(); - if (result) { - audioId = result; - onchange(audioId) - } + let audioDialogOpen = $state(false); + function onAudioDialogSubmit(newAudioId: string) { + audioId = newAudioId; + onchange(newAudioId); } function onRemoveAudio() { @@ -264,11 +289,26 @@ return audio.error.code === MediaError.MEDIA_ERR_NETWORK && audio.error.message?.includes('demuxer seek failed'); } + 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} + + {/if} {#if !audioId} {#if !readonly} - {:else} @@ -322,7 +362,7 @@ {#if !readonly} - + audioDialogOpen = true}> {$t`Replace audio`} 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 c8dee3b26f..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,6 +52,7 @@ {:else} onchange?.(ws.wsId, value[ws.wsId], value)} + {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 ffa5882ba9..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,6 +77,7 @@ {:else} getAudioId(value[ws.wsId]), audioId => setAudioId(audioId, ws.wsId)} + {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; 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..5ac1a20ece 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditorPrimitive.svelte @@ -13,6 +13,7 @@ import ComplexForms from '../field-editors/ComplexForms.svelte'; import type {EditorSubGridProps} from '$lib/components/editor/editor-sub-grid.svelte'; import {mergeProps} from 'bits-ui'; + import {initSubjectContext} from '$lib/entry-editor/object-editors/subject-context'; interface Props extends Omit { entry: IEntry; @@ -32,6 +33,7 @@ const writingSystemService = useWritingSystemService(); const complexFormTypes = useComplexFormTypes(); const currentView = useCurrentView(); + initSubjectContext(() => entry); function onFieldChanged(field: FieldId) { onchange?.(entry, field); @@ -39,7 +41,7 @@ - + - + {#if !modalMode} - + onFieldChanged('complexForms')} @@ -73,7 +75,7 @@ - + - + {/if} - + - + { example: IExampleSentence; @@ -26,6 +27,7 @@ const writingSystemService = useWritingSystemService(); const currentView = useCurrentView(); + initSubjectContext(() => example); function onFieldChanged(field: FieldId) { onchange?.(example, field); @@ -33,7 +35,7 @@ - + - + {#if writingSystemService.defaultAnalysis} - + + import type {IEntry, IExampleSentence, ISense} from '$lib/dotnet-types'; + import {isEntry, isExample, isSense} from '$lib/utils'; + import EntryEditorPrimitive from '$lib/entry-editor/object-editors/EntryEditorPrimitive.svelte'; + import SenseEditorPrimitive from '$lib/entry-editor/object-editors/SenseEditorPrimitive.svelte'; + import ExampleEditorPrimitive from '$lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte'; + import * as Editor from '$lib/components/editor'; + + let { + object + }: { + object: IEntry | ISense | IExampleSentence | undefined + } = $props(); + + + + + {#if isEntry(object)} + + {:else if isSense(object)} + + {:else if isExample(object)} + + {/if} + + diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte index 2c21054bb9..f1ceb9ddd7 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte @@ -12,6 +12,7 @@ import {cn} from '$lib/utils'; import type {EditorSubGridProps} from '$lib/components/editor/editor-sub-grid.svelte'; import {mergeProps} from 'bits-ui'; + import {initSubjectContext} from '$lib/entry-editor/object-editors/subject-context'; interface Props extends Omit { sense: ISense; @@ -30,14 +31,14 @@ const partsOfSpeech = usePartsOfSpeech(); const semanticDomains = useSemanticDomains(); const currentView = useCurrentView(); - + initSubjectContext(() => sense); function onFieldChanged(field: FieldId) { onchange?.(sense, field); } - + - + - +