/** * Haven Plugin & Theme Loader * * Plugins: Drop .plugin.js files into Haven/plugins/ * Themes: Drop .theme.css files into Haven/themes/ * * Plugin format: * /** @name MyPlugin @description Does things @author Me @version 1.0 */ * class MyPlugin { * start() { /* called when enabled */ } * stop() { /* called when disabled */ } * } * * Theme format: * /** @name MyTheme @description Dark neon @author Me @version 1.0 */ * :root { --bg-primary: #000; ... } */ window.HavenPluginLoader = (function () { 'use strict'; // ═══════════════════════════════════════════════════════ // HavenApi: exposed to plugins as window.HavenApi // ═══════════════════════════════════════════════════════ const HavenApi = { // ── DOM helpers ── DOM: { /** Add a CSS class string to
*/ addStyle(id, css) { this.removeStyle(id); const el = document.createElement('style'); el.id = `haven-plugin-style-${id}`; el.textContent = css; document.head.appendChild(el); return el; }, removeStyle(id) { document.getElementById(`haven-plugin-style-${id}`)?.remove(); }, /** Query inside the app */ query(sel) { return document.querySelector(sel); }, queryAll(sel) { return [...document.querySelectorAll(sel)]; }, }, // Structural layout plugins are mutually exclusive. Acquiring the shared // owner before moving regions prevents two reversible plugins from saving // each other's temporary DOM as their native restore point. Layout: { _owner: null, _reservedOwner: null, acquire(owner) { const id = String(owner || '').trim(); if (!id || (this._reservedOwner && this._reservedOwner !== id) || (this._owner && this._owner !== id)) return false; if (this._owner === id) return true; this._owner = id; document.documentElement.setAttribute('data-haven-layout-owner', id); document.dispatchEvent(new CustomEvent('haven:layout-owner-change', { detail: { owner: id } })); return true; }, release(owner) { if (this._owner !== String(owner || '').trim()) return false; this._owner = null; document.documentElement.removeAttribute('data-haven-layout-owner'); document.dispatchEvent(new CustomEvent('haven:layout-owner-change', { detail: { owner: null } })); return true; }, _reserve(owner) { this._reservedOwner = String(owner || '').trim() || null; }, _clearReservation(owner) { const id = String(owner || '').trim() || null; if (this._reservedOwner !== id) return false; this._reservedOwner = null; return true; }, get owner() { return this._owner; }, }, // ── Data (localStorage wrapper) ── Data: { save(pluginName, key, value) { const store = JSON.parse(localStorage.getItem('haven_plugin_data') || '{}'); if (!store[pluginName]) store[pluginName] = {}; store[pluginName][key] = value; localStorage.setItem('haven_plugin_data', JSON.stringify(store)); }, load(pluginName, key, fallback = null) { const store = JSON.parse(localStorage.getItem('haven_plugin_data') || '{}'); return store[pluginName]?.[key] ?? fallback; }, delete(pluginName, key) { const store = JSON.parse(localStorage.getItem('haven_plugin_data') || '{}'); if (store[pluginName]) { delete store[pluginName][key]; } localStorage.setItem('haven_plugin_data', JSON.stringify(store)); }, }, // ── UI helpers ── UI: { showToast(message, type = 'info') { if (window.app && window.app._showToast) { window.app._showToast(message, type); } }, /** Show a simple confirm dialog; returns a Promise${t('settings.plugins_section.no_plugins')}
`; } else { container.innerHTML = ''; for (const [file, p] of loadedPlugins) { const card = document.createElement('div'); card.className = 'plugin-card'; const suppressedLabel = suppressExtensions && p.enabled ? ` • ${escHtml(t('settings.plugins_section.suppressed_safe_mode'))}` : ''; const toggleDisabled = suppressExtensions && !p.enabled; card.innerHTML = `No themes found. Drop .theme.css files into the themes/ folder.