import { GetRoomEngine } from '@nitrots/nitro-renderer'; import { CreateLinkEvent, GetRoomSession, SendMessageComposer } from '../../api'; /** * Plugin descriptor registered by external plugin scripts. */ export interface INitroPlugin { /** Unique plugin name */ name: string; /** Label shown on the button in room tools */ label: string; /** CSS class for the icon (nitro-icon class) */ icon?: string; /** Called when the plugin button is clicked */ onOpen: () => void; /** Called to close/destroy the plugin UI */ onClose?: () => void; /** Called when the plugin is first loaded, receives the Nitro API */ onInit?: (api: INitroPluginApi) => void; } /** * API exposed to external plugins via window.NitroPlugins */ export interface INitroPluginApi { /** Register a plugin */ register: (plugin: INitroPlugin) => void; /** Unregister a plugin by name */ unregister: (name: string) => void; /** Get all registered plugins */ getPlugins: () => INitroPlugin[]; /** Fire a Nitro link event (e.g., 'navigator/toggle-room-info') */ createLinkEvent: (link: string) => void; /** Get the room engine instance */ getRoomEngine: () => ReturnType; /** Get the current room session */ getRoomSession: () => ReturnType; /** Send a message composer to the server */ sendMessage: typeof SendMessageComposer; /** Create a draggable floating window and return its container element */ createWindow: (id: string, title: string, width: number) => HTMLDivElement | null; /** Destroy a floating window by id */ destroyWindow: (id: string) => void; } // Internal plugin storage const _plugins: INitroPlugin[] = []; const _listeners: Array<() => void> = []; function notifyListeners() { _listeners.forEach(fn => fn()); } const pluginApi: INitroPluginApi = { register(plugin: INitroPlugin) { if (_plugins.some(p => p.name === plugin.name)) return; _plugins.push(plugin); plugin.onInit?.(pluginApi); notifyListeners(); }, unregister(name: string) { const index = _plugins.findIndex(p => p.name === name); if (index >= 0) { _plugins[index].onClose?.(); _plugins.splice(index, 1); notifyListeners(); } }, getPlugins() { return [..._plugins]; }, createLinkEvent(link: string) { CreateLinkEvent(link); }, getRoomEngine() { return GetRoomEngine(); }, getRoomSession() { return GetRoomSession(); }, sendMessage: SendMessageComposer, createWindow(id: string, title: string, width: number): HTMLDivElement | null { // Remove existing window with same id pluginApi.destroyWindow(id); // Create overlay container const overlay = document.createElement('div'); overlay.id = `nitro-plugin-window-${id}`; overlay.style.cssText = `position:fixed;z-index:500;top:50%;left:50%;transform:translate(-50%,-50%)`; // Card wrapper const card = document.createElement('div'); card.style.cssText = `width:${width}px;background:#2c3e50;border:1px solid #283F5D;border-radius:6px;box-shadow:0 8px 32px rgba(0,0,0,0.5);overflow:hidden;font-family:Ubuntu,sans-serif`; // Header (draggable) const header = document.createElement('div'); header.style.cssText = `display:flex;align-items:center;justify-content:center;position:relative;min-height:33px;background:linear-gradient(180deg,#3c6a8e 0%,#2a4f6e 100%);cursor:move;user-select:none`; const titleEl = document.createElement('span'); titleEl.textContent = title; titleEl.style.cssText = `color:#fff;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,0.5)`; const closeBtn = document.createElement('div'); closeBtn.style.cssText = `position:absolute;right:8px;width:20px;height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;border-radius:50%;background:rgba(255,255,255,0.1)`; closeBtn.innerHTML = '✕'; closeBtn.addEventListener('click', () => pluginApi.destroyWindow(id)); header.appendChild(titleEl); header.appendChild(closeBtn); // Make draggable let isDragging = false; let offsetX = 0, offsetY = 0; header.addEventListener('mousedown', (e: MouseEvent) => { isDragging = true; const rect = overlay.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; overlay.style.transform = 'none'; overlay.style.left = rect.left + 'px'; overlay.style.top = rect.top + 'px'; }); document.addEventListener('mousemove', (e: MouseEvent) => { if (!isDragging) return; overlay.style.left = (e.clientX - offsetX) + 'px'; overlay.style.top = (e.clientY - offsetY) + 'px'; }); document.addEventListener('mouseup', () => { isDragging = false; }); // Content area const content = document.createElement('div'); content.style.cssText = `padding:16px`; card.appendChild(header); card.appendChild(content); overlay.appendChild(card); document.body.appendChild(overlay); return content; }, destroyWindow(id: string) { const existing = document.getElementById(`nitro-plugin-window-${id}`); if (existing) existing.remove(); } }; /** * Subscribe to plugin list changes. Returns unsubscribe function. */ export function subscribePlugins(listener: () => void): () => void { _listeners.push(listener); return () => { const idx = _listeners.indexOf(listener); if (idx >= 0) _listeners.splice(idx, 1); }; } export function getRegisteredPlugins(): INitroPlugin[] { return [..._plugins]; } // Expose globally so external scripts can use it (window as any).NitroPlugins = pluginApi; export { pluginApi };