/**
* 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 '
';
});
}
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 = '