Compare commits
2 Commits
8d193eda28
...
e7b299b9cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7b299b9cc | ||
|
|
d39852aa3d |
@ -4,12 +4,12 @@ const path = require('path');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const FEATURES_DIR = path.join(__dirname, '..', '..', 'frontend', 'features');
|
||||
const FEATURES_DIR = path.join(__dirname, '..', '..', 'frontend', 'components');
|
||||
|
||||
/* =========================
|
||||
GET /api/features/list
|
||||
|
||||
Walks frontend/features/<id>/ and returns one entry per directory that
|
||||
Walks frontend/components/<id>/ and returns one entry per directory that
|
||||
contains a feature.json — the WebUI's page manifest, discovered from the
|
||||
folders themselves (exactly how /api/themes/list discovers themes). Drop a
|
||||
features/<id>/ folder in and its page appears; delete it and the page is
|
||||
|
||||
@ -401,14 +401,14 @@ class AdminSystem {
|
||||
return `<div class="sys-stat"><span class="sys-stat-label">${this.escape(label)}</span><strong class="sys-stat-value">${this.escape(value)}</strong></div>`;
|
||||
}
|
||||
|
||||
// OS stat with a distro icon (bundled under /icons/os/, generic Linux glyph
|
||||
// OS stat with a distro icon (bundled under /core/icons/os/, generic Linux glyph
|
||||
// for anything we don't have a logo for).
|
||||
_osStat(os) {
|
||||
const slug = String(os || '').toLowerCase().replace(/[^a-z0-9]/g, '') || 'linux';
|
||||
return `<div class="sys-stat">
|
||||
<span class="sys-stat-label">OS</span>
|
||||
<strong class="sys-stat-value sys-id-value">
|
||||
<span class="sys-id-icon"><img src="/icons/os/${slug}.svg" alt="" onerror="this.onerror=null;this.src='/icons/os/linux.svg'"></span>
|
||||
<span class="sys-id-icon"><img src="/core/icons/os/${slug}.svg" alt="" onerror="this.onerror=null;this.src='/core/icons/os/linux.svg'"></span>
|
||||
${this.escape(os || '—')}
|
||||
</strong>
|
||||
</div>`;
|
||||
@ -430,7 +430,7 @@ class AdminSystem {
|
||||
_cpuStat(cpu) {
|
||||
const raw = String(cpu || '');
|
||||
const vendor = /amd/i.test(raw) ? 'amd' : /intel/i.test(raw) ? 'intel' : null;
|
||||
const icon = vendor ? `<span class="sys-id-icon"><img src="/icons/cpu/${vendor}.svg" alt="${vendor}"></span>` : '';
|
||||
const icon = vendor ? `<span class="sys-id-icon"><img src="/core/icons/cpu/${vendor}.svg" alt="${vendor}"></span>` : '';
|
||||
return `<div class="sys-stat">
|
||||
<span class="sys-stat-label">CPU</span>
|
||||
<strong class="sys-stat-value sys-id-value">${icon}${this.escape(this._cleanCpu(raw))}</strong>
|
||||
@ -44,8 +44,8 @@ if (typeof window.ConfigManager === 'undefined') {
|
||||
try { this.sidebar.populateSidebar(); } catch (e) {}
|
||||
// charts.js is the chart-rendering helper admin-overview pulls in.
|
||||
await Promise.all([
|
||||
lazyLoad('/features/admin/admin-overview.js'),
|
||||
lazyLoad('/features/admin/charts.js')
|
||||
lazyLoad('/components/admin/admin-overview.js'),
|
||||
lazyLoad('/components/admin/charts.js')
|
||||
]);
|
||||
if (typeof AdminOverview !== 'undefined') {
|
||||
window.adminOverview = new AdminOverview('config-section');
|
||||
@ -60,7 +60,7 @@ if (typeof window.ConfigManager === 'undefined') {
|
||||
// a config category — render its own controller into the main pane.
|
||||
if (category === 'ssh-access') {
|
||||
try { this.sidebar.populateSidebar(); } catch (e) {}
|
||||
await lazyLoad('/features/admin/ssh-page.js');
|
||||
await lazyLoad('/components/admin/ssh-page.js');
|
||||
if (typeof SshPage !== 'undefined') {
|
||||
window.sshPage = new SshPage('config-section');
|
||||
await window.sshPage.init();
|
||||
@ -76,9 +76,9 @@ if (typeof window.ConfigManager === 'undefined') {
|
||||
// we inject its content template, then init PeersPage.
|
||||
if (category === 'peers') {
|
||||
try { this.sidebar.populateSidebar(); } catch (e) {}
|
||||
await lazyLoad('/features/admin/peers-page.js');
|
||||
await lazyLoad('/components/admin/peers-page.js');
|
||||
try {
|
||||
const html = await fetch('/html/peers-content.html').then(r => r.text());
|
||||
const html = await fetch('/components/admin/peers-content.html').then(r => r.text());
|
||||
configSection.innerHTML = html;
|
||||
} catch (e) {
|
||||
configSection.innerHTML = '<div class="error">Peers page template failed to load.</div>';
|
||||
@ -100,10 +100,10 @@ if (typeof window.ConfigManager === 'undefined') {
|
||||
if (category === 'system') {
|
||||
try { this.sidebar.populateSidebar(); } catch (e) {}
|
||||
await Promise.all([
|
||||
lazyLoad('/features/admin/charts.js'),
|
||||
lazyLoad('/features/admin/admin-system.js'),
|
||||
lazyLoad('/features/admin/system-metric-page.js'),
|
||||
lazyLoad('/features/admin/system-storage-page.js')
|
||||
lazyLoad('/components/admin/charts.js'),
|
||||
lazyLoad('/components/admin/admin-system.js'),
|
||||
lazyLoad('/components/admin/system-metric-page.js'),
|
||||
lazyLoad('/components/admin/system-storage-page.js')
|
||||
]);
|
||||
if (typeof AdminSystem !== 'undefined') {
|
||||
window.adminSystem = new AdminSystem('config-section');
|
||||
@ -216,7 +216,7 @@ if (typeof window.ConfigManager === 'undefined') {
|
||||
var catIcon = catMeta.icon || category;
|
||||
var headerHTML =
|
||||
'<div class="page-header config-page-header">' +
|
||||
'<img class="page-header-icon" src="/icons/config/' + catIcon + '.svg" alt="" onerror="this.style.display=\'none\'">' +
|
||||
'<img class="page-header-icon" src="/core/icons/config/' + catIcon + '.svg" alt="" onerror="this.style.display=\'none\'">' +
|
||||
'<div class="page-header-title">' +
|
||||
'<div class="admin-breadcrumb">Config</div>' +
|
||||
'<h1>' + catTitle + '</h1>' +
|
||||
@ -88,7 +88,7 @@ class ConfigSidebar {
|
||||
|
||||
// Use correct icon from our new structure
|
||||
const iconName = category.icon || category.id;
|
||||
const iconPath = '/icons/config/' + iconName + '.svg';
|
||||
const iconPath = '/core/icons/config/' + iconName + '.svg';
|
||||
|
||||
categoryItem.innerHTML = '<img src="' + iconPath + '" alt="' + category.title + '" style="width: 20px; height: 20px; margin-right: 8px;"/> ' + category.title;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "admin",
|
||||
"routes": ["/admin", "/admin*"],
|
||||
"module": "/features/admin/index.js",
|
||||
"module": "/components/admin/index.js",
|
||||
"handler": "handleAdmin",
|
||||
"navId": "nav-config",
|
||||
"nav": { "label": "Admin", "order": 40 },
|
||||
@ -16,7 +16,7 @@ LP.features.register({
|
||||
async mount(ctx) {
|
||||
window.configCategory = ctx.services.router.adminCategoryFromPath(window.location.pathname);
|
||||
|
||||
const html = await ctx.loadFragment('/html/config-content.html');
|
||||
const html = await ctx.loadFragment('/components/admin/config-content.html');
|
||||
ctx.setContent(html, 'Admin');
|
||||
|
||||
if (window.configManager) {
|
||||
@ -384,9 +384,9 @@ class SystemStoragePage {
|
||||
}
|
||||
|
||||
// App-icon <img>, falling back to the generic app icon when the slug has no
|
||||
// bundled icon. Icons are served at /icons/apps/<slug>.svg.
|
||||
// bundled icon. Icons are served at /core/icons/apps/<slug>.svg.
|
||||
_iconImg(slug) {
|
||||
return `<img class="sys-task-icon" src="/icons/apps/${encodeURIComponent(slug)}.svg" alt="" onerror="this.onerror=null;this.src='/icons/apps/default.svg'">`;
|
||||
return `<img class="sys-task-icon" src="/core/icons/apps/${encodeURIComponent(slug)}.svg" alt="" onerror="this.onerror=null;this.src='/core/icons/apps/default.svg'">`;
|
||||
}
|
||||
|
||||
_appIconHtml(app) {
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "app-detail",
|
||||
"routes": ["/app", "/app*"],
|
||||
"module": "/features/app-detail/index.js",
|
||||
"module": "/components/app-detail/index.js",
|
||||
"handler": "handleAppDetail",
|
||||
"navId": "nav-app-center",
|
||||
"order": 25
|
||||
@ -42,7 +42,7 @@ LP.features.register({
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ctx.loadFragment('/html/apps-unified-layout.html');
|
||||
const html = await ctx.loadFragment('/components/apps/apps-unified-layout.html');
|
||||
ctx.setContent(html, appName);
|
||||
|
||||
if (!window.appTabbedManager) {
|
||||
@ -522,9 +522,9 @@ class AppsManager {
|
||||
} else if (!icon && id === 'installed') {
|
||||
iconHtml = '<svg class="category-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
} else {
|
||||
let iconPath = icon || `/icons/categories/${id}.svg`;
|
||||
let iconPath = icon || `/core/icons/categories/${id}.svg`;
|
||||
if (!iconPath.startsWith('/')) iconPath = '/' + iconPath;
|
||||
iconHtml = `<img src="${iconPath}" alt="${name}" onerror="this.src='/icons/categories/default.svg'"/>`;
|
||||
iconHtml = `<img src="${iconPath}" alt="${name}" onerror="this.src='/core/icons/categories/default.svg'"/>`;
|
||||
}
|
||||
|
||||
div.innerHTML = `${iconHtml} ${name}`;
|
||||
@ -715,7 +715,7 @@ class AppsManager {
|
||||
if (onHiddenTab) {
|
||||
window.appTabbedManager.switchTab('config');
|
||||
}
|
||||
let icon = app.icon || '/icons/apps/default.svg';
|
||||
let icon = app.icon || '/core/icons/apps/default.svg';
|
||||
|
||||
// Ensure absolute path from root
|
||||
if (!icon.startsWith('/')) {
|
||||
@ -735,7 +735,7 @@ class AppsManager {
|
||||
const headerHTML = `
|
||||
<div class="app-info">
|
||||
<div class="app-card-icon" style="width: 80px; height: 80px; min-width: 80px;">
|
||||
<img src="${icon}" alt="${shortName}" onerror="this.onerror=null; this.src='/icons/apps/default.svg'" style="width: 100%; height: 100%; object-fit: contain;"/>
|
||||
<img src="${icon}" alt="${shortName}" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg'" style="width: 100%; height: 100%; object-fit: contain;"/>
|
||||
</div>
|
||||
<div class="app-details">
|
||||
<h2>${shortName}</h2>
|
||||
@ -936,14 +936,14 @@ class AppsManager {
|
||||
if (requiredService && !this.checkServiceInstalled(requiredService) && !appData.installed) {
|
||||
const slug = requiredService.toLowerCase();
|
||||
const serviceLabel = slug.charAt(0).toUpperCase() + slug.slice(1);
|
||||
const iconUrl = `/icons/apps/${encodeURIComponent(slug)}.svg`;
|
||||
const iconUrl = `/core/icons/apps/${encodeURIComponent(slug)}.svg`;
|
||||
configSection.innerHTML = `
|
||||
<div class="config-title">
|
||||
<h3>🛠️ Configuration Settings</h3>
|
||||
<p>Configure ${this.escHtml(appData.name)} to match your requirements</p>
|
||||
</div>
|
||||
<div class="dep-required-card" data-service="${this.escAttr(slug)}">
|
||||
<img src="${iconUrl}" alt="" class="dep-required-icon" onerror="this.onerror=null; this.src='/icons/apps/default.svg';">
|
||||
<img src="${iconUrl}" alt="" class="dep-required-icon" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg';">
|
||||
<div class="dep-required-body">
|
||||
<div class="dep-required-title">${this.escHtml(serviceLabel)} required</div>
|
||||
<div class="dep-required-reason">${this.escHtml(serviceLabel)} needs to be installed before you can configure ${this.escHtml(appData.name)}.</div>
|
||||
@ -1764,7 +1764,7 @@ class AppsManager {
|
||||
card.dataset.search = searchHaystack;
|
||||
|
||||
const appName = app.command.split(' ').pop();
|
||||
let icon = app.icon || '/icons/apps/default.svg';
|
||||
let icon = app.icon || '/core/icons/apps/default.svg';
|
||||
|
||||
// Ensure absolute path from root
|
||||
if (!icon.startsWith('/')) {
|
||||
@ -1809,7 +1809,7 @@ class AppsManager {
|
||||
card.innerHTML = `
|
||||
<div class="app-card-top" style="cursor: pointer;" onclick="appsManager.showAppDetail('${appName}')">
|
||||
<div class="app-card-icon">
|
||||
<img src="${icon}" alt="${app.name}" onerror="this.src='/icons/apps/default.svg'"/>
|
||||
<img src="${icon}" alt="${app.name}" onerror="this.src='/core/icons/apps/default.svg'"/>
|
||||
</div>
|
||||
<div class="app-card-content">
|
||||
<div class="app-card-title" style="cursor: pointer;">${app.name.split(' - ')[0].trim()}</div>
|
||||
@ -1889,7 +1889,7 @@ class AppsManager {
|
||||
cat.name.toLowerCase() === categoryId.toLowerCase()
|
||||
);
|
||||
|
||||
let iconPath = category ? category.icon : `/icons/categories/${categoryId}.svg`;
|
||||
let iconPath = category ? category.icon : `/core/icons/categories/${categoryId}.svg`;
|
||||
|
||||
// Ensure absolute path from root
|
||||
if (iconPath && !iconPath.startsWith('/')) {
|
||||
@ -2041,7 +2041,7 @@ class AppsManager {
|
||||
const fieldId = fieldKey;
|
||||
const slug = this.serviceForField(fieldKey, fieldConfig);
|
||||
const serviceName = slug ? slug.charAt(0).toUpperCase() + slug.slice(1) : '';
|
||||
const iconUrl = slug ? `/icons/apps/${encodeURIComponent(slug)}.svg` : '/icons/apps/default.svg';
|
||||
const iconUrl = slug ? `/core/icons/apps/${encodeURIComponent(slug)}.svg` : '/core/icons/apps/default.svg';
|
||||
const isCheckbox = fieldConfig.type === 'checkbox';
|
||||
const hiddenInput = isCheckbox
|
||||
? `<input type="hidden" id="${fieldId}" name="${cfgKey}" value="false">`
|
||||
@ -2050,7 +2050,7 @@ class AppsManager {
|
||||
return `
|
||||
<div class="dep-required-card" data-service="${this.escAttr(slug)}">
|
||||
${hiddenInput}
|
||||
<img src="${iconUrl}" alt="" class="dep-required-icon" onerror="this.onerror=null; this.src='/icons/apps/default.svg';">
|
||||
<img src="${iconUrl}" alt="" class="dep-required-icon" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg';">
|
||||
<div class="dep-required-body">
|
||||
<div class="dep-required-title">${this.escHtml(fieldConfig.label)}</div>
|
||||
<div class="dep-required-reason">${this.escHtml(disabledReason)}</div>
|
||||
@ -2135,7 +2135,7 @@ class AppsManager {
|
||||
});
|
||||
}
|
||||
|
||||
const iconUrl = app.icon ? (app.icon.startsWith('/') ? app.icon : '/' + app.icon) : `/icons/apps/${slug}.svg`;
|
||||
const iconUrl = app.icon ? (app.icon.startsWith('/') ? app.icon : '/' + app.icon) : `/core/icons/apps/${slug}.svg`;
|
||||
const shortName = app.name.split(' - ')[0];
|
||||
|
||||
const eoBadges = badges.map(b => ({
|
||||
@ -2352,12 +2352,12 @@ class AppsManager {
|
||||
const cfgKey = `CFG_${a.slug.toUpperCase()}_NETWORK`;
|
||||
const current = (a.config && a.config[cfgKey]) || 'default';
|
||||
const checked = current === 'gluetun' ? 'checked' : '';
|
||||
const icon = a.icon ? (a.icon.startsWith('/') ? a.icon : '/' + a.icon) : '/icons/apps/default.svg';
|
||||
const icon = a.icon ? (a.icon.startsWith('/') ? a.icon : '/' + a.icon) : '/core/icons/apps/default.svg';
|
||||
return `
|
||||
<label class="gluetun-country-item">
|
||||
<input type="checkbox" data-slug="${a.slug}" data-current="${current}" ${checked}>
|
||||
<span class="gluetun-country-name" style="display:flex; align-items:center; gap:10px;">
|
||||
<img src="${icon}" alt="" style="width:20px; height:20px; object-fit:contain;" onerror="this.onerror=null; this.src='/icons/apps/default.svg';">
|
||||
<img src="${icon}" alt="" style="width:20px; height:20px; object-fit:contain;" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg';">
|
||||
${a.name || a.slug}
|
||||
</span>
|
||||
</label>`;
|
||||
@ -2408,7 +2408,7 @@ class AppsManager {
|
||||
const existing = document.getElementById('mullvad-generate-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const mullvadIcon = (window.gluetunProviderIcons && window.gluetunProviderIcons.mullvad) || '/icons/vpn/mullvad.svg';
|
||||
const mullvadIcon = (window.gluetunProviderIcons && window.gluetunProviderIcons.mullvad) || '/core/icons/vpn/mullvad.svg';
|
||||
const bodyHtml = `
|
||||
<p class="eo-modal-section-text">
|
||||
Enter your 16-digit Mullvad account number. A new WireGuard key will be generated locally
|
||||
@ -3144,7 +3144,7 @@ class AppsManager {
|
||||
if (existing) existing.remove();
|
||||
|
||||
const displayName = appData?.name?.split(' - ')[0]?.trim() || appName;
|
||||
let icon = appData?.icon || `/icons/apps/${appName}.svg`;
|
||||
let icon = appData?.icon || `/core/icons/apps/${appName}.svg`;
|
||||
if (icon && !icon.startsWith('/')) icon = '/' + icon;
|
||||
|
||||
const bodyHtml = `
|
||||
@ -3350,11 +3350,11 @@ class AppsManager {
|
||||
|
||||
// Only load scripts if they're not already loaded
|
||||
const scripts = [
|
||||
{ name: 'TaskManager', src: '/shared/task/task-manager.js' },
|
||||
{ name: 'TaskCommands', src: '/shared/task/task-commands.js' },
|
||||
{ name: 'TaskActions', src: '/shared/task/task-actions.js' },
|
||||
{ name: 'TaskRouter', src: '/shared/task/task-router.js' },
|
||||
{ name: 'TasksManager', src: '/features/tasks/tasks-manager.js' }
|
||||
{ name: 'TaskManager', src: '/core/lib/task-manager.js' },
|
||||
{ name: 'TaskCommands', src: '/core/lib/task-commands.js' },
|
||||
{ name: 'TaskActions', src: '/core/lib/task-actions.js' },
|
||||
{ name: 'TaskRouter', src: '/core/lib/task-router.js' },
|
||||
{ name: 'TasksManager', src: '/components/tasks/tasks-manager.js' }
|
||||
];
|
||||
|
||||
for (const script of scripts) {
|
||||
@ -3761,7 +3761,7 @@ class AppsManager {
|
||||
|
||||
showUpdateConfirmModal(appName) {
|
||||
let displayName = appName;
|
||||
let icon = `/icons/apps/${appName}.svg`;
|
||||
let icon = `/core/icons/apps/${appName}.svg`;
|
||||
if (window.apps) {
|
||||
const app = window.apps.find(a =>
|
||||
(a.command || '').endsWith(` ${appName}`) ||
|
||||
@ -3820,7 +3820,7 @@ class AppsManager {
|
||||
// Confirmation modal for the destructive "Uninstall App" action.
|
||||
showUninstallConfirmModal(appName) {
|
||||
let displayName = appName;
|
||||
let icon = `/icons/apps/${appName}.svg`;
|
||||
let icon = `/core/icons/apps/${appName}.svg`;
|
||||
let app = null;
|
||||
if (window.apps) {
|
||||
app = window.apps.find(a =>
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "apps",
|
||||
"routes": ["/apps", "/apps*"],
|
||||
"module": "/features/apps/index.js",
|
||||
"module": "/components/apps/index.js",
|
||||
"handler": "handleApps",
|
||||
"navId": "nav-app-center",
|
||||
"nav": { "label": "App Center", "order": 20 },
|
||||
@ -28,7 +28,7 @@ LP.features.register({
|
||||
// Load the unified layout only if it isn't already present — preserves grid
|
||||
// state when moving between categories / back from app-detail (legacy behaviour).
|
||||
if (!document.querySelector('.apps-layout')) {
|
||||
const html = await ctx.loadFragment('/html/apps-unified-layout.html');
|
||||
const html = await ctx.loadFragment('/components/apps/apps-unified-layout.html');
|
||||
ctx.setContent(html, 'Applications');
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ class RoutingManager {
|
||||
const renderRow = (p) => {
|
||||
const app = byApp.get(p.appSlug);
|
||||
const url = this._previewUrl(p, app);
|
||||
const iconUrl = `/icons/apps/${encodeURIComponent(p.appSlug)}.svg`;
|
||||
const iconUrl = `/core/icons/apps/${encodeURIComponent(p.appSlug)}.svg`;
|
||||
const badges = [
|
||||
p.webui ? '<span class="routing-badge routing-badge-webui" title="Marked as the webui (click-to-open) port">webui</span>' : '',
|
||||
p.access === 'public' ? '<span class="routing-badge routing-badge-public" title="Host port is exposed">public</span>' : ''
|
||||
@ -399,7 +399,7 @@ class ServicesManager {
|
||||
: (svc.openUrl ? [{ url: svc.openUrl, label: svc.openLabel || 'Open' }] : []);
|
||||
const openBtn = linksToRender.map(l => renderOpenBtn(l.url, l.label)).join('');
|
||||
|
||||
const iconUrl = this.currentApp ? `/icons/apps/${encodeURIComponent(this.currentApp)}.svg` : '';
|
||||
const iconUrl = this.currentApp ? `/core/icons/apps/${encodeURIComponent(this.currentApp)}.svg` : '';
|
||||
const iconHtml = iconUrl
|
||||
? `<img src="${iconUrl}" alt="${escapeHtml(this.currentApp || '')}" class="task-app-icon service-app-icon" onerror="this.style.display='none'">`
|
||||
: '';
|
||||
@ -101,7 +101,7 @@ class ToolsManager {
|
||||
const deleteTool = tools.find(t => t.id === 'delete_user');
|
||||
const adminTool = tools.find(t => t.id === 'set_admin');
|
||||
const appLabel = (window.getAppDisplayName ? window.getAppDisplayName(appName) : appName);
|
||||
const iconUrl = `/icons/apps/${encodeURIComponent(appName)}.svg`;
|
||||
const iconUrl = `/core/icons/apps/${encodeURIComponent(appName)}.svg`;
|
||||
|
||||
// Reopen this exact modal (used as returnTo when a row action's
|
||||
// sub-modal is cancelled — keeps the user in flow instead of
|
||||
@ -440,7 +440,7 @@ class ToolsManager {
|
||||
const prefill = (opts && opts.prefill) || {};
|
||||
const returnTo = opts && opts.returnTo;
|
||||
let submitted = false;
|
||||
const appIconUrl = this.currentApp ? `/icons/apps/${encodeURIComponent(this.currentApp)}.svg` : '';
|
||||
const appIconUrl = this.currentApp ? `/core/icons/apps/${encodeURIComponent(this.currentApp)}.svg` : '';
|
||||
const bodyHtml = `
|
||||
${tool.confirm ? `<div class="tool-modal-confirm">${escapeHtml(tool.confirm)}</div>` : ''}
|
||||
<form class="tool-form">
|
||||
@ -592,7 +592,7 @@ class ToolsManager {
|
||||
.sort((x, y) => (x.app.name || x.slug).localeCompare(y.app.name || y.slug))
|
||||
.map(({ app, slug }) => {
|
||||
const checked = selected.has(slug) ? 'checked' : '';
|
||||
const iconUrl = `/icons/apps/${encodeURIComponent(slug)}.svg`;
|
||||
const iconUrl = `/core/icons/apps/${encodeURIComponent(slug)}.svg`;
|
||||
const displayName = escapeHtml(app.name || slug);
|
||||
return `
|
||||
<label class="installed-apps-item">
|
||||
@ -763,7 +763,7 @@ class ToolsManager {
|
||||
|
||||
list.innerHTML = flat.map(u => {
|
||||
const checked = selected.has(u.id) ? 'checked' : '';
|
||||
const iconUrl = `/icons/apps/${encodeURIComponent(u.slug)}.svg`;
|
||||
const iconUrl = `/core/icons/apps/${encodeURIComponent(u.slug)}.svg`;
|
||||
const appLabel = displayName(u.slug);
|
||||
const showButton = u.label && u.label !== appLabel;
|
||||
const fullLabel = showButton
|
||||
@ -646,7 +646,7 @@ class BackupPage {
|
||||
const when = has ? 'Last backed up ' + this.formatRelative(sys.latest_time) : 'No backup yet';
|
||||
return `
|
||||
<div class="backup-app-tile backup-app-tile--system" data-system="1" title="Back up system config">
|
||||
<img class="backup-app-tile-icon" src="/icons/apps/libreportal.svg" alt="" onerror="this.style.display='none'">
|
||||
<img class="backup-app-tile-icon" src="/core/icons/apps/libreportal.svg" alt="" onerror="this.style.display='none'">
|
||||
<div class="backup-app-tile-text">
|
||||
<div class="backup-app-tile-name">Configs</div>
|
||||
<div class="backup-app-tile-meta">
|
||||
@ -680,7 +680,7 @@ class BackupPage {
|
||||
const command = a.command || '';
|
||||
return command.endsWith(` ${slug}`) || a.name?.toLowerCase() === slug.toLowerCase();
|
||||
});
|
||||
let icon = match?.icon || '/icons/apps/default.svg';
|
||||
let icon = match?.icon || '/core/icons/apps/default.svg';
|
||||
if (!icon.startsWith('/')) icon = '/' + icon;
|
||||
const displayName = (typeof window.getAppDisplayName === 'function')
|
||||
? window.getAppDisplayName(slug)
|
||||
@ -695,7 +695,7 @@ class BackupPage {
|
||||
const { icon, displayName } = this.appMeta(app.app);
|
||||
return `
|
||||
<div class="backup-app-tile" data-app="${this.escape(app.app)}" title="Open ${this.escape(displayName)} backup history">
|
||||
<img class="backup-app-tile-icon" src="${this.escape(icon)}" alt="" onerror="this.src='/icons/apps/default.svg'">
|
||||
<img class="backup-app-tile-icon" src="${this.escape(icon)}" alt="" onerror="this.src='/core/icons/apps/default.svg'">
|
||||
<div class="backup-app-tile-text">
|
||||
<div class="backup-app-tile-name">${this.escape(displayName)}</div>
|
||||
<div class="backup-app-tile-meta">
|
||||
@ -1056,7 +1056,7 @@ class BackupPage {
|
||||
const deepLink = hasApp
|
||||
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
|
||||
: null;
|
||||
const iconUrl = hasApp ? `/icons/apps/${encodeURIComponent(r.app)}.svg` : '/icons/apps/libreportal.svg';
|
||||
const iconUrl = hasApp ? `/core/icons/apps/${encodeURIComponent(r.app)}.svg` : '/core/icons/apps/libreportal.svg';
|
||||
const displayName = hasApp ? this.appMeta(r.app).displayName : 'Configs';
|
||||
const appChip = hasApp
|
||||
? `<a class="backup-snapshot-link backup-snapshot-app-chip" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}" title="Open ${this.escape(displayName)} backups">${this.escape(displayName)}</a>`
|
||||
@ -1980,7 +1980,7 @@ class BackupPage {
|
||||
: 'No backup yet';
|
||||
|
||||
const rows = [
|
||||
row('__system__', '/icons/apps/libreportal.svg', 'Configs', sysSub, preTickSystem),
|
||||
row('__system__', '/core/icons/apps/libreportal.svg', 'Configs', sysSub, preTickSystem),
|
||||
...sortedApps.map(app => {
|
||||
const meta = this.appMeta(app.app);
|
||||
const sub = app.latest_snapshot
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "backup",
|
||||
"routes": ["/backup", "/backup*"],
|
||||
"module": "/features/backup/index.js",
|
||||
"module": "/components/backup/index.js",
|
||||
"handler": "handleBackup",
|
||||
"navId": "nav-backup",
|
||||
"nav": { "label": "Backups", "order": 50 },
|
||||
@ -14,14 +14,14 @@ LP.features.register({
|
||||
routes: ['/backup', '/backup*'],
|
||||
// Controllers the feature needs; lazy-loaded on first mount (idempotent).
|
||||
scripts: [
|
||||
'/features/backup/backup-schema.js',
|
||||
'/features/backup/backup-page.js',
|
||||
'/shared/js/backup-app-card.js',
|
||||
'/components/backup/backup-schema.js',
|
||||
'/components/backup/backup-page.js',
|
||||
'/core/lib/backup-app-card.js',
|
||||
],
|
||||
|
||||
async mount(ctx) {
|
||||
await ctx.loadScripts(this.scripts);
|
||||
const html = await ctx.loadFragment('/html/backup-content.html');
|
||||
const html = await ctx.loadFragment('/components/backup/backup-content.html');
|
||||
ctx.setContent(html, 'Backups');
|
||||
if (typeof BackupPage === 'undefined') {
|
||||
throw new Error('BackupPage controller failed to load');
|
||||
@ -34,14 +34,14 @@ function renderInstalledApps() {
|
||||
|
||||
function createInstalledAppCard(app) {
|
||||
const appName = app.command.split(' ').pop();
|
||||
let icon = app.icon || '/icons/apps/default.svg';
|
||||
let icon = app.icon || '/core/icons/apps/default.svg';
|
||||
if (!icon.startsWith('/')) icon = '/' + icon;
|
||||
const shortName = app.name.split(' - ')[0].trim();
|
||||
|
||||
return `
|
||||
<div class="frontpage-app-tile" onclick="window.location.href='/app/${appName}'">
|
||||
<div class="frontpage-app-icon-wrap">
|
||||
<img src="${icon}" alt="${shortName}" onerror="this.src='/icons/apps/default.svg'">
|
||||
<img src="${icon}" alt="${shortName}" onerror="this.src='/core/icons/apps/default.svg'">
|
||||
<div class="frontpage-app-overlay" id="frontpage-overlay-${appName}" onclick="event.stopPropagation()"></div>
|
||||
</div>
|
||||
<span class="frontpage-app-name">${shortName}</span>
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "dashboard",
|
||||
"routes": ["/", "/dashboard"],
|
||||
"module": "/features/dashboard/index.js",
|
||||
"module": "/components/dashboard/index.js",
|
||||
"handler": "handleDashboard",
|
||||
"navId": "nav-dashboard",
|
||||
"nav": { "label": "Dashboard", "order": 10 },
|
||||
@ -13,7 +13,7 @@ LP.features.register({
|
||||
routes: ['/', '/dashboard'],
|
||||
|
||||
async mount(ctx) {
|
||||
const html = await ctx.loadFragment('/html/dashboard-content.html');
|
||||
const html = await ctx.loadFragment('/components/dashboard/dashboard-content.html');
|
||||
ctx.setContent(html, 'Dashboard');
|
||||
|
||||
// Render the installed-apps icon grid (handleDashboard's only post-render call).
|
||||
@ -5,7 +5,7 @@
|
||||
{
|
||||
"id": "dashboard",
|
||||
"routes": ["/", "/dashboard"],
|
||||
"module": "/features/dashboard/index.js",
|
||||
"module": "/components/dashboard/index.js",
|
||||
"handler": "handleDashboard",
|
||||
"navId": "nav-dashboard",
|
||||
"nav": { "label": "Dashboard", "order": 10 }
|
||||
@ -13,7 +13,7 @@
|
||||
{
|
||||
"id": "apps",
|
||||
"routes": ["/apps", "/apps*"],
|
||||
"module": "/features/apps/index.js",
|
||||
"module": "/components/apps/index.js",
|
||||
"handler": "handleApps",
|
||||
"navId": "nav-app-center",
|
||||
"nav": { "label": "App Center", "order": 20 }
|
||||
@ -21,14 +21,14 @@
|
||||
{
|
||||
"id": "app-detail",
|
||||
"routes": ["/app", "/app*"],
|
||||
"module": "/features/app-detail/index.js",
|
||||
"module": "/components/app-detail/index.js",
|
||||
"handler": "handleAppDetail",
|
||||
"navId": "nav-app-center"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"routes": ["/admin", "/admin*"],
|
||||
"module": "/features/admin/index.js",
|
||||
"module": "/components/admin/index.js",
|
||||
"handler": "handleAdmin",
|
||||
"navId": "nav-config",
|
||||
"nav": { "label": "Admin", "order": 40 }
|
||||
@ -42,7 +42,7 @@
|
||||
{
|
||||
"id": "tasks",
|
||||
"routes": ["/tasks", "/tasks*"],
|
||||
"module": "/features/tasks/index.js",
|
||||
"module": "/components/tasks/index.js",
|
||||
"handler": "handleTasks",
|
||||
"navId": "nav-tasks",
|
||||
"nav": { "label": "Tasks", "order": 60 }
|
||||
@ -50,14 +50,14 @@
|
||||
{
|
||||
"id": "updater",
|
||||
"routes": ["/updater", "/updater*"],
|
||||
"module": "/features/updater/index.js",
|
||||
"module": "/components/updater/index.js",
|
||||
"navId": "nav-updater",
|
||||
"nav": { "label": "Updates", "order": 30 }
|
||||
},
|
||||
{
|
||||
"id": "backup",
|
||||
"routes": ["/backup", "/backup*"],
|
||||
"module": "/features/backup/index.js",
|
||||
"module": "/components/backup/index.js",
|
||||
"handler": "handleBackup",
|
||||
"navId": "nav-backup",
|
||||
"nav": { "label": "Backups", "order": 50 }
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "tasks",
|
||||
"routes": ["/tasks", "/tasks*"],
|
||||
"module": "/features/tasks/index.js",
|
||||
"module": "/components/tasks/index.js",
|
||||
"handler": "handleTasks",
|
||||
"navId": "nav-tasks",
|
||||
"nav": { "label": "Tasks", "order": 60 },
|
||||
@ -11,7 +11,7 @@ LP.features.register({
|
||||
routes: ['/tasks', '/tasks*'],
|
||||
|
||||
async mount(ctx) {
|
||||
const html = await ctx.loadFragment('/html/tasks-content.html');
|
||||
const html = await ctx.loadFragment('/components/tasks/tasks-content.html');
|
||||
ctx.setContent(html, 'Tasks');
|
||||
|
||||
if (window.tasksManager) {
|
||||
@ -119,8 +119,8 @@ class TasksManager {
|
||||
? 'LibrePortal'
|
||||
: ((appName && window.getAppDisplayName) ? window.getAppDisplayName(appName) : (appName || '')));
|
||||
const icon = isSystemTask
|
||||
? '/icons/libreportal.svg'
|
||||
: (appName ? `/icons/apps/${encodeURIComponent(appName)}.svg` : null);
|
||||
? '/core/icons/libreportal.svg'
|
||||
: (appName ? `/core/icons/apps/${encodeURIComponent(appName)}.svg` : null);
|
||||
const typeIcon = (this.getTaskTypeIcon ? this.getTaskTypeIcon(task)?.icon : '') || '';
|
||||
return { appName, isSystemTask, actionTitle, displayName, icon, typeIcon };
|
||||
}
|
||||
@ -1112,7 +1112,7 @@ class TasksManager {
|
||||
renderTaskIcons(task) {
|
||||
const typeIcon = `<span class="task-type-icon">${this.getTaskTypeIcon(task).icon}</span>`;
|
||||
// `app: 'system'` is a category sentinel (config_update, system_update, …),
|
||||
// not a real app slug, so it has no /icons/apps/system.svg — fall through
|
||||
// not a real app slug, so it has no /core/icons/apps/system.svg — fall through
|
||||
// to the LibrePortal-system branch so those tasks still get a logo.
|
||||
const isSystemSentinel = task.app === 'system';
|
||||
if (task.app && !isSystemSentinel) {
|
||||
@ -1120,7 +1120,7 @@ class TasksManager {
|
||||
return `${typeIcon}<img src="${appIconPath}" alt="${task.app}" class="task-app-icon" onerror="this.style.display='none'">`;
|
||||
}
|
||||
if (isSystemSentinel || this.isLibrePortalSystemTask(task)) {
|
||||
return `${typeIcon}<img src="/icons/libreportal.svg" alt="LibrePortal" class="task-app-icon">`;
|
||||
return `${typeIcon}<img src="/core/icons/libreportal.svg" alt="LibrePortal" class="task-app-icon">`;
|
||||
}
|
||||
return typeIcon;
|
||||
}
|
||||
@ -1137,7 +1137,7 @@ class TasksManager {
|
||||
}
|
||||
|
||||
// Default icon path
|
||||
return `/icons/apps/${task.app}.svg`;
|
||||
return `/core/icons/apps/${task.app}.svg`;
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "updater",
|
||||
"routes": ["/updater", "/updater*"],
|
||||
"module": "/features/updater/index.js",
|
||||
"module": "/components/updater/index.js",
|
||||
"navId": "nav-updater",
|
||||
"nav": { "label": "Updates", "order": 30 },
|
||||
"order": 30,
|
||||
@ -9,11 +9,11 @@
|
||||
LP.features.register({
|
||||
id: 'updater',
|
||||
routes: ['/updater', '/updater*'],
|
||||
scripts: ['/features/updater/updater-page.js'],
|
||||
scripts: ['/components/updater/updater-page.js'],
|
||||
|
||||
async mount(ctx) {
|
||||
await ctx.loadScripts(this.scripts);
|
||||
const html = await ctx.loadFragment('/html/updater-content.html');
|
||||
const html = await ctx.loadFragment('/components/updater/updater-content.html');
|
||||
ctx.setContent(html, 'Updates');
|
||||
if (typeof UpdaterPage === 'undefined') {
|
||||
throw new Error('UpdaterPage controller failed to load');
|
||||
@ -64,7 +64,7 @@ class AuthManager {
|
||||
<div class="login-content">
|
||||
<div class="aurora-header">
|
||||
<div class="aurora-logo">
|
||||
<img src="/icons/libreportal.svg" alt="LibrePortal" onload="this.classList.add('loaded')" onerror="this.style.display='none'">
|
||||
<img src="/core/icons/libreportal.svg" alt="LibrePortal" onload="this.classList.add('loaded')" onerror="this.style.display='none'">
|
||||
<h1>LibrePortal</h1>
|
||||
</div>
|
||||
<p class="aurora-subtitle">Step softly back into your own private universe</p>
|
||||
@ -27,7 +27,7 @@ class LoadingUI {
|
||||
<div class="loading-container">
|
||||
<div class="aurora-header">
|
||||
<div class="aurora-logo">
|
||||
<img src="/icons/libreportal.svg" alt="LibrePortal" onload="this.classList.add('loaded')" onerror="this.style.display='none'">
|
||||
<img src="/core/icons/libreportal.svg" alt="LibrePortal" onload="this.classList.add('loaded')" onerror="this.style.display='none'">
|
||||
<h1>LibrePortal</h1>
|
||||
</div>
|
||||
<p class="aurora-subtitle">Drifting softly into your private universe...</p>
|
||||
@ -68,7 +68,7 @@ class SetupWizard {
|
||||
<div class="setup-content">
|
||||
<div class="aurora-header">
|
||||
<div class="aurora-logo">
|
||||
<img src="/icons/libreportal.svg" alt="LibrePortal" onload="this.classList.add('loaded')" onerror="this.style.display='none'">
|
||||
<img src="/core/icons/libreportal.svg" alt="LibrePortal" onload="this.classList.add('loaded')" onerror="this.style.display='none'">
|
||||
<h1>LibrePortal</h1>
|
||||
</div>
|
||||
<p class="aurora-subtitle">Tuning your private universe before takeoff...</p>
|
||||
@ -425,7 +425,7 @@ class SetupWizard {
|
||||
const app = manifest.get(slug) || {};
|
||||
const name = app.title || app.name || fallback.name;
|
||||
const desc = app.description || fallback.description;
|
||||
const icon = app.icon || `/icons/apps/${slug}.svg`;
|
||||
const icon = app.icon || `/core/icons/apps/${slug}.svg`;
|
||||
// Sub-option lives OUTSIDE the parent label (nested <label> behaviour
|
||||
// is messy — clicks would toggle both checkboxes). Both labels are
|
||||
// siblings inside a wrapper div; CSS connects them visually.
|
||||
@ -440,7 +440,7 @@ class SetupWizard {
|
||||
<input type="checkbox" data-app="${slug}" ${defaultChecked ? 'checked' : ''}>
|
||||
<div class="setup-app-icon-wrap">
|
||||
<img src="${icon}" alt="${name}" class="setup-app-icon"
|
||||
onerror="this.onerror=null; this.src='/icons/apps/default.svg'">
|
||||
onerror="this.onerror=null; this.src='/core/icons/apps/default.svg'">
|
||||
</div>
|
||||
<div class="setup-app-info">
|
||||
<div class="setup-app-name">${name}</div>
|
||||
@ -39,18 +39,18 @@ class SystemLoader {
|
||||
global: 'configManager',
|
||||
dependencies: ['data'],
|
||||
scripts: [
|
||||
'/shared/js/config-options.js',
|
||||
'/shared/js/config-shared.js',
|
||||
'/features/admin/config-validator.js',
|
||||
'/features/admin/toggle-manager.js',
|
||||
'/features/admin/config-core.js',
|
||||
'/features/admin/domain-manager.js',
|
||||
'/features/admin/ip-whitelist-manager.js',
|
||||
'/features/admin/config-renderer.js',
|
||||
'/features/admin/config-sidebar.js',
|
||||
'/features/admin/config-form.js',
|
||||
'/features/admin/config-utils.js',
|
||||
'/features/admin/config-manager.js'
|
||||
'/core/lib/config-options.js',
|
||||
'/core/lib/config-shared.js',
|
||||
'/components/admin/config-validator.js',
|
||||
'/components/admin/toggle-manager.js',
|
||||
'/components/admin/config-core.js',
|
||||
'/components/admin/domain-manager.js',
|
||||
'/components/admin/ip-whitelist-manager.js',
|
||||
'/components/admin/config-renderer.js',
|
||||
'/components/admin/config-sidebar.js',
|
||||
'/components/admin/config-form.js',
|
||||
'/components/admin/config-utils.js',
|
||||
'/components/admin/config-manager.js'
|
||||
]
|
||||
});
|
||||
|
||||
@ -69,9 +69,9 @@ class SystemLoader {
|
||||
global: null,
|
||||
dependencies: [],
|
||||
scripts: [
|
||||
'/js/components/topbar.js',
|
||||
'/js/components/update-notifier.js',
|
||||
'/js/components/mobile-menu.js'
|
||||
'/core/ui/topbar.js',
|
||||
'/core/ui/update-notifier.js',
|
||||
'/core/ui/mobile-menu.js'
|
||||
]
|
||||
});
|
||||
|
||||
@ -89,7 +89,7 @@ class SystemLoader {
|
||||
},
|
||||
global: null,
|
||||
dependencies: [],
|
||||
script: '/js/components/confirmation-dialog.js'
|
||||
script: '/core/ui/confirmation-dialog.js'
|
||||
});
|
||||
|
||||
// Notifications
|
||||
@ -106,7 +106,7 @@ class SystemLoader {
|
||||
},
|
||||
global: 'notificationSystem',
|
||||
dependencies: [],
|
||||
script: '/js/components/notifications.js'
|
||||
script: '/core/ui/notifications.js'
|
||||
});
|
||||
|
||||
// Dashboard
|
||||
@ -167,14 +167,14 @@ class SystemLoader {
|
||||
global: 'tasksManager',
|
||||
dependencies: [],
|
||||
scripts: [
|
||||
'/shared/task/task-event-bus.js',
|
||||
'/shared/task/task-commands.js',
|
||||
'/shared/task/task-actions.js',
|
||||
'/shared/task/task-router.js',
|
||||
'/shared/task/task-global-functions.js',
|
||||
'/shared/task/task-manager.js',
|
||||
'/js/task-parameter-preserve.js',
|
||||
'/features/tasks/tasks-manager.js'
|
||||
'/core/lib/task-event-bus.js',
|
||||
'/core/lib/task-commands.js',
|
||||
'/core/lib/task-actions.js',
|
||||
'/core/lib/task-router.js',
|
||||
'/core/lib/task-global-functions.js',
|
||||
'/core/lib/task-manager.js',
|
||||
'/core/lib/task-parameter-preserve.js',
|
||||
'/components/tasks/tasks-manager.js'
|
||||
]
|
||||
});
|
||||
|
||||
@ -204,13 +204,13 @@ class SystemLoader {
|
||||
global: 'appsManager',
|
||||
dependencies: ['data'],
|
||||
scripts: [
|
||||
'/features/apps/port-manager.js',
|
||||
'/shared/task/task-manager.js', // Add TaskManager for backup functionality
|
||||
'/shared/js/backup-app-card.js',
|
||||
'/features/apps/services-manager.js',
|
||||
'/features/apps/tools-manager.js',
|
||||
'/features/apps/routing-manager.js',
|
||||
'/features/apps/apps-manager.js'
|
||||
'/components/apps/port-manager.js',
|
||||
'/core/lib/task-manager.js', // Add TaskManager for backup functionality
|
||||
'/core/lib/backup-app-card.js',
|
||||
'/components/apps/services-manager.js',
|
||||
'/components/apps/tools-manager.js',
|
||||
'/components/apps/routing-manager.js',
|
||||
'/components/apps/apps-manager.js'
|
||||
]
|
||||
});
|
||||
|
||||
@ -232,7 +232,7 @@ class SystemLoader {
|
||||
},
|
||||
global: 'appTabbedManager',
|
||||
dependencies: [],
|
||||
script: '/features/apps/app-tabbed-manager.js'
|
||||
script: '/components/apps/app-tabbed-manager.js'
|
||||
});
|
||||
|
||||
// console.log('TEST: Components added. Total components:', this.components.size);
|
||||
@ -639,9 +639,9 @@ class SystemLoader {
|
||||
iconUrls.add(app.icon.startsWith('/') ? app.icon : '/' + app.icon);
|
||||
} else {
|
||||
const cleanAppName = app.command?.split(' ').pop() || app.name;
|
||||
iconUrls.add(`/icons/apps/${cleanAppName}.svg`);
|
||||
iconUrls.add(`/core/icons/apps/${cleanAppName}.svg`);
|
||||
}
|
||||
iconUrls.add('/icons/apps/default.svg');
|
||||
iconUrls.add('/core/icons/apps/default.svg');
|
||||
});
|
||||
}
|
||||
|
||||
@ -655,8 +655,8 @@ class SystemLoader {
|
||||
}
|
||||
|
||||
// Add 'all' and 'installed' category icons
|
||||
iconUrls.add('/icons/categories/all.svg');
|
||||
iconUrls.add('/icons/categories/installed.svg');
|
||||
iconUrls.add('/core/icons/categories/all.svg');
|
||||
iconUrls.add('/core/icons/categories/installed.svg');
|
||||
|
||||
// console.log(`📦 Found ${iconUrls.size} unique icons to preload`);
|
||||
|
||||
|
Before Width: | Height: | Size: 598 B After Width: | Height: | Size: 598 B |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |