Files
Nitro-V3/src/components/plugins/NitroPluginApi.ts
T
2026-03-15 14:01:25 +01:00

194 lines
6.0 KiB
TypeScript

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<typeof GetRoomEngine>;
/** Get the current room session */
getRoomSession: () => ReturnType<typeof GetRoomSession>;
/** 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 };