149 lines
3.8 KiB
JavaScript
149 lines
3.8 KiB
JavaScript
import MeridianWM from './wm.js';
|
|
import State from './state.js';
|
|
import Bus from './bus.js';
|
|
|
|
const Registry = (() => {
|
|
const APPS = {};
|
|
let _launcherOpen = false;
|
|
|
|
function registerApp(def) {
|
|
if (def.id) {
|
|
APPS[def.id] = def;
|
|
}
|
|
}
|
|
|
|
function unregisterApp(id) {
|
|
delete APPS[id];
|
|
}
|
|
|
|
function getApp(id) {
|
|
return APPS[id] || null;
|
|
}
|
|
|
|
function launchApp(id) {
|
|
const def = APPS[id];
|
|
if (!def) {
|
|
console.warn(`App ${id} not registered`);
|
|
return;
|
|
}
|
|
|
|
const title = def.title || id;
|
|
const w = def.width || 640;
|
|
const h = def.height || 480;
|
|
|
|
// Create window with a render-callback; wm will mount container then call it.
|
|
const win = MeridianWM.openWindow({
|
|
app: id,
|
|
title,
|
|
w, h,
|
|
content: (container) => {
|
|
// Tag container for per-app CSS scoping
|
|
container.setAttribute('data-app', id);
|
|
const api = {
|
|
state: State,
|
|
bus: Bus,
|
|
close: () => MeridianWM.closeWindow(win && win.id),
|
|
setTitle: (t) => {
|
|
const el = document.getElementById(win && win.id);
|
|
if (el) {
|
|
const titleEl = el.querySelector('.m-window-title');
|
|
if (titleEl) titleEl.textContent = t;
|
|
}
|
|
},
|
|
};
|
|
try {
|
|
def.render(container, api);
|
|
} catch (e) {
|
|
container.textContent = `Error loading ${id}: ${e.message}`;
|
|
console.error(`[${id}] render error`, e);
|
|
}
|
|
},
|
|
});
|
|
closeLauncher();
|
|
}
|
|
|
|
function getRegisteredApps() {
|
|
return Object.values(APPS);
|
|
}
|
|
|
|
function toggleLauncher() {
|
|
_launcherOpen = !_launcherOpen;
|
|
const popover = document.getElementById('launcher-popover');
|
|
const btn = document.getElementById('launcher');
|
|
if (popover) {
|
|
if (_launcherOpen) {
|
|
renderLauncherApps(popover);
|
|
popover.classList.remove('hidden');
|
|
} else {
|
|
popover.classList.add('hidden');
|
|
}
|
|
}
|
|
if (btn) {
|
|
btn.classList.toggle('active', _launcherOpen);
|
|
}
|
|
return _launcherOpen;
|
|
}
|
|
|
|
function closeLauncher() {
|
|
_launcherOpen = false;
|
|
const popover = document.getElementById('launcher-popover');
|
|
const btn = document.getElementById('launcher');
|
|
if (popover) popover.classList.add('hidden');
|
|
if (btn) btn.classList.remove('active');
|
|
}
|
|
|
|
function renderLauncherApps(popover) {
|
|
const apps = getRegisteredApps();
|
|
popover.innerHTML = '';
|
|
if (apps.length === 0) {
|
|
popover.innerHTML = '<div style="padding:12px;text-align:center;color:var(--text-faint);font-size:13px;">No apps registered</div>';
|
|
return;
|
|
}
|
|
for (const app of apps) {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'launcher-app-btn';
|
|
btn.setAttribute('aria-label', `Launch ${app.title || app.id}`);
|
|
btn.innerHTML = `
|
|
<div class="launcher-app-icon">${app.icon || '◆'}</div>
|
|
<div class="launcher-app-label">${escapeHtml(app.title || app.id)}</div>
|
|
`;
|
|
btn.addEventListener('click', () => launchApp(app.id));
|
|
popover.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function init() {
|
|
const btn = document.getElementById('launcher');
|
|
if (btn) {
|
|
btn.addEventListener('click', toggleLauncher);
|
|
}
|
|
|
|
// Close launcher when clicking elsewhere
|
|
document.addEventListener('mousedown', (e) => {
|
|
if (_launcherOpen && !e.target.closest('#launcher') && !e.target.closest('#launcher-popover')) {
|
|
closeLauncher();
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
APPS,
|
|
registerApp,
|
|
unregisterApp,
|
|
getApp,
|
|
launchApp,
|
|
getRegisteredApps,
|
|
toggleLauncher,
|
|
init
|
|
};
|
|
})();
|
|
|
|
export default Registry;
|
|
export { Registry };
|