mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
ui plugins
This commit is contained in:
@@ -22,6 +22,7 @@ import { ModToolsView } from './mod-tools/ModToolsView';
|
|||||||
import { NavigatorView } from './navigator/NavigatorView';
|
import { NavigatorView } from './navigator/NavigatorView';
|
||||||
import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView';
|
import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView';
|
||||||
import { NitropediaView } from './nitropedia/NitropediaView';
|
import { NitropediaView } from './nitropedia/NitropediaView';
|
||||||
|
import { ExternalPluginLoader } from './plugins/ExternalPluginLoader';
|
||||||
import { RightSideView } from './right-side/RightSideView';
|
import { RightSideView } from './right-side/RightSideView';
|
||||||
import { RoomView } from './room/RoomView';
|
import { RoomView } from './room/RoomView';
|
||||||
import { ToolbarView } from './toolbar/ToolbarView';
|
import { ToolbarView } from './toolbar/ToolbarView';
|
||||||
@@ -118,6 +119,7 @@ export const MainView: FC<{}> = props =>
|
|||||||
<FloorplanEditorView />
|
<FloorplanEditorView />
|
||||||
<FurniEditorView />
|
<FurniEditorView />
|
||||||
<YoutubeTvView />
|
<YoutubeTvView />
|
||||||
|
<ExternalPluginLoader />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { GetConfigurationValue } from '../../api';
|
||||||
|
import { subscribePlugins } from './NitroPluginApi';
|
||||||
|
|
||||||
|
// Force the global API to be initialized
|
||||||
|
import './NitroPluginApi';
|
||||||
|
|
||||||
|
export const ExternalPluginLoader: FC<{}> = () =>
|
||||||
|
{
|
||||||
|
const [, forceUpdate] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
return subscribePlugins(() => forceUpdate(n => n + 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// MainView only renders after isReady=true in App.tsx,
|
||||||
|
// so the configuration is guaranteed to be loaded at this point.
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const scripts: HTMLScriptElement[] = [];
|
||||||
|
|
||||||
|
let pluginUrls: string[] = [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pluginUrls = GetConfigurationValue<string[]>('external.plugins', []);
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.warn('[NitroPlugins] Could not read external.plugins config:', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginUrls || pluginUrls.length === 0)
|
||||||
|
{
|
||||||
|
console.log('[NitroPlugins] No external plugins configured');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[NitroPlugins] Loading external plugins:', pluginUrls);
|
||||||
|
|
||||||
|
for (const url of pluginUrls)
|
||||||
|
{
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = url;
|
||||||
|
script.async = true;
|
||||||
|
script.onload = () => console.log(`[NitroPlugins] Loaded: ${url}`);
|
||||||
|
script.onerror = () => console.warn(`[NitroPlugins] Failed to load: ${url}`);
|
||||||
|
document.head.appendChild(script);
|
||||||
|
scripts.push(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
scripts.forEach(s => s.remove());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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 };
|
||||||
Reference in New Issue
Block a user