Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 0 additions & 2 deletions frontend/viewer/src/lib/DialogsProvider.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import NewEntryDialog from '$lib/entry-editor/NewEntryDialog.svelte';
import DeleteDialog from '$lib/entry-editor/DeleteDialog.svelte';
import AudioDialog from './components/audio/AudioDialog.svelte';
import {useDialogsService} from '$lib/services/dialogs-service';
import {useProjectContext} from '$lib/project-context.svelte';
const projectContext = useProjectContext();
Expand All @@ -14,6 +13,5 @@

{#if projectContext.maybeApi}
<NewEntryDialog/>
<AudioDialog/>
{/if}
<DeleteDialog bind:this={deleteDialog}/>
40 changes: 17 additions & 23 deletions frontend/viewer/src/lib/components/audio/AudioDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,34 @@
import {t} from 'svelte-i18n-lingui';
import {Button} from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import {useDialogsService} from '$lib/services/dialogs-service.js';
import {useBackHandler} from '$lib/utils/back-handler.svelte';
import {watch} from 'runed';
import AudioProvider from './audio-provider.svelte';
import AudioEditor from './audio-editor.svelte';
import {useLexboxApi} from '$lib/services/service-provider';
import {UploadFileResult} from '$lib/dotnet-types/generated-types/MiniLcm/Media/UploadFileResult';
import {AppNotification} from '$lib/notifications/notifications';

let open = $state(false);
import type {Snippet} from 'svelte';

let {
open = $bindable(false),
title = undefined,
onSubmit = () => {},
children = undefined
} : {
open: boolean,
title?: string,
onSubmit?: (audioId: string) => void,
children?: Snippet
} = $props();
useBackHandler({addToStack: () => open, onBack: () => open = false, key: 'audio-dialog'});
const dialogsService = useDialogsService();
dialogsService.invokeAudioDialog = getAudio;
const lexboxApi = useLexboxApi();

let submitting = $state(false);
let selectedFile = $state<File>();
let finalAudio = $state<File>();
const tooBig = $derived((finalAudio?.size ?? 0) > 10 * 1024 * 1024);

let requester: {
resolve: (mediaUri: string | undefined) => void
} | undefined;


async function getAudio() {
reset();
return new Promise<string | undefined>((resolve) => {
requester = {resolve};
open = true;
});
}

watch(() => open, () => {
if (!open) reset();
});
Expand All @@ -49,8 +44,6 @@
}

function reset() {
requester?.resolve(undefined);
requester = undefined;
clearAudio();
}

Expand All @@ -61,12 +54,11 @@

