All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 19s
149 lines
3.7 KiB
TypeScript
149 lines
3.7 KiB
TypeScript
import type { NodeKey } from 'lexical';
|
|
import type { JSX } from 'react';
|
|
|
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
|
|
import { mergeRegister } from '@lexical/utils';
|
|
import {
|
|
$setSelection,
|
|
CLICK_COMMAND,
|
|
COMMAND_PRIORITY_LOW,
|
|
KEY_ESCAPE_COMMAND,
|
|
SELECTION_CHANGE_COMMAND,
|
|
} from 'lexical';
|
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
|
import { getS3Url } from '../s3Config';
|
|
|
|
interface AudioComponentProps {
|
|
nodeKey: NodeKey;
|
|
src: string;
|
|
fileKey?: string;
|
|
uploadProgress?: number;
|
|
uploadError?: string;
|
|
}
|
|
|
|
export default function AudioComponent({
|
|
nodeKey,
|
|
src,
|
|
fileKey,
|
|
uploadProgress,
|
|
uploadError,
|
|
}: AudioComponentProps): JSX.Element {
|
|
const [editor] = useLexicalComposerContext();
|
|
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
|
|
const resolvedSrc = useMemo(() => {
|
|
if (fileKey) {
|
|
return getS3Url(fileKey);
|
|
}
|
|
return src;
|
|
}, [fileKey, src]);
|
|
|
|
const isUploading = uploadProgress !== undefined && uploadProgress < 100 && !uploadError;
|
|
|
|
const handleEscape = useCallback(() => {
|
|
if (!isSelected) {
|
|
return false;
|
|
}
|
|
$setSelection(null);
|
|
editor.update(() => {
|
|
setSelected(true);
|
|
editor.getRootElement()?.focus();
|
|
});
|
|
return true;
|
|
}, [editor, isSelected, setSelected]);
|
|
|
|
const handleClick = useCallback(
|
|
(event: MouseEvent) => {
|
|
if (!containerRef.current) {
|
|
return false;
|
|
}
|
|
if (containerRef.current.contains(event.target as Node)) {
|
|
if (event.shiftKey) {
|
|
setSelected(!isSelected);
|
|
} else {
|
|
clearSelection();
|
|
setSelected(true);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
[clearSelection, isSelected, setSelected],
|
|
);
|
|
|
|
useEffect(() => {
|
|
return mergeRegister(
|
|
editor.registerCommand(
|
|
SELECTION_CHANGE_COMMAND,
|
|
() => false,
|
|
COMMAND_PRIORITY_LOW,
|
|
),
|
|
editor.registerCommand(
|
|
CLICK_COMMAND,
|
|
handleClick,
|
|
COMMAND_PRIORITY_LOW,
|
|
),
|
|
editor.registerCommand(
|
|
KEY_ESCAPE_COMMAND,
|
|
handleEscape,
|
|
COMMAND_PRIORITY_LOW,
|
|
),
|
|
);
|
|
}, [editor, handleClick, handleEscape]);
|
|
|
|
const borderColor = uploadError
|
|
? 'rgba(244, 67, 54, 0.6)'
|
|
: isSelected
|
|
? 'var(--accent-color, #4CAF50)'
|
|
: 'var(--border-color, #ccc)';
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="editor-audio-card"
|
|
style={{
|
|
borderColor,
|
|
}}
|
|
tabIndex={0}
|
|
onFocus={() => {
|
|
clearSelection();
|
|
setSelected(true);
|
|
}}
|
|
>
|
|
<div className="editor-audio-wrapper">
|
|
{resolvedSrc ? (
|
|
<audio
|
|
ref={audioRef}
|
|
className="editor-audio-player"
|
|
controls
|
|
preload="metadata"
|
|
src={resolvedSrc}
|
|
/>
|
|
) : (
|
|
<div className="editor-audio-placeholder">Preparing audio...</div>
|
|
)}
|
|
{isUploading && (
|
|
<div className="editor-audio-overlay">
|
|
<div className="editor-audio-progress">
|
|
<div
|
|
className="editor-audio-progress-value"
|
|
style={{ width: `${uploadProgress || 0}%` }}
|
|
/>
|
|
</div>
|
|
<div className="editor-audio-progress-label">
|
|
Uploading... {Math.round(uploadProgress || 0)}%
|
|
</div>
|
|
</div>
|
|
)}
|
|
{uploadError && (
|
|
<div className="editor-audio-error">❌ {uploadError}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|