/** * javascript/templates.js * * The "Apps" panel: two sub-views toggled by a segmented control — * "Store" (browses/searches Community Applications' own public app feed * directly, see ajax/templates.php's ca_feed_search()) and "My Templates" * (this plugin's own saved, reusable container configs, XML in the same * schema Unraid's own Docker Manager templates use). "Use"/"Install" both * hand off to containers.js's Create Container modal, pre-filled; * templates are themselves created either from that same modal's "Save as * template" checkbox, or by installing a Store app (which saves it as a * template too, so it shows up under My Templates afterward). */ (function () { 'use strict'; const P = window.Podman; let allTemplates = []; let activeSubview = 'store'; let storeResults = []; let storeSearchTimer = null; function iconHtml(t) { if (t.icon) { return ''; } return '
' + P.escapeHtml(t.name.slice(0, 1).toUpperCase()) + '
'; } // --- My Templates -------------------------------------------------------- function cardHtml(t) { const overview = t.overview && t.overview.length > 110 ? t.overview.slice(0, 107) + '…' : (t.overview || ''); return '' + '
' + iconHtml(t) + '
' + '
' + P.escapeHtml(t.name) + '
' + '
' + P.escapeHtml(t.image) + '
' + (overview ? '
' + P.escapeHtml(overview) + '
' : '') + '
' + '
' + '' + '' + '' + '
'; } function renderTemplatesGrid() { const grid = P.el('templates-grid'); grid.innerHTML = allTemplates.length ? allTemplates.map(cardHtml).join('') : '
No templates yet — save one from the "New Container" form, install one from the Store, or import an XML template.
'; } function load() { return P.get('templates', 'list').then(function (data) { allTemplates = data; renderTemplatesGrid(); }).catch(function (err) { P.el('templates-grid').innerHTML = '
' + P.escapeHtml(err.message) + '
'; }); } function handleTemplatesGridClick(e) { const btn = e.target.closest('button[data-action]'); if (!btn) return; const name = btn.closest('.podman-template-card').dataset.name; if (btn.dataset.action === 'use') { btn.disabled = true; P.get('templates', 'get', { name: name }).then(function (config) { btn.disabled = false; P.openCreateContainerModal(config); }).catch(function (err) { btn.disabled = false; P.toast('Could not load template: ' + err.message, 'error'); }); return; } if (btn.dataset.action === 'export') { btn.disabled = true; P.get('templates', 'export', { name: name }).then(function (data) { btn.disabled = false; const blob = new Blob([data.xml], { type: 'application/xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name + '.xml'; a.click(); URL.revokeObjectURL(url); }).catch(function (err) { btn.disabled = false; P.toast('Export failed: ' + err.message, 'error'); }); return; } if (btn.dataset.action === 'delete') { P.confirm('Delete template "' + name + '"? This does not affect any running containers.', { danger: true, confirmLabel: 'Delete' }).then(function (ok) { if (!ok) return; btn.disabled = true; P.post('templates', 'remove', { name: name }).then(load).catch(function (err) { btn.disabled = false; P.toast('Delete failed: ' + err.message, 'error'); }); }); } } // --- Store ----------------------------------------------------------------- function storeCardHtml(a) { const overview = a.overview && a.overview.length > 110 ? a.overview.slice(0, 107) + '…' : (a.overview || ''); return '' + '
' + iconHtml(a) + '
' + '
' + P.escapeHtml(a.name) + '
' + '
' + P.escapeHtml(a.image) + '
' + (overview ? '
' + P.escapeHtml(overview) + '
' : '') + '
' + '
' + '' + '
'; } let storeQuery = ''; let storeSort = 'newest'; let storePage = 1; let storeTotalPages = 1; function renderStoreGrid() { const grid = P.el('store-grid'); grid.innerHTML = storeResults.length ? storeResults.map(storeCardHtml).join('') : '
No matches.
'; } function renderStorePager() { P.el('store-pager').style.display = storeTotalPages > 1 ? '' : 'none'; P.el('store-pager-label').textContent = 'Page ' + storePage + ' of ' + storeTotalPages; P.el('store-pager-prev').disabled = storePage <= 1; P.el('store-pager-next').disabled = storePage >= storeTotalPages; } // The sort toggle only means anything while browsing (no search term) — // a search is always alphabetical (see ca_feed_search()'s own comment on // why "newest"/downloads-based ordering doesn't make sense for a filtered // result set), so the toggle is hidden rather than left present but inert. function updateSortToggleVisibility() { P.el('store-sort-toggle').style.display = storeQuery ? 'none' : ''; } function loadStore() { const grid = P.el('store-grid'); grid.innerHTML = '
Loading…
'; updateSortToggleVisibility(); return P.get('templates', 'apps_search', { q: storeQuery, page: storePage, sort: storeSort }).then(function (data) { storeResults = data.results; storePage = data.page; storeTotalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); renderStoreGrid(); renderStorePager(); }).catch(function (err) { grid.innerHTML = '
' + P.escapeHtml(err.message) + '
'; }); } function handleStoreGridClick(e) { const btn = e.target.closest('button[data-action="install"]'); if (!btn) return; const templateUrl = btn.closest('.podman-template-card').dataset.templateUrl; btn.disabled = true; btn.textContent = 'Installing…'; P.post('templates', 'import_url', { url: templateUrl }).then(function (result) { return P.get('templates', 'get', { name: result.name }); }).then(function (config) { btn.disabled = false; btn.textContent = 'Install'; P.openCreateContainerModal(config); load(); // refreshes "My Templates" in the background — it's now saved there too }).catch(function (err) { btn.disabled = false; btn.textContent = 'Install'; P.toast('Install failed: ' + err.message, 'error'); }); } // --- Import Template modal --------------------------------------------- /** * A modal (not its own tab — tried that, but a modal is enough room for * this and keeps the nav from growing another entry for what's really a * secondary action off "My Templates"). */ function openImportModal() { const backdrop = document.createElement('div'); backdrop.className = 'podman-modal-backdrop'; backdrop.innerHTML = '' + ''; (document.querySelector('.podman-plugin') || document.body).appendChild(backdrop); let localTemplates = []; function renderLocalList(filter) { const list = backdrop.querySelector('#ti-local-list'); const visible = filter ? localTemplates.filter(function (t) { return t.name.toLowerCase().indexOf(filter) !== -1; }) : localTemplates; if (!visible.length) { list.innerHTML = '
' + (localTemplates.length ? 'No match.' : 'None found.') + '
'; return; } list.innerHTML = visible.map(function (t) { return '
' + '' + P.escapeHtml(t.name) + '' + '' + P.escapeHtml(t.image) + '' + '' + '
'; }).join(''); } P.get('templates', 'list_local').then(function (data) { localTemplates = data; renderLocalList(''); }).catch(function () { backdrop.querySelector('#ti-local-list').innerHTML = '
Could not read local templates.
'; }); backdrop.querySelector('#ti-local-search').addEventListener('input', function (e) { renderLocalList(e.target.value.trim().toLowerCase()); }); backdrop.querySelector('#ti-local-list').addEventListener('click', function (e) { const btn = e.target.closest('[data-action="import-local"]'); if (!btn) return; const file = btn.closest('.podman-local-template-item').dataset.file; btn.disabled = true; P.post('templates', 'import_local', { file: file }).then(function () { close(); return load(); }).catch(function (err) { btn.disabled = false; showError(err.message); }); }); backdrop.querySelector('#ti-xml').focus(); function close() { backdrop.remove(); } function showError(message) { let box = backdrop.querySelector('.podman-modal-error'); if (!box) { box = document.createElement('div'); box.className = 'podman-modal-error'; backdrop.querySelector('.podman-modal-body').appendChild(box); } box.textContent = message; } function submit() { const xml = backdrop.querySelector('#ti-xml').value.trim(); if (!xml) { showError('Paste a template XML first.'); return; } const submitBtn = backdrop.querySelector('[data-role="submit"]'); submitBtn.disabled = true; P.post('templates', 'import', { xml: xml }).then(function () { close(); return load(); }).catch(function (err) { submitBtn.disabled = false; showError(err.message); }); } function submitUrl() { const url = backdrop.querySelector('#ti-url').value.trim(); if (!url) { showError('Enter a URL first.'); return; } const fetchBtn = backdrop.querySelector('[data-role="fetch-url"]'); fetchBtn.disabled = true; P.post('templates', 'import_url', { url: url }).then(function () { close(); return load(); }).catch(function (err) { fetchBtn.disabled = false; showError(err.message); }); } backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close); backdrop.querySelector('[data-role="submit"]').addEventListener('click', submit); backdrop.querySelector('[data-role="fetch-url"]').addEventListener('click', submitUrl); backdrop.querySelector('form').addEventListener('submit', function (e) { e.preventDefault(); submit(); }); backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); }); document.addEventListener('keydown', function onKey(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } }); } // --- Shell / sub-view toggle -------------------------------------------- function switchSubview(view) { activeSubview = view; document.querySelectorAll('#apps-subview-toggle button').forEach(function (b) { b.classList.toggle('active', b.dataset.view === view); }); P.el('apps-store-view').style.display = view === 'store' ? '' : 'none'; P.el('apps-templates-view').style.display = view === 'templates' ? '' : 'none'; P.el('apps-store-search').style.display = view === 'store' ? '' : 'none'; P.el('templates-import-btn').style.display = view === 'templates' ? '' : 'none'; if (view === 'store') { updateSortToggleVisibility(); } else { P.el('store-sort-toggle').style.display = 'none'; } } function init() { const container = P.el('podman-panel-templates'); container.innerHTML = '' + '
' + '
' + '
' + '' + '' + '
' + '' + '
' + '' + '' + '
' + '' + '
' + '
' + '
' + '' + '
' + '' + '
'; document.querySelectorAll('#apps-subview-toggle button').forEach(function (b) { b.addEventListener('click', function () { switchSubview(b.dataset.view); }); }); P.el('apps-store-search').addEventListener('input', function (e) { storeQuery = e.target.value.trim(); storePage = 1; if (storeSearchTimer) clearTimeout(storeSearchTimer); storeSearchTimer = setTimeout(loadStore, 400); }); document.querySelectorAll('#store-sort-toggle button').forEach(function (b) { b.addEventListener('click', function () { storeSort = b.dataset.sort; storePage = 1; document.querySelectorAll('#store-sort-toggle button').forEach(function (x) { x.classList.toggle('active', x === b); }); loadStore(); }); }); P.el('store-pager-prev').addEventListener('click', function () { if (storePage <= 1) return; storePage -= 1; loadStore(); }); P.el('store-pager-next').addEventListener('click', function () { if (storePage >= storeTotalPages) return; storePage += 1; loadStore(); }); P.el('store-grid').addEventListener('click', handleStoreGridClick); P.el('templates-grid').addEventListener('click', handleTemplatesGridClick); P.el('templates-import-btn').addEventListener('click', openImportModal); loadStore(); return load(); } P.registerPanel('templates', { init: init, refresh: load }); })();