mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: add hide or show for media elements, mute or unmute for audio elements (#500)
* feat: create external tools section (#493) * feat: create external tools section * fix: better wording * Feature, added hide or show for media elements, mute or unmute for audio elements both effective on timeline and preview panel, with overlay icon for indicating so and state storing, * Minor UI vertical separator between magnifier and other buttons * fixed the AbortError issue in the AudioWaveform component. The error was occurring because of a race condition during cleanup when the component unmounts. * feat: implement bookmarking functionality in the timeline and project storage - Added bookmark management methods in the project store for toggling and checking bookmarks. - Updated the timeline component to display bookmark markers and integrate bookmark actions in the context menu. - Enhanced the storage service to handle bookmarks in project serialization and deserialization. - Updated project types to include bookmarks as an array of numbers. --------- Co-authored-by: Dominik K. <dominik@koch-bautechnik.de> Co-authored-by: Maze Winther <mazewinther@gmail.com>
This commit is contained in:
co-authored by
Dominik K.
Maze Winther
parent
b229d94a8d
commit
f255ccb818
@@ -19,6 +19,7 @@ import {
|
||||
Link,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Bookmark,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -651,6 +652,30 @@ export function Timeline() {
|
||||
);
|
||||
}).filter(Boolean);
|
||||
})()}
|
||||
|
||||
{/* Bookmark markers */}
|
||||
{(() => {
|
||||
const { activeProject } = useProjectStore.getState();
|
||||
if (!activeProject?.bookmarks?.length) return null;
|
||||
|
||||
return activeProject.bookmarks.map((bookmarkTime, i) => (
|
||||
<div
|
||||
key={`bookmark-${i}`}
|
||||
className="absolute top-0 h-10 w-0.5 !bg-primary cursor-pointer"
|
||||
style={{
|
||||
left: `${bookmarkTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
usePlaybackStore.getState().seek(bookmarkTime);
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-[-1px] left-[-5px] text-primary">
|
||||
<Bookmark className="h-3 w-3 fill-primary" />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
@@ -773,6 +798,23 @@ export function Timeline() {
|
||||
<ContextMenuItem onClick={(e) => e.stopPropagation()}>
|
||||
Track settings (soon)
|
||||
</ContextMenuItem>
|
||||
{activeProject?.bookmarks?.length && activeProject.bookmarks.length > 0 && (
|
||||
<>
|
||||
<ContextMenuItem disabled>Bookmarks</ContextMenuItem>
|
||||
{activeProject.bookmarks.map((bookmarkTime, i) => (
|
||||
<ContextMenuItem
|
||||
key={`bookmark-menu-${i}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
seek(bookmarkTime);
|
||||
}}
|
||||
>
|
||||
<Bookmark className="h-3 w-3 mr-2 inline-block" />
|
||||
{bookmarkTime.toFixed(1)}s
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))}
|
||||
@@ -828,6 +870,7 @@ function TimelineToolbar({
|
||||
toggleRippleEditing,
|
||||
} = useTimelineStore();
|
||||
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
|
||||
const { toggleBookmark, isBookmarked } = useProjectStore();
|
||||
|
||||
// Action handlers
|
||||
const handleSplitSelected = () => {
|
||||
@@ -957,6 +1000,13 @@ function TimelineToolbar({
|
||||
const handleZoomSliderChange = (values: number[]) => {
|
||||
setZoomLevel(values[0]);
|
||||
};
|
||||
|
||||
const handleToggleBookmark = async () => {
|
||||
await toggleBookmark(currentTime);
|
||||
};
|
||||
|
||||
// Check if the current time is bookmarked
|
||||
const currentBookmarked = isBookmarked(currentTime);
|
||||
return (
|
||||
<div className="border-b flex items-center justify-between px-2 py-1">
|
||||
<div className="flex items-center gap-1 w-full">
|
||||
@@ -1088,6 +1138,17 @@ function TimelineToolbar({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete element (Delete)</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="text" size="icon" onClick={handleToggleBookmark}>
|
||||
<Bookmark className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{currentBookmarked ? "Remove bookmark" : "Add bookmark"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -1121,6 +1182,8 @@ function TimelineToolbar({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="h-6 w-px bg-border mx-1" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="text" size="icon" onClick={handleZoomOut}>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
Type,
|
||||
Copy,
|
||||
RefreshCw,
|
||||
EyeOff,
|
||||
Eye,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
} from "lucide-react";
|
||||
import { useMediaStore } from "@/stores/media-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
@@ -66,11 +70,18 @@ export function TimelineElement({
|
||||
addElementToTrack,
|
||||
replaceElementMedia,
|
||||
rippleEditingEnabled,
|
||||
toggleElementHidden,
|
||||
} = useTimelineStore();
|
||||
const { currentTime } = usePlaybackStore();
|
||||
|
||||
const [elementMenuOpen, setElementMenuOpen] = useState(false);
|
||||
|
||||
const mediaItem =
|
||||
element.type === "media"
|
||||
? mediaItems.find((item) => item.id === element.mediaId)
|
||||
: null;
|
||||
const isAudio = mediaItem?.type === "audio";
|
||||
|
||||
const {
|
||||
resizing,
|
||||
isResizing,
|
||||
@@ -141,6 +152,11 @@ export function TimelineElement({
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleElementHidden = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
toggleElementHidden(track.id, element.id);
|
||||
};
|
||||
|
||||
const handleReplaceClip = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (element.type !== "media") {
|
||||
@@ -332,7 +348,7 @@ export function TimelineElement({
|
||||
track.type
|
||||
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
|
||||
isBeingDragged ? "z-50" : "z-10"
|
||||
}`}
|
||||
} ${element.hidden ? "opacity-50" : ""}`}
|
||||
onClick={(e) => onElementClick && onElementClick(e, element)}
|
||||
onMouseDown={handleElementMouseDown}
|
||||
onContextMenu={(e) =>
|
||||
@@ -343,6 +359,16 @@ export function TimelineElement({
|
||||
{renderElementContent()}
|
||||
</div>
|
||||
|
||||
{element.hidden && (
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none">
|
||||
{isAudio ? (
|
||||
<VolumeX className="h-6 w-6 text-white" />
|
||||
) : (
|
||||
<EyeOff className="h-6 w-6 text-white" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSelected && (
|
||||
<>
|
||||
<div
|
||||
@@ -363,6 +389,29 @@ export function TimelineElement({
|
||||
<Scissors className="h-4 w-4 mr-2" />
|
||||
Split at playhead
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleToggleElementHidden}>
|
||||
{isAudio ? (
|
||||
element.hidden ? (
|
||||
<Volume2 className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<VolumeX className="h-4 w-4 mr-2" />
|
||||
)
|
||||
) : element.hidden ? (
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
<span>
|
||||
{isAudio
|
||||
? element.hidden
|
||||
? "Unmute"
|
||||
: "Mute"
|
||||
: element.hidden
|
||||
? "Show"
|
||||
: "Hide"}{" "}
|
||||
{element.type === "text" ? "text" : "clip"}
|
||||
</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={handleElementDuplicateContext}>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Duplicate {element.type === "text" ? "text" : "clip"}
|
||||
|
||||
Reference in New Issue
Block a user