/** * Plugin File Manager Widget * * Reusable inline file manager for plugins that manage files via the * web_ui_actions system. Driven entirely by x-widget-config in the schema — * no external HTML file or iframe needed. * * Any plugin can adopt this widget by: * 1. Defining web_ui_actions in manifest.json (list, get, save, upload, * delete, create, toggle) with ui_hidden: true * 2. Adding x-widget: "plugin-file-manager" to a field in config_schema.json * with x-widget-config mapping the action IDs * * Schema example: * { * "file_manager": { * "type": "null", * "title": "Data Files", * "x-widget": "plugin-file-manager", * "x-widget-config": { * "actions": { * "list": "list-files", * "get": "get-file", * "save": "save-file", * "upload": "upload-file", * "delete": "delete-file", * "create": "create-file", * "toggle": "toggle-category" * }, * "upload_hint": "JSON files with day numbers 1–365 as keys", * "directory_label": "of_the_day/", * "create_fields": [ * { "key": "category_name", "label": "Category Name", * "placeholder": "e.g., my_words", "pattern": "^[a-z0-9_]+$", * "hint": "Lowercase letters, numbers, underscores" }, * { "key": "display_name", "label": "Display Name", * "placeholder": "e.g., My Words", "hint": "Optional — auto-generated if blank" } * ] * } * } * } * * @module PluginFileManagerWidget */ (function () { 'use strict'; if (typeof window.LEDMatrixWidgets === 'undefined') { console.error('[PluginFileManager] LEDMatrixWidgets registry not found.'); return; } // ─── Inject widget-scoped styles once ──────────────────────────────────── if (!document.getElementById('pfm-styles')) { const style = document.createElement('style'); style.id = 'pfm-styles'; style.textContent = ` .pfm-root { font-family: inherit; } .pfm-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:.75rem; } .pfm-title { font-size:1rem; font-weight:600; color:#111827; } .pfm-dir { font-size:.75rem; color:#6b7280; margin-top:.125rem; } .pfm-upload { border:2px dashed #d1d5db; border-radius:.5rem; padding:1.25rem; text-align:center; cursor:pointer; transition:border-color .15s,background .15s; } .pfm-upload:hover,.pfm-upload.dragover { border-color:#3b82f6; background:#eff6ff; } .pfm-upload p { font-size:.875rem; color:#4b5563; margin:.25rem 0 0; } .pfm-upload small { font-size:.75rem; color:#9ca3af; } .pfm-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:.75rem; margin-top:.75rem; } .pfm-card { border:1px solid #e5e7eb; border-radius:.5rem; padding:.875rem; background:#fff; transition:box-shadow .15s; } .pfm-card:hover { box-shadow:0 1px 4px rgba(0,0,0,.1); } .pfm-card.disabled { opacity:.55; } .pfm-card-top { display:flex; align-items:center; justify-content:space-between; margin-bottom:.5rem; } .pfm-card-icon { width:2rem; height:2rem; background:#f3f4f6; border-radius:.375rem; display:flex; align-items:center; justify-content:center; color:#6b7280; font-size:1rem; } .pfm-card-name { font-weight:600; color:#111827; font-size:.875rem; margin:.375rem 0 .125rem; } .pfm-card-meta { font-size:.75rem; color:#6b7280; line-height:1.5; } .pfm-card-actions { display:flex; gap:.375rem; margin-top:.625rem; } .pfm-btn { display:inline-flex; align-items:center; gap:.25rem; padding:.375rem .75rem; border-radius:.375rem; font-size:.8125rem; font-weight:500; border:none; cursor:pointer; transition:background .15s; } .pfm-btn-primary { background:#2563eb; color:#fff; flex:1; justify-content:center; } .pfm-btn-primary:hover { background:#1d4ed8; } .pfm-btn-danger { background:#dc2626; color:#fff; } .pfm-btn-danger:hover { background:#b91c1c; } .pfm-btn-secondary { background:#f3f4f6; color:#374151; border:1px solid #d1d5db; } .pfm-btn-secondary:hover { background:#e5e7eb; } .pfm-btn-sm { padding:.25rem .5rem; font-size:.75rem; } .pfm-btn-create { background:#059669; color:#fff; } .pfm-btn-create:hover { background:#047857; } .pfm-toggle-wrap { display:flex; align-items:center; gap:.375rem; } .pfm-toggle-label { font-size:.75rem; color:#6b7280; } .pfm-toggle-cb { position:relative; display:inline-block; width:2rem; height:1.125rem; } .pfm-toggle-cb input { opacity:0; width:0; height:0; } .pfm-toggle-slider { position:absolute; inset:0; background:#d1d5db; border-radius:9999px; cursor:pointer; transition:background .2s; } .pfm-toggle-slider:before { content:''; position:absolute; height:.75rem; width:.75rem; left:.1875rem; bottom:.1875rem; background:#fff; border-radius:50%; transition:transform .2s; } .pfm-toggle-cb input:checked + .pfm-toggle-slider { background:#10b981; } .pfm-toggle-cb input:checked + .pfm-toggle-slider:before { transform:translateX(.875rem); } .pfm-empty { text-align:center; padding:2rem; color:#9ca3af; } .pfm-empty i { font-size:2rem; margin-bottom:.5rem; display:block; } /* Modal */ .pfm-overlay { position:fixed; inset:0; background:rgba(0,0,0,.5); display:flex; align-items:flex-start; justify-content:center; z-index:9999; padding:2rem 1rem; overflow-y:auto; } .pfm-modal { background:#fff; border-radius:.75rem; width:100%; max-width:56rem; box-shadow:0 20px 50px rgba(0,0,0,.3); margin:auto; } .pfm-modal-header { display:flex; align-items:center; justify-content:space-between; padding:1rem 1.25rem; border-bottom:1px solid #e5e7eb; } .pfm-modal-title { font-size:1rem; font-weight:600; color:#111827; } .pfm-modal-body { padding:1.25rem; overflow-y:auto; max-height:70vh; } .pfm-modal-footer { display:flex; justify-content:flex-end; gap:.5rem; padding:.875rem 1.25rem; border-top:1px solid #e5e7eb; background:#f9fafb; border-radius:0 0 .75rem .75rem; } /* Entry table */ .pfm-table-wrap { overflow-x:auto; } .pfm-table { width:100%; border-collapse:collapse; font-size:.8125rem; } .pfm-table th { background:#f9fafb; text-align:left; padding:.5rem .625rem; font-weight:600; color:#374151; border-bottom:1px solid #e5e7eb; white-space:nowrap; position:sticky; top:0; } .pfm-table td { padding:.375rem .625rem; border-bottom:1px solid #f3f4f6; vertical-align:top; } .pfm-table tr.today-row td { background:#fef9c3; } .pfm-table td input, .pfm-table td textarea { width:100%; border:1px solid #d1d5db; border-radius:.25rem; padding:.25rem .375rem; font-size:.8125rem; font-family:inherit; resize:vertical; background:#fff; } .pfm-table td input:focus, .pfm-table td textarea:focus { outline:none; border-color:#3b82f6; } .pfm-day-col { width:3rem; text-align:center; font-weight:600; color:#6b7280; white-space:nowrap; } .pfm-pagination { display:flex; align-items:center; justify-content:space-between; margin-top:.75rem; font-size:.8125rem; color:#6b7280; } .pfm-page-jump { display:flex; align-items:center; gap:.375rem; font-size:.8125rem; } .pfm-page-jump input { width:3.5rem; padding:.25rem .375rem; border:1px solid #d1d5db; border-radius:.25rem; text-align:center; } /* Form in create modal */ .pfm-field { margin-bottom:.875rem; } .pfm-field label { display:block; font-size:.875rem; font-weight:500; color:#374151; margin-bottom:.25rem; } .pfm-field input { width:100%; padding:.4rem .625rem; border:1px solid #d1d5db; border-radius:.375rem; font-size:.875rem; } .pfm-field input:focus { outline:none; border-color:#3b82f6; } .pfm-field-hint { font-size:.75rem; color:#9ca3af; margin-top:.2rem; } .pfm-field-error { font-size:.75rem; color:#dc2626; margin-top:.2rem; } /* Delete danger box */ .pfm-danger-box { background:#fef2f2; border:1px solid #fecaca; border-radius:.5rem; padding:.875rem; font-size:.875rem; color:#991b1b; } `; document.head.appendChild(style); } // ─── Per-instance state ─────────────────────────────────────────────────── const _state = new Map(); // fieldId → { pluginId, actions, createFields, files, page, entriesPerPage, modal } function getState(fieldId) { if (!_state.has(fieldId)) _state.set(fieldId, { pluginId: '', actions: {}, createFields: [], uploadHint: '', directoryLabel: '', files: [], page: 1, entriesPerPage: 20, currentModal: null }); return _state.get(fieldId); } // ─── API helper ─────────────────────────────────────────────────────────── async function callAction(pluginId, actionId, params = {}) { const resp = await fetch('/api/v3/plugins/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ plugin_id: pluginId, action_id: actionId, params }) }); return resp.json(); } function notify(msg, type) { if (window.showNotification) window.showNotification(msg, type); else console.log(`[PFM][${type}] ${msg}`); } function escHtml(s) { const d = document.createElement('div'); d.textContent = String(s ?? ''); return d.innerHTML; } function formatSize(bytes) { if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB'; return (bytes / 1024).toFixed(2) + ' KB'; } function formatDate(iso) { try { return new Date(iso).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' }); } catch { return iso; } } // ─── Core: load files ───────────────────────────────────────────────────── async function loadFiles(fieldId) { const st = getState(fieldId); const root = document.getElementById(`${fieldId}_pfm`); if (!root) return; const grid = root.querySelector('.pfm-grid'); if (grid) grid.innerHTML = '