// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cros_components/dropdown/dropdown_option.js';
import './cra/cra-button.js';
import './cra/cra-dialog.js';
import './cra/cra-dropdown.js';
import './expandable-card.js';
import {
createRef,
css,
CSSResultGroup,
html,
map,
nothing,
PropertyDeclarations,
ref,
} from 'chrome://resources/mwc/lit/index.js';
import {i18n} from '../core/i18n.js';
import {useRecordingDataManager} from '../core/lit/context.js';
import {
ReactiveLitElement,
ScopedAsyncComputed,
} from '../core/reactive/lit.js';
import {computed} from '../core/reactive/signal.js';
import {
ExportAudioFormat,
ExportTranscriptionFormat,
settings,
} from '../core/state/settings.js';
import {
assertEnumVariant,
assertInstanceof,
assertString,
} from '../core/utils/assert.js';
import {AsyncJobQueue} from '../core/utils/async_job_queue.js';
import {CraDialog} from './cra/cra-dialog.js';
import {CraDropdown} from './cra/cra-dropdown.js';
interface DropdownOption<T extends string> {
headline: string;
value: T;
}
export class ExportDialog extends ReactiveLitElement {
static override styles: CSSResultGroup = css`
:host {
display: contents;
}
cra-dialog {
width: 440px;
& > [slot="content"] {
display: flex;
flex-flow: column;
gap: 16px;
padding-bottom: 12px;
padding-top: 24px;
}
& > [slot="actions"] {
padding-top: 12px;
}
}
.header {
color: var(--cros-sys-on_surface);
font: var(--cros-headline-1-font);
}
cros-dropdown-option cra-icon {
color: var(--cros-sys-primary);
}
`;
static override properties: PropertyDeclarations = {
recordingId: {type: String},
};
recordingId: string|null = null;
private readonly recordingIdSignal = this.propSignal('recordingId');
private readonly dialog = createRef<CraDialog>();
private readonly exportSettings = computed(
() => settings.value.exportSettings,
);
private readonly recordingDataManager = useRecordingDataManager();
private readonly transcription = new ScopedAsyncComputed(this, async () => {
if (this.recordingIdSignal.value === null) {
return null;
}
return this.recordingDataManager.getTranscription(
this.recordingIdSignal.value,
);
});
private readonly transcriptionAvailable = computed(
() => !(this.transcription.value?.isEmpty() ?? true),
);
private readonly exportQueue = new AsyncJobQueue('drop');
private get saveEnabled() {
return (
this.exportSettings.value.audio ||
(this.transcriptionAvailable.value &&
this.exportSettings.value.transcription)
);
}
show(): void {
// There's no user waiting for the dialog open animation to be done.
void this.dialog.value?.show();
}
hide(): void {
this.dialog.value?.close();
}
private save() {
const recordingId = this.recordingId;
const exportSettings = this.exportSettings.value;
if (!this.saveEnabled || recordingId === null) {
return;
}
// TODO(pihsun): Loading state for export recording.
// TODO(pihsun): Handle failure.
this.exportQueue.push(async () => {
await this.recordingDataManager.exportRecording(
recordingId,
exportSettings,
);
this.hide();
});
}
private enableExportAudio() {
settings.mutate((s) => {
s.exportSettings.audio = true;
});
}
private disableExportAudio() {
settings.mutate((s) => {
s.exportSettings.audio = false;
});
}
private enableExportTranscription() {
settings.mutate((s) => {
s.exportSettings.transcription = true;
});
}
private disableExportTranscription() {
settings.mutate((s) => {
s.exportSettings.transcription = false;
});
}
private onAudioFormatChange(ev: Event) {
settings.mutate((s) => {
s.exportSettings.audioFormat = assertEnumVariant(
ExportAudioFormat,
assertInstanceof(ev.target, CraDropdown).value,
);
});
}
private onTranscriptionFormatChange(ev: Event) {
settings.mutate((s) => {
s.exportSettings.transcriptionFormat = assertEnumVariant(
ExportTranscriptionFormat,
assertInstanceof(ev.target, CraDropdown).value,
);
});
}
private renderDropdownOptions<T extends string>(
options: Array<DropdownOption<T>>,
selected: T,
): RenderResult {
return map(options, ({headline, value}) => {
const icon = value === selected ?
html`<cra-icon name="checked" slot="end"></cra-icon>` :
nothing;
// lit-analyzer somehow doesn't think "T extends string" is assignable to
// "string", so we have to add `assertString`...
return html`
<cros-dropdown-option
.headline=${headline}
.value=${assertString(value)}
>
${icon}
</cros-dropdown-option>
`;
});
}
override render(): RenderResult {
// TODO(pihsun): Investigate why the cros-dropdown can't be closed by
// clicking on the select again...
// TODO: b/344784478 - Show estimate file size.
const audioOptions = this.renderDropdownOptions(
[
{
headline: i18n.exportDialogAudioFormatWebmOption,
value: ExportAudioFormat.WEBM_ORIGINAL,
},
],
this.exportSettings.value.audioFormat,
);
const transcriptionOptions = this.renderDropdownOptions(
[
{
headline: i18n.exportDialogTranscriptionFormatTxtOption,
value: ExportTranscriptionFormat.TXT,
},
],
this.exportSettings.value.transcriptionFormat,
);
return html`<cra-dialog ${ref(this.dialog)}>
<div slot="headline">${i18n.exportDialogHeader}</div>
<div slot="content">
<expandable-card
?expanded=${this.exportSettings.value.audio}
@expandable-card-expanded=${this.enableExportAudio}
@expandable-card-collapsed=${this.disableExportAudio}
>
<span slot="header" class="header">
${i18n.exportDialogAudioHeader}
</span>
<cra-dropdown
.value=${this.exportSettings.value.audioFormat}
slot="content"
@change=${this.onAudioFormatChange}
>
${audioOptions}
</cra-dropdown>
</expandable-card>
<expandable-card
?expanded=${this.exportSettings.value.transcription}
?disabled=${!this.transcriptionAvailable.value}
@expandable-card-expanded=${this.enableExportTranscription}
@expandable-card-collapsed=${this.disableExportTranscription}
>
<span slot="header" class="header">
${i18n.exportDialogTranscriptionHeader}
</span>
<cra-dropdown
slot="content"
.value=${this.exportSettings.value.transcriptionFormat}
@change=${this.onTranscriptionFormatChange}
>
${transcriptionOptions}
</cra-dropdown>
</expandable-card>
</div>
<div slot="actions">
<cra-button
.label=${i18n.exportDialogCancelButton}
button-style="secondary"
@click=${this.hide}
></cra-button>
<cra-button
.label=${i18n.exportDialogSaveButton}
@click=${this.save}
?disabled=${!this.saveEnabled}
></cra-button>
</div>
</cra-dialog>`;
}
}
window.customElements.define('export-dialog', ExportDialog);
declare global {
interface HTMLElementTagNameMap {
'export-dialog': ExportDialog;
}
}