async function submitAudio() {
if (!selectedFile) throw new Error('No audio to upload');
if (!requester) throw new Error('No requester');

submitting = true;
try {
const audioId = await uploadAudio();
requester.resolve(audioId);
onSubmit(audioId);
close();
} finally {
submitting = false;
Expand All @@ -92,6 +84,7 @@
case UploadFileResult.Error:
throw new Error(response.errorMessage ?? $t`Unknown error`);
}
if (!response.mediaUri) throw new Error($t`No mediaUri returned`);

return response.mediaUri;
}
Expand Down Expand Up @@ -140,8 +133,9 @@
<Dialog.Root bind:open>
<Dialog.DialogContent class="grid-rows-[auto_1fr_auto] sm:min-h-[min(calc(100%-16px),30rem)]">
<Dialog.DialogHeader>
<Dialog.DialogTitle>{$t`Add audio`}</Dialog.DialogTitle>
<Dialog.DialogTitle>{title || $t`Add audio`}</Dialog.DialogTitle>
</Dialog.DialogHeader>
{@render children?.()}
{#if !selectedFile}
<AudioProvider {onFileSelected} {onRecordingComplete}/>
{:else}
Expand Down
33 changes: 23 additions & 10 deletions frontend/viewer/src/lib/components/editor/field/field-root.svelte
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
<script lang="ts" module>
import {Context} from 'runed';

type FieldRootStateProps = {
import type {FieldId} from '$lib/entry-editor/field-data';
const fieldIdSymbol = Symbol('fw-lite-field-id');
class FieldRootState {
//require using the constructor when this type is used
private readonly [fieldIdSymbol] = true;
labelId: string;
};
fieldId? = $state<FieldId>();
label? = $state<string>();

constructor(labelId: string) {
this.labelId = labelId;
}
}

const fieldRootContext = new Context<FieldRootStateProps>('Field.Root');
const fieldRootContext = new Context<FieldRootState>('Field.Root');

export function usesFieldRoot(props: FieldRootStateProps): FieldRootStateProps {
export function usesFieldRoot(props: FieldRootState): FieldRootState {
return fieldRootContext.set(props);
}

type FieldTitleStateProps = FieldRootStateProps;
type FieldTitleStateProps = FieldRootState;

export function useFieldTitle(): FieldTitleStateProps {
return fieldRootContext.get();
}

type FieldBodyStateProps = FieldRootStateProps;
type FieldBodyStateProps = FieldRootState;
export function tryUseFieldBody(): FieldBodyStateProps | undefined {
return fieldRootContext.getOr(undefined);
}
Expand All @@ -28,19 +37,23 @@
import type {WithElementRef} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';

type FieldRootProps = WithElementRef<HTMLAttributes<HTMLDivElement>>;
type FieldRootProps = { fieldId?: FieldId } & WithElementRef<HTMLAttributes<HTMLDivElement>>;

const fieldLabelId = $props.id();
usesFieldRoot({labelId: fieldLabelId});
const fieldProps = usesFieldRoot(new FieldRootState(fieldLabelId));
$effect(() => {
fieldProps.fieldId = fieldId;
});

const {
class: className,
children,
fieldId = undefined,
ref = $bindable(null),
...restProps
}: FieldRootProps = $props();
</script>

<div class={cn('grid grid-cols-subgrid col-span-full items-baseline', className)} {...restProps}>
<div style="grid-area: {fieldId}" class={cn('grid grid-cols-subgrid col-span-full items-baseline', className)} {...restProps}>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
39 changes: 29 additions & 10 deletions frontend/viewer/src/lib/components/field-editors/audio-input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,32 @@
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';
const handled = Symbol();
let {
loader = defaultLoader,
audioId = $bindable(),
onchange = () => {},
readonly = false,
wsLabel = undefined,
}: {
loader?: (audioId: string) => Promise<{stream: ReadableStream, filename: string} | undefined | typeof handled>,
audioId: string | undefined,
onchange?: (audioId: string | undefined) => void;
readonly?: boolean;
wsLabel?: string;
} = $props();
const projectContext = useProjectContext();
const api = $derived(projectContext?.maybeApi);
const supportsAudio = $derived(projectContext?.features.audio);
const dialogService = useDialogsService();
const fieldProps = tryUseFieldBody();
async function defaultLoader(audioId: string) {
if (!api) throw new Error('No api, unable to load audio');
Expand Down Expand Up @@ -217,12 +223,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() {
Expand Down Expand Up @@ -264,11 +268,26 @@
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 subject = useSubjectContext();
</script>
{#if supportsAudio}
{#if !readonly}
<AudioDialog title={dialogTitle} bind:open={audioDialogOpen} onSubmit={onAudioDialogSubmit}>
{#if subject.current}

Check failure on line 277 in frontend/viewer/src/lib/components/field-editors/audio-input.svelte

View workflow job for this annotation

GitHub Actions / check-and-lint

'subject' is possibly 'undefined'.
<OverrideFields shownFields={[fieldProps.fieldId]}>

Check failure on line 278 in frontend/viewer/src/lib/components/field-editors/audio-input.svelte

View workflow job for this annotation

GitHub Actions / check-and-lint

Type '\"lexemeForm\" | \"citationForm\" | \"complexForms\" | \"complexFormTypes\" | \"components\" | \"literalMeaning\" | \"note\" | \"gloss\" | \"definition\" | \"partOfSpeechId\" | \"semanticDomains\" | \"sentence\" | \"translation\" | \"reference\" | undefined' is not assignable to type '\"lexemeForm\" | \"citationForm\" | \"complexForms\" | \"complexFormTypes\" | \"components\" | \"literalMeaning\" | \"note\" | \"gloss\" | \"definition\" | \"partOfSpeechId\" | \"semanticDomains\" | \"sentence\" | \"translation\" | \"reference\"'.\n Type 'undefined' is not assignable to type '\"lexemeForm\" | \"citationForm\" | \"complexForms\" | \"complexFormTypes\" | \"components\" | \"literalMeaning\" | \"note\" | \"gloss\" | \"definition\" | \"partOfSpeechId\" | \"semanticDomains\" | \"sentence\" | \"translation\" | \"reference\"'.

Check failure on line 278 in frontend/viewer/src/lib/components/field-editors/audio-input.svelte

View workflow job for this annotation

GitHub Actions / check-and-lint

'fieldProps' is possibly 'undefined'.
<LexiconEditorPrimitive object={subject.current}/>

Check failure on line 279 in frontend/viewer/src/lib/components/field-editors/audio-input.svelte

View workflow job for this annotation

GitHub Actions / check-and-lint

'subject' is possibly 'undefined'.
</OverrideFields>
{/if}
</AudioDialog>
{/if}
{#if !audioId}
{#if !readonly}
<Button variant="secondary" icon="i-mdi-microphone-plus" size="sm" iconProps={{class: 'size-5'}} onclick={onGetAudioClick}>
<Button variant="secondary"
icon="i-mdi-microphone-plus"
size="sm"
iconProps={{class: 'size-5'}}
onclick={() => audioDialogOpen = true}>
{$t`Add audio`}
</Button>
{:else}
Expand Down Expand Up @@ -322,7 +341,7 @@
</ResponsiveMenu.Trigger>
<ResponsiveMenu.Content>
{#if !readonly}
<ResponsiveMenu.Item icon="i-mdi-microphone-plus" onSelect={onGetAudioClick}>
<ResponsiveMenu.Item icon="i-mdi-microphone-plus" onSelect={() => audioDialogOpen = true}>
{$t`Replace audio`}
</ResponsiveMenu.Item>
<ResponsiveMenu.Item icon="i-mdi-delete" onSelect={onRemoveAudio}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
{:else}
<AudioInput
bind:audioId={value[ws.wsId]} onchange={() => onchange?.(ws.wsId, value[ws.wsId], value)}
wsLabel={ws.abbreviation}
{readonly} />
{/if}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
{:else}
<AudioInput
bind:audioId={() => getAudioId(value[ws.wsId]), audioId => setAudioId(audioId, ws.wsId)}
wsLabel={ws.abbreviation}
{readonly} />
{/if}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<EditorSubGridProps, 'onchange'> {
entry: IEntry;
Expand All @@ -32,14 +33,15 @@
const writingSystemService = useWritingSystemService();
const complexFormTypes = useComplexFormTypes();
const currentView = useCurrentView();
initSubjectContext(() => entry);

function onFieldChanged(field: FieldId) {
onchange?.(entry, field);
}
</script>

<Editor.SubGrid {...mergeProps(rest, { class: 'gap-2', style: { gridTemplateAreas: objectTemplateAreas($currentView, entry) } })}>
<Editor.Field.Root style="grid-area: lexemeForm" class={cn($currentView.fields.lexemeForm.show || 'hidden')}>
<Editor.Field.Root fieldId="lexemeForm" class={cn($currentView.fields.lexemeForm.show || 'hidden')}>
<Editor.Field.Title name={vt($t`Lexeme form`, $t`Word`)} helpId={fieldData.lexemeForm.helpId} />
<Editor.Field.Body subGrid>
<MultiWsInput
Expand All @@ -51,7 +53,7 @@
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root style="grid-area: citationForm" class={cn($currentView.fields.citationForm.show || 'hidden')}>
<Editor.Field.Root fieldId="citationForm" class={cn($currentView.fields.citationForm.show || 'hidden')}>
<Editor.Field.Title name={vt($t`Citation form`, $t`Display as`)} helpId={fieldData.citationForm.helpId} />
<Editor.Field.Body subGrid>
<MultiWsInput
Expand All @@ -63,7 +65,7 @@
</Editor.Field.Root>

{#if !modalMode}
<Editor.Field.Root style="grid-area: complexForms" class={cn($currentView.fields.complexForms.show || 'hidden')}>
<Editor.Field.Root fieldId="complexForms" class={cn($currentView.fields.complexForms.show || 'hidden')}>
<Editor.Field.Title name={vt($t`Complex forms`, $t`Part of`)} helpId={fieldData.complexForms.helpId} />
<Editor.Field.Body>
<ComplexForms onchange={() => onFieldChanged('complexForms')}
Expand All @@ -73,7 +75,7 @@
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root style="grid-area: components" class={cn($currentView.fields.components.show || 'hidden')}>
<Editor.Field.Root fieldId="components" class={cn($currentView.fields.components.show || 'hidden')}>
<Editor.Field.Title name={$t`Components`} helpId={fieldData.components.helpId} />
<Editor.Field.Body>
<ComplexFormComponents
Expand All @@ -84,7 +86,7 @@
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root style="grid-area: complexFormTypes" class={cn($currentView.fields.complexFormTypes.show || 'hidden')}>
<Editor.Field.Root fieldId="complexFormTypes" class={cn($currentView.fields.complexFormTypes.show || 'hidden')}>
<Editor.Field.Title name={vt($t`Complex form types`, $t`Uses components as`)} helpId={fieldData.complexFormTypes.helpId} />
<Editor.Field.Body>
<MultiSelect
Expand All @@ -99,7 +101,7 @@
</Editor.Field.Root>
{/if}

<Editor.Field.Root style="grid-area: literalMeaning" class={cn($currentView.fields.literalMeaning.show || 'hidden')}>
<Editor.Field.Root fieldId="literalMeaning" class={cn($currentView.fields.literalMeaning.show || 'hidden')}>
<Editor.Field.Title name={vt($t`Literal meaning`)} helpId={fieldData.literalMeaning.helpId} />
<Editor.Field.Body subGrid>
<RichMultiWsInput
Expand All @@ -110,7 +112,7 @@
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root style="grid-area: note" class={cn($currentView.fields.note.show || 'hidden')}>
<Editor.Field.Root fieldId="note" class={cn($currentView.fields.note.show || 'hidden')}>
<Editor.Field.Title name={vt($t`Note`)} helpId={fieldData.note.helpId} />
<Editor.Field.Body subGrid>
<RichMultiWsInput
Expand Down
Loading
Loading