mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
* feat: separate page into different component, greatly improving the loading and reactivity
* fix: various fixes
* feat: migrate to Chakra UI v3 - update theme, fix deprecated props
- Add whiteAlpha color palette with semantic tokens
- Update button recipe with fontWeight 600 and hover states
- Move Poppins font from theme to HTML tag className
- Fix deprecated props: isDisabled→disabled, align→alignItems/textAlign
- Remove button.css as styles are now handled by Chakra v3
* fix: complete Chakra UI v3 deprecated prop migrations
- Replace all isDisabled with disabled
- Replace all isChecked with checked
- Replace all isLoading with loading
- Replace all isOpen with open
- Replace all noOfLines with lineClamp
- Replace all align with alignItems on Flex/Stack components
- Replace all justify with justifyContent on Flex/Stack components
- Update temporary Select components to use new prop names
- Update REFACTOR2.md with completion status
* fix: add value prop to Menu.Item for proper hover states in Chakra v3
* fix: update browse page components for Chakra UI v3 compatibility
- Fix FilterSidebar status filter styling and prop usage
- Update Pagination component to use new Chakra v3 props and structure
- Refactor TranscriptTable to use modern Chakra patterns
- Clean up browse page layout and props
- Remove unused import from transcripts API view
- Enhance theme with additional semantic color tokens
* fix: polish browse page UI for Chakra v3
- Add rounded corners to FilterSidebar
- Adjust responsive breakpoints from md to lg for table/card view
- Add consistent font weights to table headers
- Improve card view typography and spacing
- Fix padding and margins for better mobile experience
- Remove unused table recipe from theme
* fix: padding
* fix: rework transcript page
* fix: more tidy layout for topic
* fix: share and privacy using chakra3 select
* fix: fix share and privacy select, now working, with closing dialog
* fix: complete Chakra UI v3 migration for share components and fix all TypeScript errors
- Refactor shareZulip.tsx to integrate modal content directly
- Replace react-select-search with Chakra UI v3 Select components using collection pattern
- Convert all Checkbox components to use v3 composable structure (Checkbox.Root, etc.)
- Fix Card components to use Card.Root and Card.Body
- Replace deprecated textColor prop with color prop
- Update Menu components to use v3 namespace pattern (Menu.Root, Menu.Trigger, etc.)
- Remove unused AlertDialog imports
- Fix useDisclosure hook changes (isOpen -> open)
- Replace UnorderedList with List.Root and ListItem with List.Item
- Fix Skeleton components by removing isLoaded prop and using conditional rendering
- Update Button variants to valid v3 options
- Fix Spinner props (remove thickness, speed, emptyColor)
- Update toast API to use custom toaster component
- Fix Progress components and FormControl to Field.Root
- Update Alert to use compound component pattern
- Remove shareModal.tsx file after integration
* fix: bring back topic list
* fix: normalize menu item
* fix: migrate rooms page to Chakra UI v3 pattern
- Updated layout to match browse page with Flex container and proper spacing
- Migrated add/edit room modal from custom HTML to Chakra UI v3 Dialog component
- Replaced all Select components with Chakra UI v3 Select using createListCollection
- Replaced FormControl/FormLabel/FormHelperText with Field.Root/Field.Label/Field.HelperText
- Removed inline styles and used Chakra props (mr={2} instead of style={{ marginRight: "8px" }})
- Fixed TypeScript interfaces removing OptionBase extension
- Fixed theme.ts accordion anatomy import issue
* refactor: convert rooms list to table view with responsive design
- Create RoomTable component for desktop view showing room details in columns
- Create RoomCards component for mobile/tablet responsive view
- Refactor RoomList to use table/card components based on screen size
- Display Zulip configuration, room size, and recording settings in table
- Remove unused RoomItem component
- Import Room type from API for proper typing
* refactor: extract RoomActionsMenu component to eliminate duplication
- Create RoomActionsMenu component for consistent room action menus
- Update RoomCards and RoomTable to use the new shared component
- Remove duplicated menu code from both components
* feat: add icons to TranscriptActionsMenu for consistency
- Add FaTrash icon for Delete action with red color
- Add FaArrowsRotate icon for Reprocess action
- Matches the pattern established in RoomActionsMenu
* refactor: update icons from Font Awesome to Lucide React
- Replace FaEllipsisVertical with LuMenu in menu triggers
- Replace FaLink with LuLink for copy URL buttons
- Replace FaPencil with LuPen for edit actions
- Replace FaTrash with LuTrash for delete actions
- Replace FaArrowsRotate with LuRotateCw for reprocess action
- Consistent icon library usage across all components
* refactor: little pass on the icons
* fix: lu icon
* fix: primary for button
* fix: recording page with mic selection
* fix: also fix duration
* fix: use combobox for share zulip
* fix: use proper theming for button, variant was not recognized
* fix: room actions menu
* fix: remove other variant primary left.
196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
import React, { useRef, useEffect, useState } from "react";
|
|
|
|
import WaveSurfer from "wavesurfer.js";
|
|
import RegionsPlugin from "wavesurfer.js/dist/plugins/regions.esm.js";
|
|
|
|
import { formatTime, formatTimeMs } from "../../lib/time";
|
|
import { Topic } from "./webSocketTypes";
|
|
import { AudioWaveform } from "../../api";
|
|
import { waveSurferStyles } from "../../styles/recorder";
|
|
import { Box, Flex, IconButton } from "@chakra-ui/react";
|
|
import { LuPause, LuPlay } from "react-icons/lu";
|
|
|
|
type PlayerProps = {
|
|
topics: Topic[];
|
|
useActiveTopic: [
|
|
Topic | null,
|
|
React.Dispatch<React.SetStateAction<Topic | null>>,
|
|
];
|
|
waveform: AudioWaveform;
|
|
media: HTMLMediaElement;
|
|
mediaDuration: number;
|
|
};
|
|
|
|
export default function Player(props: PlayerProps) {
|
|
const waveformRef = useRef<HTMLDivElement>(null);
|
|
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null);
|
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
|
const [currentTime, setCurrentTime] = useState<number>(0);
|
|
const [waveRegions, setWaveRegions] = useState<RegionsPlugin | null>(null);
|
|
const [activeTopic, setActiveTopic] = props.useActiveTopic;
|
|
const topicsRef = useRef(props.topics);
|
|
const [firstRender, setFirstRender] = useState<boolean>(true);
|
|
|
|
const keyHandler = (e) => {
|
|
if (e.key == " ") {
|
|
wavesurfer?.playPause();
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
document.addEventListener("keyup", keyHandler);
|
|
return () => {
|
|
document.removeEventListener("keyup", keyHandler);
|
|
};
|
|
});
|
|
|
|
// Waveform setup
|
|
useEffect(() => {
|
|
if (waveformRef.current) {
|
|
const _wavesurfer = WaveSurfer.create({
|
|
container: waveformRef.current,
|
|
peaks: [props.waveform.data],
|
|
height: "auto",
|
|
duration: Math.floor(props.mediaDuration / 1000),
|
|
media: props.media,
|
|
|
|
...waveSurferStyles.playerSettings,
|
|
});
|
|
|
|
// styling
|
|
const wsWrapper = _wavesurfer.getWrapper();
|
|
wsWrapper.style.cursor = waveSurferStyles.playerStyle.cursor;
|
|
wsWrapper.style.backgroundColor =
|
|
waveSurferStyles.playerStyle.backgroundColor;
|
|
wsWrapper.style.borderRadius = waveSurferStyles.playerStyle.borderRadius;
|
|
|
|
_wavesurfer.on("play", () => {
|
|
setIsPlaying(true);
|
|
});
|
|
_wavesurfer.on("pause", () => {
|
|
setIsPlaying(false);
|
|
});
|
|
_wavesurfer.on("timeupdate", setCurrentTime);
|
|
|
|
setWaveRegions(_wavesurfer.registerPlugin(RegionsPlugin.create()));
|
|
|
|
_wavesurfer.toggleInteraction(true);
|
|
|
|
setWavesurfer(_wavesurfer);
|
|
|
|
return () => {
|
|
_wavesurfer.destroy();
|
|
setIsPlaying(false);
|
|
setCurrentTime(0);
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!wavesurfer) return;
|
|
if (!props.media) return;
|
|
wavesurfer.setMediaElement(props.media);
|
|
}, [props.media, wavesurfer]);
|
|
|
|
useEffect(() => {
|
|
if (!waveRegions) return;
|
|
|
|
topicsRef.current = props.topics;
|
|
if (firstRender) {
|
|
setFirstRender(false);
|
|
// wait for the waveform to render, if you don't markers will be stacked on top of each other
|
|
// I tried to listen for the waveform to be ready but it didn't work
|
|
setTimeout(() => {
|
|
renderMarkers();
|
|
}, 300);
|
|
} else {
|
|
renderMarkers();
|
|
}
|
|
}, [props.topics, waveRegions]);
|
|
|
|
const renderMarkers = () => {
|
|
if (!waveRegions) return;
|
|
|
|
waveRegions.clearRegions();
|
|
|
|
for (let topic of topicsRef.current) {
|
|
const content = document.createElement("div");
|
|
content.setAttribute("style", waveSurferStyles.marker);
|
|
content.onmouseover = (e) => {
|
|
content.style.backgroundColor =
|
|
waveSurferStyles.markerHover.backgroundColor;
|
|
content.style.width = "300px";
|
|
if (content.parentElement) {
|
|
content.parentElement.style.zIndex = "999";
|
|
}
|
|
};
|
|
content.onmouseout = () => {
|
|
content.setAttribute("style", waveSurferStyles.marker);
|
|
if (content.parentElement) {
|
|
content.parentElement.style.zIndex = "0";
|
|
}
|
|
};
|
|
content.textContent = topic.title;
|
|
|
|
const region = waveRegions.addRegion({
|
|
start: topic.timestamp,
|
|
content,
|
|
drag: false,
|
|
resize: false,
|
|
top: 0,
|
|
});
|
|
region.on("click", (e) => {
|
|
e.stopPropagation();
|
|
setActiveTopic(topic);
|
|
wavesurfer?.setTime(region.start);
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (activeTopic) {
|
|
wavesurfer?.setTime(activeTopic.timestamp);
|
|
}
|
|
}, [activeTopic]);
|
|
|
|
const handlePlayClick = () => {
|
|
wavesurfer?.playPause();
|
|
};
|
|
|
|
const timeLabel = () => {
|
|
if (props.mediaDuration && Math.floor(props.mediaDuration / 1000) > 0)
|
|
return `${formatTime(currentTime)}/${formatTimeMs(props.mediaDuration)}`;
|
|
return "";
|
|
};
|
|
|
|
return (
|
|
<Flex className="flex items-center w-full relative">
|
|
<IconButton
|
|
aria-label={isPlaying ? "Pause" : "Play"}
|
|
variant={"ghost"}
|
|
colorPalette={"blue"}
|
|
mr={2}
|
|
id="play-btn"
|
|
onClick={handlePlayClick}
|
|
size="sm"
|
|
>
|
|
{isPlaying ? <LuPause /> : <LuPlay />}
|
|
</IconButton>
|
|
|
|
<Box position="relative" flex={1}>
|
|
<Box ref={waveformRef} height={14}></Box>
|
|
<Box
|
|
zIndex={50}
|
|
backgroundColor="rgba(255, 255, 255, 0.5)"
|
|
fontSize={"sm"}
|
|
shadow={"0px 0px 4px 0px white"}
|
|
position={"absolute"}
|
|
right={0}
|
|
bottom={0}
|
|
>
|
|
{timeLabel()}
|
|
</Box>
|
|
</Box>
|
|
</Flex>
|
|
);
|
|
}
|