/
alt3rmann
/
label-studio
Обзор
Документация
Войти
/
alt3rmann
/
label-studio
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
web/libs/editor/src/components/Timeline/Controls/ConfigControl.tsx
243 строки
8 KB
Mark Galea
feat: FIT-173: Audio spectrograms (#7400)
13 июн 2025, 22:02
Не верифицирован
13 июн 2025, 22:02
b92c090
Код
Авторство
О чём код?
import type React from "react"; import { type FC, type MouseEvent, useContext, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Toggle } from "@humansignal/ui"; import { Block, Elem } from "../../../utils/bem"; import { IconConfig } from "@humansignal/icons"; import { TimelineContext } from "../Context"; import { ControlButton } from "../Controls"; import { Slider } from "./Slider"; import { SpectrogramControl } from "./SpectrogramControl"; import "./ConfigControl.scss"; import { FF_AUDIO_SPECTROGRAMS, isFF } from "../../../utils/feature-flags"; // Define Scale Options Type type SpectrogramScale = "linear" | "log" | "mel"; const MAX_SPEED = 2.5; const MAX_ZOOM = 150; const MIN_SPEED = 0.5; const MIN_ZOOM = 1; export interface ConfigControlProps { configModal: boolean; speed: number; amp: number; onSetModal?: (e: MouseEvent<HTMLButtonElement>) => void; onSpeedChange: (speed: number) => void; onAmpChange: (amp: number) => void; toggleVisibility?: (layerName: string, isVisible: boolean) => void; layerVisibility?: Map<string, boolean>; waveform: Waveform; } type Waveform = {}; export const ConfigControl: FC<ConfigControlProps> = ({ configModal, speed, amp, onSpeedChange, onSetModal, onAmpChange, toggleVisibility, layerVisibility, waveform, }) => { const { settings, changeSetting } = useContext(TimelineContext); const playbackSpeed = speed ?? 1; const [isTimeline, setTimeline] = useState(true); const [isAudioWave, setAudioWave] = useState(true); const [isSpectrogram, setSpectrogram] = useState(false); // Refs for positioning const modalRef = useRef<HTMLDivElement>(null); const buttonRef = useRef<HTMLButtonElement>(null); // Effect to dynamically position the modal within the viewport useEffect(() => { // Check if modal is open and refs are attached if (configModal && modalRef.current && buttonRef.current) { const buttonRect = buttonRef.current.getBoundingClientRect(); const modal = modalRef.current; // Temporarily make it visible off-screen to measure its actual size modal.style.opacity = "0"; modal.style.position = "fixed"; // Ensure fixed for measurement modal.style.top = "-9999px"; modal.style.left = "-9999px"; const calculatePosition = () => { if (!modalRef.current || !buttonRef.current) return; // Refs might detach const modalRect = modal.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const margin = 10; // Margin from viewport edges // Default position: below the button, aligned left let top = buttonRect.bottom + 5; let left = buttonRect.left; // Adjust top if modal goes below viewport if (top + modalRect.height > viewportHeight - margin) { // Try placing above the button first const topAbove = buttonRect.top - modalRect.height - 5; if (topAbove > margin) { top = topAbove; // Place above if enough space } else { top = viewportHeight - modalRect.height - margin; // Stick to bottom edge } } // Adjust top if modal goes above viewport if (top < margin) { top = margin; } // Adjust left if modal goes beyond right edge if (left + modalRect.width > viewportWidth - margin) { left = viewportWidth - modalRect.width - margin; } // Adjust left if modal goes beyond left edge if (left < margin) { left = margin; } // Apply calculated styles modal.style.top = `${top}px`; modal.style.left = `${left}px`; modal.style.opacity = "1"; // Make visible after positioning }; // Calculate after a short delay or next frame to allow measurement requestAnimationFrame(calculatePosition); } else if (modalRef.current) { // Reset opacity when closing modalRef.current.style.opacity = "0"; } }, [configModal]); // Rerun effect when modal visibility changes useEffect(() => { if (layerVisibility) { const defaultDisplay = true; setTimeline(layerVisibility?.get?.("timeline") ?? defaultDisplay); setAudioWave(layerVisibility?.get?.("waveform") ?? defaultDisplay); setSpectrogram(layerVisibility?.get?.("spectrogram") ?? false); } }, [layerVisibility]); const handleSetTimeline = () => { setTimeline(!isTimeline); toggleVisibility?.("timeline", !isTimeline); }; const handleSetAudioWave = () => { setAudioWave(!isAudioWave); toggleVisibility?.("waveform", !isAudioWave); toggleVisibility?.("regions", !isAudioWave); }; const handleSetSpectrogram = () => { setSpectrogram(!isSpectrogram); toggleVisibility?.("spectrogram", !isSpectrogram); }; const handleChangePlaybackSpeed = (e: React.FormEvent<HTMLInputElement>) => { const _playbackSpeed = Number.parseFloat(e.currentTarget.value); if (isNaN(_playbackSpeed)) return; onSpeedChange(_playbackSpeed); }; const handleChangeAmp = (e: React.FormEvent<HTMLInputElement>) => { const _amp = Number.parseFloat(e.currentTarget.value); onAmpChange(_amp); }; const renderLayerToggles = () => { return ( <Elem name={"buttons"}> <Elem name="menu-button" onClick={handleSetTimeline}> {isTimeline ? "Hide" : "Show"} timeline </Elem> <Elem name="menu-button" onClick={handleSetAudioWave}> {isAudioWave ? "Hide" : "Show"} audio wave </Elem> {isFF(FF_AUDIO_SPECTROGRAMS) && ( <Elem name="menu-button" onClick={handleSetSpectrogram}> {isSpectrogram ? "Hide" : "Show"} spectrogram </Elem> )} </Elem> ); }; const renderModal = () => { const modalJSX = ( <Elem block="audio-config" name="modal" ref={modalRef} onClick={(e: MouseEvent<HTMLDivElement>) => e.stopPropagation()} style={{ opacity: 0, position: "fixed" }} > <Elem name="scroll-content"> <Elem name="section-header">Playback Settings</Elem> <Slider min={MIN_SPEED} max={MAX_SPEED} step={0.1} value={speed} description={"Playback speed"} info={"Increase or decrease the playback speed"} onChange={handleChangePlaybackSpeed} /> <Slider min={MIN_ZOOM} max={MAX_ZOOM} step={0.1} value={amp} description={"Audio zoom y-axis"} info={"Increase or decrease the appearance of amplitude"} onChange={handleChangeAmp} /> <Elem name="toggle"> <Toggle checked={settings?.loopRegion} onChange={(e) => changeSetting?.("loopRegion", e.target.checked)} label="Loop Regions" labelProps={{ size: "small" }} /> </Elem> <Elem name="toggle"> <Toggle checked={settings?.autoPlayNewSegments} onChange={(e) => changeSetting?.("autoPlayNewSegments", e.target.checked)} label="Auto-play New Regions" labelProps={{ size: "small" }} /> </Elem> {isFF(FF_AUDIO_SPECTROGRAMS) && ( <> <Elem name="section-header">Spectrogram Settings</Elem> <SpectrogramControl waveform={waveform} /> </> )} </Elem> {renderLayerToggles()} </Elem> ); return typeof document !== "undefined" ? createPortal(modalJSX, document.body) : null; }; return ( <Block name="audio-config" ref={buttonRef} onClick={(e: MouseEvent<HTMLButtonElement>) => e.stopPropagation()}> <ControlButton look={configModal ? "active" : undefined} onClick={onSetModal}> {<IconConfig />} </ControlButton> {configModal && renderModal()} </Block> ); };