import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { selectViewerUrlSnapshot, useViewerStore } from '../store/viewerStore'; import type { ViewerUrlSnapshot } from '../types/viewer'; import { createViewerShareUrl, decodeViewerStateFromSearch, writeViewerStateToUrl } from '../utils/viewStateUrl'; export interface UseViewerUrlStateOptions { syncToUrl?: boolean; debounceMs?: number; } export interface UseViewerUrlStateResult { shareUrl: string; copyShareLink: () => Promise; hasUrlState: boolean; } export function useViewerUrlState( machineId: string, options: UseViewerUrlStateOptions = {} ): UseViewerUrlStateResult { const { syncToUrl = true, debounceMs = 140 } = options; const applyUrlSnapshot = useViewerStore((state) => state.applyUrlSnapshot); const snapshot = useViewerStore(selectViewerUrlSnapshot); const [hasUrlState, setHasUrlState] = useState(false); const didHydrateRef = useRef(false); useEffect(() => { if (didHydrateRef.current || typeof window === 'undefined') { return; } didHydrateRef.current = true; const decoded = decodeViewerStateFromSearch(window.location.search); const containsViewerParams = Object.keys(decoded).length > 0; if (containsViewerParams) { applyUrlSnapshot({ ...decoded, machineId: decoded.machineId ?? machineId }); setHasUrlState(true); } }, [applyUrlSnapshot, machineId]); const mergedSnapshot: ViewerUrlSnapshot = useMemo( () => ({ ...snapshot, machineId: snapshot.machineId ?? machineId }), [machineId, snapshot] ); const serializedSnapshot = useMemo( () => JSON.stringify(mergedSnapshot), [mergedSnapshot] ); useEffect(() => { if (!syncToUrl || typeof window === 'undefined' || !didHydrateRef.current) { return undefined; } const timeoutId = window.setTimeout(() => { writeViewerStateToUrl(mergedSnapshot, 'replace'); }, debounceMs); return () => window.clearTimeout(timeoutId); }, [debounceMs, mergedSnapshot, serializedSnapshot, syncToUrl]); const shareUrl = useMemo(() => { if (typeof window === 'undefined') { return createViewerShareUrl(mergedSnapshot); } return createViewerShareUrl(mergedSnapshot, window.location.href); }, [mergedSnapshot, serializedSnapshot]); const copyShareLink = useCallback(async () => { const latestSnapshot = { ...selectViewerUrlSnapshot(useViewerStore.getState()), machineId }; const url = createViewerShareUrl( latestSnapshot, typeof window !== 'undefined' ? window.location.href : undefined ); if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(url); return url; } if (typeof document !== 'undefined') { const textarea = document.createElement('textarea'); textarea.value = url; textarea.setAttribute('readonly', 'true'); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; textarea.style.pointerEvents = 'none'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } finally { document.body.removeChild(textarea); } } return url; }, [machineId]); return { shareUrl, copyShareLink, hasUrlState }; }