241 lines
6.8 KiB
TypeScript
241 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
|
import { RecordEvent, RecordingSession } from '@/types/record-mode';
|
|
|
|
interface RecordModeContextType {
|
|
isActive: boolean;
|
|
setIsActive: (active: boolean) => void;
|
|
events: RecordEvent[];
|
|
addEvent: (event: Omit<RecordEvent, 'id' | 'timestamp'>) => void;
|
|
updateEvent: (id: string, event: Partial<RecordEvent>) => void;
|
|
removeEvent: (id: string) => void;
|
|
clearEvents: () => void;
|
|
isPlaying: boolean;
|
|
playEvents: () => void;
|
|
stopPlayback: () => void;
|
|
cursorPosition: { x: number; y: number };
|
|
zoomLevel: number;
|
|
isBlurry: boolean;
|
|
currentSession: RecordingSession | null;
|
|
saveSession: (name: string) => void;
|
|
isFeedbackActive: boolean;
|
|
setIsFeedbackActive: (active: boolean) => void;
|
|
}
|
|
|
|
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
|
|
|
export function useRecordMode(): RecordModeContextType {
|
|
const context = useContext(RecordModeContext);
|
|
if (!context) {
|
|
// Return a fail-safe fallback for SSR/Static generation where provider might be missing
|
|
return {
|
|
isActive: false,
|
|
setIsActive: () => { },
|
|
events: [],
|
|
addEvent: () => { },
|
|
updateEvent: () => { },
|
|
removeEvent: () => { },
|
|
clearEvents: () => { },
|
|
isPlaying: false,
|
|
playEvents: () => { },
|
|
stopPlayback: () => { },
|
|
cursorPosition: { x: 0, y: 0 },
|
|
zoomLevel: 1,
|
|
isBlurry: false,
|
|
currentSession: null,
|
|
isFeedbackActive: false,
|
|
setIsFeedbackActive: () => { },
|
|
saveSession: () => { },
|
|
};
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function RecordModeProvider({ children }: { children: React.ReactNode }) {
|
|
const [isActive, setIsActiveState] = useState(false);
|
|
const [events, setEvents] = useState<RecordEvent[]>([]);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
|
|
const [zoomLevel, setZoomLevel] = useState(1);
|
|
const [isBlurry, setIsBlurry] = useState(false);
|
|
const [isFeedbackActive, setIsFeedbackActiveState] = useState(false);
|
|
|
|
// Synchronous mutually exclusive setters
|
|
const setIsActive = (active: boolean) => {
|
|
setIsActiveState(active);
|
|
if (active) setIsFeedbackActiveState(false);
|
|
};
|
|
|
|
const setIsFeedbackActive = (active: boolean) => {
|
|
setIsFeedbackActiveState(active);
|
|
if (active) setIsActiveState(false);
|
|
};
|
|
|
|
const playbackRequestRef = useRef<number>(0);
|
|
const isPlayingRef = useRef(false);
|
|
|
|
// Load draft from localStorage on mount
|
|
useEffect(() => {
|
|
const savedEvents = localStorage.getItem('klz-record-events');
|
|
const savedActive = localStorage.getItem('klz-record-active');
|
|
if (savedEvents) setEvents(JSON.parse(savedEvents));
|
|
if (savedActive) setIsActive(JSON.parse(savedActive));
|
|
}, []);
|
|
|
|
// Sync events to localStorage
|
|
useEffect(() => {
|
|
localStorage.setItem('klz-record-events', JSON.stringify(events));
|
|
}, [events]);
|
|
|
|
// Sync active state to localStorage
|
|
useEffect(() => {
|
|
localStorage.setItem('klz-record-active', JSON.stringify(isActive));
|
|
if (isActive && isFeedbackActive) {
|
|
setIsFeedbackActive(false);
|
|
}
|
|
}, [isActive, isFeedbackActive]);
|
|
|
|
useEffect(() => {
|
|
if (isFeedbackActive && isActive) {
|
|
setIsActive(false);
|
|
}
|
|
}, [isFeedbackActive, isActive]);
|
|
|
|
const addEvent = (event: Omit<RecordEvent, 'id' | 'timestamp'>) => {
|
|
const newEvent: RecordEvent = {
|
|
...event,
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
timestamp: Date.now(),
|
|
};
|
|
setEvents((prev) => [...prev, newEvent]);
|
|
};
|
|
|
|
const updateEvent = (id: string, updatedFields: Partial<RecordEvent>) => {
|
|
setEvents((prev) =>
|
|
prev.map((event) => (event.id === id ? { ...event, ...updatedFields } : event))
|
|
);
|
|
};
|
|
|
|
const removeEvent = (id: string) => {
|
|
setEvents((prev) => prev.filter((event) => event.id !== id));
|
|
};
|
|
|
|
const clearEvents = () => {
|
|
if (confirm('Clear all recorded events?')) {
|
|
setEvents([]);
|
|
}
|
|
};
|
|
|
|
const currentSession: RecordingSession | null = events.length > 0 ? {
|
|
id: 'draft',
|
|
name: 'Draft Session',
|
|
events,
|
|
createdAt: new Date().toISOString(),
|
|
} : null;
|
|
|
|
const saveSession = (name: string) => {
|
|
// In a real app, this would be an API call
|
|
console.log('Saving session:', name, events);
|
|
};
|
|
|
|
const playEvents = async () => {
|
|
if (events.length === 0 || isPlayingRef.current) return;
|
|
|
|
setIsPlaying(true);
|
|
isPlayingRef.current = true;
|
|
|
|
// Sort events by timestamp just in case
|
|
const sortedEvents = [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
|
|
for (const event of sortedEvents) {
|
|
if (!isPlayingRef.current) break;
|
|
|
|
// 1. Move Cursor
|
|
if (event.rect) {
|
|
setCursorPosition({
|
|
x: event.rect.x + event.rect.width / 2,
|
|
y: event.rect.y + event.rect.height / 2,
|
|
});
|
|
}
|
|
|
|
// 2. Handle Action
|
|
if (event.selector) {
|
|
const el = document.querySelector(event.selector) as HTMLElement;
|
|
if (el) {
|
|
if (event.type === 'scroll') {
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
} else if (event.type === 'click') {
|
|
// Precise industrial click sequence: mousedown -> mouseup -> click
|
|
const eventCoords = {
|
|
clientX: event.rect ? event.rect.x + event.rect.width / 2 : 0,
|
|
clientY: event.rect ? event.rect.y + event.rect.height / 2 : 0
|
|
};
|
|
|
|
const dispatchMouse = (type: string) => {
|
|
el.dispatchEvent(new MouseEvent(type, {
|
|
view: window,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
...eventCoords
|
|
}));
|
|
};
|
|
|
|
dispatchMouse('mousedown');
|
|
dispatchMouse('mouseup');
|
|
dispatchMouse('click');
|
|
|
|
// Fallback for native elements
|
|
el.click();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Zoom/Blur
|
|
if (event.zoom) setZoomLevel(event.zoom);
|
|
if (event.motionBlur) setIsBlurry(true);
|
|
|
|
// 4. Wait
|
|
await new Promise((resolve) => setTimeout(resolve, event.duration || 1000));
|
|
setIsBlurry(false);
|
|
}
|
|
|
|
setIsPlaying(false);
|
|
isPlayingRef.current = false;
|
|
setZoomLevel(1);
|
|
};
|
|
|
|
const stopPlayback = () => {
|
|
setIsPlaying(false);
|
|
isPlayingRef.current = false;
|
|
setZoomLevel(1);
|
|
setIsBlurry(false);
|
|
};
|
|
|
|
return (
|
|
<RecordModeContext.Provider
|
|
value={{
|
|
isActive,
|
|
setIsActive,
|
|
events,
|
|
addEvent,
|
|
updateEvent,
|
|
removeEvent,
|
|
clearEvents,
|
|
isPlaying,
|
|
playEvents,
|
|
stopPlayback,
|
|
cursorPosition,
|
|
zoomLevel,
|
|
isBlurry,
|
|
currentSession,
|
|
saveSession,
|
|
isFeedbackActive,
|
|
setIsFeedbackActive,
|
|
}}
|
|
>
|
|
{children}
|
|
</RecordModeContext.Provider>
|
|
);
|
|
}
|