/** * 3D Foundation Project * Copyright 2025 Smithsonian Institution * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import "@ff/ui/Splitter"; import "@ff/ui/Button"; import "./PropertyView"; import CVAudioTask from "../../components/CVAudioTask"; import { TaskView, customElement, html, property } from "../../components/CVTask"; import List from "client/../../libs/ff-ui/source/List"; import { IAudioClip } from "client/schema/meta"; import Notification from "@ff/ui/Notification"; import CVMediaManager from "client/components/CVMediaManager"; //////////////////////////////////////////////////////////////////////////////// @customElement("sv-audio-task-view") export default class AudioTaskView extends TaskView { private _dragCounter = 0; protected selectedIndex = -1; protected optionText = ["No", "Yes"]; protected connected() { super.connected(); this.task.on("update", this.onUpdate, this); this.activeDocument.setup.audio.outs.narrationPlaying.on("value", this.onUpdate, this); //this.addEventListener("drop", this.onDropFile); } protected disconnected() { //this.removeEventListener("drop", this.onDropFile); this.activeDocument.setup.audio.outs.narrationPlaying.off("value", this.onUpdate, this); this.task.off("update", this.onUpdate, this); super.disconnected(); } protected render() { if(!this.task.audioManager || !this.activeDocument) { return; } const ins = this.task.ins; const languageManager = this.activeDocument.setup.language; const narrationFlagClass = "sv-task-option-base-align"; const audio = this.task.audioManager; const audioList = audio.getAudioList(); const audioElement = audio.getAudioClip(ins.activeId.value); const narrationEnabled = !ins.isNarration.value && audioList.some(clip => clip.id === this.task.audioManager.narrationId); const detailView = audioElement ? html`
` : null; return html`
${languageManager.getUILocalizedString("Audio Elements")}
${detailView}
`; } protected onClickCreate() { this.task.ins.create.set(); } protected onClickDelete() { this.task.ins.delete.set(); } protected onClickPlay() { this.activeDocument.setup.audio.setupAudio(); this.task.ins.play.set(); } protected onClickStop() { this.task.ins.stop.set(); } protected onSelectAudio(event: ISelectAudioEvent) { this.selectedIndex = event.detail.index; this.task.ins.activeId.setValue(event.detail.clip ? event.detail.clip.id : ""); } protected onDropFile(event: DragEvent) { event.preventDefault(); let filename = ""; let newFile : File = null; const element = event.target as HTMLElement; if(element.tagName != "INPUT") { return; } if(event.dataTransfer.files.length === 1) { newFile = event.dataTransfer.files.item(0); filename = newFile.name; } else { const filepath = event.dataTransfer.getData("text/plain"); if(filepath.length > 0) { filename = filepath; } } const id = element.parentElement.parentElement.id; const type = (id == "filename")? "audio": "subs"; const fileProp = (type == "audio") ? this.task.ins.filepath : this.task.ins.captionPath; const ext = filename.toLowerCase().split(".").pop(); if(type === "subs" && ext != "vtt"){ Notification.show(`Unable to load - Only .vtt files are currently supported.`, "warning"); }else if(type === "audio" && ["mp3","m4a","flac","ogg","wav"].indexOf(ext) === -1){ Notification.show(`Unable to load - Unsupported audio format .${ext}`, "warning"); }else{ if(type === "audio" && ext === "m4a"){ // Only m4a does not have 100% browser support Notification.show(`.${ext} audio file are not supported by some browsers`, "info", 3000); } if(newFile !== null) { const mediaManager = this.system.getMainComponent(CVMediaManager); mediaManager.uploadFile(filename, newFile, mediaManager.root).then(() => fileProp.setValue(filename)).catch(e => { Notification.show(`Audio file upload failed.`, "warning"); fileProp.setValue(""); }); } else { fileProp.setValue(filename); } } element.classList.remove("sv-drop-zone"); this._dragCounter = 0; } protected onDragEnter(event: DragEvent) { const element = event.target as HTMLElement; if(element.tagName == "INPUT") { element.classList.add("sv-drop-zone"); event.preventDefault(); this._dragCounter++; } } protected onDragOver(event: DragEvent) { event.preventDefault(); } protected onDragLeave(event: DragEvent) { const element = event.target as HTMLElement; if(element.tagName == "INPUT") { this._dragCounter--; if(this._dragCounter === 0) { element.classList.remove("sv-drop-zone"); } } } } //////////////////////////////////////////////////////////////////////////////// interface ISelectAudioEvent extends CustomEvent { target: AudioList; detail: { clip: IAudioClip; index: number; } } @customElement("sv-audio-list") export class AudioList extends List { @property({ attribute: false }) selectedItem: IAudioClip = null; protected firstConnected() { super.firstConnected(); this.classList.add("sv-audio-list"); } protected renderItem(item: IAudioClip) { return html`
${item.name}
`; } protected isItemSelected(item: IAudioClip) { return item === this.selectedItem; } protected onClickItem(event: MouseEvent, item: IAudioClip, index: number) { this.dispatchEvent(new CustomEvent("select", { detail: { clip: item, index } })); } protected onClickEmpty(event: MouseEvent) { this.dispatchEvent(new CustomEvent("select", { detail: { clip: null, index: -1 } })); } }