verstak/frontend/src/lib/PluginPage.svelte

241 lines
7.3 KiB
Svelte

<script>
import { onMount, onDestroy } from 'svelte'
export let sectionId = ''
// Parse "plugin:name:pageId"
$: parts = sectionId.split(':')
$: pluginName = parts[1] || ''
$: pageId = parts[2] || 'main'
$: pageLabel = pageId.charAt(0).toUpperCase() + pageId.slice(1)
let htmlPanel = ''
let loading = true
let error = ''
let iframeEl = null
let messageQueue = []
let iframeReady = false
function wailsCall(method, ...args) {
try {
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
const fn = window['go']['main']['App'][method]
if (typeof fn === 'function') return fn(...args)
}
} catch (e) { console.error('Wails error:', method, e) }
return Promise.reject(new Error('Wails not connected: ' + method))
}
// Post message to iframe (queue if not ready)
function postToIframe(msg) {
if (iframeEl && iframeEl.contentWindow && iframeReady) {
iframeEl.contentWindow.postMessage(msg, '*')
} else {
messageQueue.push(msg)
}
}
// Handle messages from iframe — only accept from our own iframeEl
// The iframe identifies itself via msg.source (set by the panel HTML).
// We accept any source that starts with "plugin:" to support generic plugin panels.
function handleIframeMessage(e) {
// Verify the message comes from our iframe (srcdoc = same origin)
if (!iframeEl || !iframeEl.contentWindow || e.source !== iframeEl.contentWindow) return
const msg = e.data
if (!msg || typeof msg !== 'object') return
if (!msg.source || typeof msg.source !== 'string') return
if (!msg.action || typeof msg.action !== 'string') return
switch (msg.action) {
case 'ready':
iframeReady = true
// Flush queued messages
while (messageQueue.length > 0) {
iframeEl.contentWindow.postMessage(messageQueue.shift(), '*')
}
// Load initial data
loadCalendarData()
break
case 'get-events':
handleGetEvents(msg.data)
break
case 'create-event':
handleCreateEvent(msg.data)
break
case 'update-event':
handleUpdateEvent(msg.data)
break
case 'delete-event':
handleDeleteEvent(msg.data)
break
default:
console.log('[PluginPage] Unknown iframe action:', msg.action)
}
}
// Build the Lua function prefix from plugin name (e.g. "calendar" → "calendar.")
$: funcPrefix = pluginName ? pluginName + '.' : ''
// Load events + categories from Lua backend and send to iframe
async function loadCalendarData() {
try {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth()
const start = new Date(year, month, 1).toISOString().slice(0, 10) + 'T00:00:00'
const end = new Date(year, month + 1, 0).toISOString().slice(0, 10) + 'T23:59:59'
const [eventsRaw, categoriesRaw] = await Promise.all([
wailsCall('CallPluginFunction', pluginName, funcPrefix + 'get_events', JSON.stringify({ start, end })),
wailsCall('CallPluginFunction', pluginName, funcPrefix + 'get_categories', '{}'),
])
const events = eventsRaw ? JSON.parse(eventsRaw) : []
const categories = categoriesRaw ? JSON.parse(categoriesRaw) : []
postToIframe({
source: 'verstak',
type: 'calendar-data',
events,
categories,
})
} catch (e) {
console.error('[PluginPage] loadCalendarData:', e)
}
}
async function handleGetEvents(data) {
try {
const params = JSON.stringify({ start: data.start, end: data.end })
const raw = await wailsCall('CallPluginFunction', pluginName, funcPrefix + 'get_events', params)
const events = raw ? JSON.parse(raw) : []
postToIframe({
source: 'verstak',
type: 'calendar-data',
events,
})
} catch (e) {
console.error('[PluginPage] get-events:', e)
}
}
async function handleCreateEvent(data) {
try {
const params = JSON.stringify(data)
const raw = await wailsCall('CallPluginFunction', pluginName, funcPrefix + 'create_event', params)
const result = raw ? JSON.parse(raw) : {}
postToIframe({
source: 'verstak',
type: 'event-created',
event: result,
})
loadCalendarData()
} catch (e) {
console.error('[PluginPage] create-event:', e)
postToIframe({
source: 'verstak',
type: 'error',
message: String(e),
})
}
}
async function handleUpdateEvent(data) {
try {
const params = JSON.stringify(data)
await wailsCall('CallPluginFunction', pluginName, funcPrefix + 'update_event', params)
postToIframe({
source: 'verstak',
type: 'event-updated',
event: data,
})
loadCalendarData()
} catch (e) {
console.error('[PluginPage] update-event:', e)
}
}
async function handleDeleteEvent(data) {
try {
const params = JSON.stringify({ id: data.id })
await wailsCall('CallPluginFunction', pluginName, funcPrefix + 'delete_event', params)
postToIframe({
source: 'verstak',
type: 'event-deleted',
id: data.id,
})
loadCalendarData()
} catch (e) {
console.error('[PluginPage] delete-event:', e)
}
}
onMount(async () => {
try {
htmlPanel = (await wailsCall('GetPluginPanelHTML', pluginName)) || ''
} catch (e) {
error = String(e)
}
loading = false
// Listen for messages from iframe
window.addEventListener('message', handleIframeMessage)
})
onDestroy(() => {
window.removeEventListener('message', handleIframeMessage)
})
// Expose drop handler for parent Svelte components
export function handleDrop(data, date) {
postToIframe({
source: 'verstak',
type: 'drop',
date: date,
data: data,
})
}
</script>
<div class="plugin-page">
<div class="plugin-page-header">
<h2>{pluginName}{pageLabel}</h2>
</div>
{#if loading}
<p class="loading">Загрузка…</p>
{:else if error}
<p class="error">{error}</p>
{:else if htmlPanel}
<iframe
bind:this={iframeEl}
class="plugin-frame"
srcdoc={htmlPanel}
sandbox="allow-scripts allow-same-origin"
title="{pluginName} panel"
></iframe>
{:else}
<div class="empty-state">
<p>Плагин «{pluginName}» активен, но HTML-панель не настроена.</p>
<p class="hint">Для отображения контента добавьте поле <code>panel</code> в <code>plugin.json</code>.</p>
</div>
{/if}
</div>
<style>
.plugin-page { padding: 1.5rem; height: 100%; display: flex; flex-direction: column; }
.plugin-page-header { margin-bottom: 1rem; flex-shrink: 0; }
.plugin-page-header h2 { margin: 0; font-size: 1.1rem; color: var(--text, #e0e0e0); }
.loading { color: var(--text-dim, #888); }
.error { color: #f87171; }
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-dim, #888); }
.empty-state code { background: var(--surface-alt, #252538); padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.85rem; }
.plugin-frame { flex: 1; border: 1px solid var(--border, #2a2a3e); border-radius: 8px; background: #fff; width: 100%; min-height: 400px; display: block; }
</style>