about/static/js/admin.js

1300 lines
44 KiB
JavaScript

// Global variables
let currentPluginData = {};
let fileUploadQueue = new Map();
function initSettingsEditor(pluginName, settings) {
console.log(`Initializing settings editor for ${pluginName}:`, settings);
currentPluginData[pluginName] = settings;
const container = document.getElementById(`settings-${pluginName}`);
if (!container) {
console.error(`Container not found for plugin: ${pluginName}`);
return;
}
container.innerHTML = '';
if (!settings || Object.keys(settings).length === 0) {
settings = createDefaultStructure(pluginName, getPluginType(pluginName));
}
renderPluginForm(container, pluginName, settings);
}
function renderPluginForm(container, pluginName, settings) {
const pluginType = getPluginType(pluginName);
const formWrapper = document.createElement('div');
formWrapper.className = 'plugin-settings-wrapper';
if (pluginType.type === 'array-based') {
renderArrayBasedPlugin(formWrapper, pluginName, settings, pluginType);
} else {
renderObjectBasedPlugin(formWrapper, pluginName, settings);
}
container.appendChild(formWrapper);
}
function createDefaultStructure(pluginName, pluginType) {
const baseStructure = {
ui: {
sectionTitle: getDefaultSectionTitle(pluginName)
}
};
if (pluginType.type === 'array-based') {
baseStructure[pluginType.arrayField] = [];
}
const specificDefaults = {
'steam': { steamid: '' },
'lastfm': { username: '' },
'beatleader': { username: '' },
'webring': { webring_url: '', username: '' },
'code': { github: { username: '' }, wakatime: { api_key: '' } },
'info': { sourceCodeURL: '' },
'profile': { name: '', title: '', subtitle: '', bio: '', profileImage: '' }
};
if (specificDefaults[pluginName]) {
Object.assign(baseStructure, specificDefaults[pluginName]);
}
return baseStructure;
}
function getPluginType(pluginName) {
const arrayBasedPlugins = {
'social': {
arrayField: 'links',
itemSchema: {
name: 'string',
url: 'string',
icon: 'string',
iconPath: 'string'
}
},
'techstack': {
arrayField: 'technologies',
itemSchema: {
name: 'string',
icon: 'string',
iconPath: 'string'
}
},
'projects': {
arrayField: 'projects',
itemSchema: {
name: 'string',
description: 'text',
github: 'string',
live: 'string',
image: 'image',
technologies: 'array'
}
},
'neofetch': {
arrayField: 'machines',
itemSchema: {
name: 'string',
output: 'text'
}
},
'services': {
arrayField: 'services',
itemSchema: {
name: 'string',
url: 'string',
description: 'text',
icon: 'image'
}
},
'personal': {
arrayField: 'info',
itemSchema: {
title: 'string',
content: 'text',
image: 'image',
icon: 'string',
category: 'string'
}
},
'meme': {
arrayField: 'memes',
itemSchema: {
text: 'string',
image: 'image',
type: 'select',
source: 'string',
category: 'string'
}
}
};
if (arrayBasedPlugins[pluginName]) {
return { type: 'array-based', ...arrayBasedPlugins[pluginName] };
}
return { type: 'object-based' };
}
function renderArrayBasedPlugin(container, pluginName, settings, pluginType) {
// Render non-array fields first
Object.keys(settings).forEach(key => {
if (key !== pluginType.arrayField) {
createField(key, settings[key], container, '', pluginName);
}
});
const arrayData = settings[pluginType.arrayField] || [];
renderManagedArray(container, pluginName, pluginType.arrayField, arrayData, pluginType.itemSchema);
}
function renderObjectBasedPlugin(container, pluginName, settings) {
Object.keys(settings).forEach(key => {
createField(key, settings[key], container, '', pluginName);
});
const addMoreBtn = document.createElement('button');
addMoreBtn.type = 'button';
addMoreBtn.className = 'btn btn-secondary add-setting-btn';
addMoreBtn.textContent = 'Add Setting';
addMoreBtn.onclick = () => addObjectSetting(container, pluginName);
container.appendChild(addMoreBtn);
}
function createField(key, value, parent = document.body, path = '', pluginName = '') {
const field = document.createElement('div');
field.className = 'settings-field';
const currentPath = path ? `${path}.${key}` : key;
let fieldType = Array.isArray(value) ? 'array' : typeof value;
if (value === null || value === undefined) {
fieldType = 'string';
value = '';
}
const header = document.createElement('div');
header.className = 'field-header';
const label = document.createElement('div');
label.className = 'field-label';
label.textContent = formatFieldName(key);
header.appendChild(label);
const typeLabel = document.createElement('div');
typeLabel.className = 'field-type';
typeLabel.textContent = fieldType;
header.appendChild(typeLabel);
field.appendChild(header);
if (typeof value === 'boolean') {
const checkboxContainer = document.createElement('div');
checkboxContainer.className = 'field-checkbox';
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = value;
input.name = currentPath;
input.id = `${currentPath.replace(/\./g, '_')}`;
const label = document.createElement('label');
label.htmlFor = input.id;
label.textContent = formatFieldName(key);
checkboxContainer.appendChild(input);
checkboxContainer.appendChild(label);
field.appendChild(checkboxContainer);
} else if (typeof value === 'number') {
const input = document.createElement('input');
input.className = 'field-input';
input.type = 'number';
input.value = value;
input.name = currentPath;
input.placeholder = getPlaceholder(key);
field.appendChild(input);
} else if (typeof value === 'string' || (value === null || value === undefined)) {
const stringValue = value || '';
if (isImageField(key, stringValue)) {
createImageField(field, currentPath, stringValue, key, pluginName);
} else if (key.toLowerCase().includes('description') ||
key.toLowerCase().includes('bio') ||
key.toLowerCase().includes('content') ||
key.toLowerCase().includes('message') ||
stringValue.length > 100) {
const textarea = document.createElement('textarea');
textarea.className = 'field-input field-textarea';
textarea.value = stringValue;
textarea.name = currentPath;
textarea.placeholder = getPlaceholder(key);
textarea.rows = Math.min(Math.max(3, Math.ceil(stringValue.length / 50)), 8);
field.appendChild(textarea);
} else {
const input = document.createElement('input');
input.className = 'field-input';
input.type = 'text';
input.value = stringValue;
input.name = currentPath;
input.placeholder = getPlaceholder(key);
field.appendChild(input);
}
} else if (Array.isArray(value)) {
if (key === 'ascii' || key === 'colors') {
const textarea = document.createElement('textarea');
textarea.className = 'field-input field-textarea';
textarea.value = value.join('\n');
textarea.name = currentPath;
textarea.rows = Math.min(value.length + 2, 12);
textarea.placeholder = key === 'ascii' ? 'Enter ASCII art lines' : 'Enter colors (one per line)';
field.appendChild(textarea);
} else {
renderGenericArray(field, value, currentPath, pluginName);
}
} else if (typeof value === 'object' && value !== null) {
const objectContainer = document.createElement('div');
objectContainer.className = 'object-container';
objectContainer.style.marginLeft = '20px';
objectContainer.style.borderLeft = '2px solid #ddd';
objectContainer.style.paddingLeft = '15px';
objectContainer.style.marginTop = '10px';
const toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'object-toggle-btn';
toggleBtn.textContent = '▼ ';
toggleBtn.style.background = 'none';
toggleBtn.style.border = 'none';
toggleBtn.style.cursor = 'pointer';
toggleBtn.style.fontSize = '12px';
const objectLabel = document.createElement('span');
objectLabel.textContent = `${formatFieldName(key)} (${Object.keys(value).length} properties)`;
objectLabel.style.fontWeight = 'bold';
objectLabel.style.color = '#666';
const objectHeader = document.createElement('div');
objectHeader.className = 'object-header';
objectHeader.style.marginBottom = '10px';
objectHeader.appendChild(toggleBtn);
objectHeader.appendChild(objectLabel);
const objectContent = document.createElement('div');
objectContent.className = 'object-content';
Object.keys(value).forEach(nestedKey => {
createField(nestedKey, value[nestedKey], objectContent, currentPath, pluginName);
});
toggleBtn.addEventListener('click', () => {
const isCollapsed = objectContent.style.display === 'none';
objectContent.style.display = isCollapsed ? 'block' : 'none';
toggleBtn.textContent = isCollapsed ? '▼ ' : '▶ ';
});
const addNestedBtn = document.createElement('button');
addNestedBtn.type = 'button';
addNestedBtn.className = 'btn btn-sm btn-secondary';
addNestedBtn.textContent = `Add to ${formatFieldName(key)}`;
addNestedBtn.style.marginTop = '10px';
addNestedBtn.onclick = () => addNestedField(objectContent, currentPath);
objectContainer.appendChild(objectHeader);
objectContainer.appendChild(objectContent);
objectContainer.appendChild(addNestedBtn);
field.appendChild(objectContainer);
}
parent.appendChild(field);
}
function createImageField(field, currentPath, currentValue, key, pluginName) {
const imageContainer = document.createElement('div');
imageContainer.className = 'image-field-container';
const isIconField = key.toLowerCase().includes('icon');
const applyIconClass = (imgEl, val) => {
const guessIcon = isIconField || isBareIconName(val) || (typeof val === 'string' && val.includes('/static/icons/'));
imgEl.className = 'image-preview' + (guessIcon ? ' image-preview--icon' : '');
};
// Current image preview
if (currentValue) {
const preview = document.createElement('div');
preview.className = 'current-image-preview';
const img = document.createElement('img');
img.src = resolveIconURL(currentValue);
img.alt = `Current ${key}`;
applyIconClass(img, currentValue); // <-- apply small icon styling when appropriate
img.onerror = () => {
img.style.display = 'none';
preview.innerHTML += `<div style="color: var(--bad);">Image failed to load: ${resolveIconURL(currentValue)}</div>`;
};
const meta = document.createElement('div');
meta.style.fontSize = '12px';
meta.style.color = 'var(--muted)';
meta.innerHTML = `
<div>Value: <code>${currentValue}</code></div>
${isBareIconName(currentValue) ? `<div class="icon-default-hint">Default icon path: <code>${resolveIconURL(currentValue)}</code></div>` : ''}
`;
preview.appendChild(img);
preview.appendChild(meta);
imageContainer.appendChild(preview);
}
const fieldWithUpload = document.createElement('div');
fieldWithUpload.className = 'field-with-upload';
const textInput = document.createElement('input');
textInput.className = 'field-input';
textInput.type = 'text';
textInput.value = currentValue || '';
textInput.name = currentPath;
textInput.placeholder = isIconField
? 'telegram or /static/icons/telegram.svg or uploaded URL'
: 'Enter image path or upload file';
// Live preview update
textInput.addEventListener('input', () => {
const preview = imageContainer.querySelector('.current-image-preview');
if (!preview) return;
let img = preview.querySelector('img');
if (!img) {
img = document.createElement('img');
preview.prepend(img);
}
img.style.display = '';
img.src = resolveIconURL(textInput.value);
applyIconClass(img, textInput.value); // <-- keep icon size when typing
const meta = preview.querySelector('div[style*="font-size: 12px"]');
if (meta) {
meta.innerHTML = `
<div>Value: <code>${textInput.value}</code></div>
${isBareIconName(textInput.value) ? `<div class="icon-default-hint">Default icon path: <code>${resolveIconURL(textInput.value)}</code></div>` : ''}
`;
}
});
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.className = 'image-upload-input';
fileInput.style.display = 'none';
fileInput.id = `file-${currentPath.replace(/[\.\[\]]/g, '_')}`;
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-sm btn-secondary';
uploadBtn.textContent = '📁 Upload';
uploadBtn.onclick = () => fileInput.click();
const progressDiv = document.createElement('div');
progressDiv.className = 'upload-progress';
progressDiv.textContent = 'Uploading...';
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
progressDiv.style.display = 'block';
uploadBtn.disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('plugin', pluginName);
formData.append('field', currentPath);
const response = await fetch('/admin/api/upload', { method: 'POST', body: formData });
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
textInput.value = result.url;
let preview = imageContainer.querySelector('.current-image-preview');
if (!preview) {
preview = document.createElement('div');
preview.className = 'current-image-preview';
imageContainer.insertBefore(preview, fieldWithUpload);
}
preview.innerHTML = '';
const newImg = document.createElement('img');
newImg.src = result.url;
newImg.alt = `Current ${key}`;
applyIconClass(newImg, result.url); // <-- apply small icon styling to uploaded icon too
const meta = document.createElement('div');
meta.style.fontSize = '12px';
meta.style.color = 'var(--muted)';
meta.innerHTML = `<div>Value: <code>${result.url}</code></div>`;
preview.appendChild(newImg);
preview.appendChild(meta);
showNotification('Image uploaded successfully!', 'success');
} catch (err) {
showNotification('Upload failed: ' + err.message, 'error');
} finally {
progressDiv.style.display = 'none';
uploadBtn.disabled = false;
}
});
fieldWithUpload.appendChild(textInput);
fieldWithUpload.appendChild(uploadBtn);
imageContainer.appendChild(fieldWithUpload);
imageContainer.appendChild(fileInput);
imageContainer.appendChild(progressDiv);
field.appendChild(imageContainer);
}
function renderManagedArray(container, pluginName, fieldName, items, itemSchema) {
const arrayContainer = document.createElement('div');
arrayContainer.className = 'managed-array-container';
const header = document.createElement('div');
header.className = 'managed-array-header';
header.innerHTML = `
<h4>${formatFieldName(fieldName)} (<span class="item-count">${items.length}</span> items)</h4>
`;
arrayContainer.appendChild(header);
const itemsContainer = document.createElement('div');
itemsContainer.className = 'managed-array-items';
itemsContainer.id = `managed-array-${fieldName}`;
items.forEach((item, index) => {
renderManagedArrayItem(itemsContainer, pluginName, fieldName, item, index, itemSchema);
});
arrayContainer.appendChild(itemsContainer);
const footer = document.createElement('div');
footer.className = 'managed-array-footer';
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'btn btn-secondary add-array-item';
addBtn.textContent = `Add ${fieldName.slice(0, -1)}`;
addBtn.onclick = () => addManagedArrayItem(pluginName, fieldName);
footer.appendChild(addBtn);
arrayContainer.appendChild(footer);
container.appendChild(arrayContainer);
}
function renderManagedArrayItem(container, pluginName, fieldName, item, index, itemSchema) {
const itemDiv = document.createElement('div');
itemDiv.className = 'managed-array-item';
itemDiv.setAttribute('data-index', index);
const itemHeader = document.createElement('div');
itemHeader.className = 'managed-array-item-header';
itemHeader.innerHTML = `
<span class="item-number">#${index + 1}</span>
<button type="button" class="btn btn-danger btn-sm remove-item"
onclick="removeManagedArrayItem(this, '${pluginName}', '${fieldName}')">Remove</button>
`;
itemDiv.appendChild(itemHeader);
const itemContent = document.createElement('div');
itemContent.className = 'managed-array-item-content';
Object.keys(itemSchema).forEach(key => {
const value = item[key] !== undefined ? item[key] : getDefaultValueForType(itemSchema[key]);
renderSchemaField(itemContent, `${fieldName}[${index}].${key}`, key, value, itemSchema[key], pluginName, `${fieldName}[${index}]`);
});
itemDiv.appendChild(itemContent);
container.appendChild(itemDiv);
}
function renderSchemaField(container, fieldPath, fieldName, value, fieldType, pluginName, arrayPath) {
const field = document.createElement('div');
field.className = 'schema-field';
const label = document.createElement('label');
label.textContent = formatFieldName(fieldName);
label.className = 'schema-field-label';
field.appendChild(label);
if (String(fieldName).toLowerCase() === 'icon') {
createImageField(field, fieldPath, value, fieldName, pluginName);
container.appendChild(field);
return;
}
if (String(fieldName).toLowerCase() === 'output') {
const textarea = document.createElement('textarea');
textarea.className = 'field-input field-textarea neofetch-output-field';
textarea.rows = 20;
textarea.value = value || '';
textarea.name = fieldPath;
textarea.placeholder = 'Paste neofetch output here...';
textarea.style.fontFamily = 'monospace';
textarea.style.fontSize = '12px';
textarea.style.whiteSpace = 'pre';
textarea.style.overflowX = 'auto';
field.appendChild(textarea);
container.appendChild(field);
return;
}
let input;
switch (fieldType) {
case 'text':
input = document.createElement('textarea');
input.className = 'field-input field-textarea';
input.rows = 3;
input.value = value || '';
input.name = fieldPath;
break;
case 'image':
createImageField(field, fieldPath, value, fieldName, pluginName);
container.appendChild(field);
return;
case 'select':
input = document.createElement('select');
input.className = 'field-input';
if (fieldName === 'type') {
['image', 'gif', 'text'].forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
opt.selected = value === option;
input.appendChild(opt);
});
}
input.name = fieldPath;
break;
case 'array':
input = document.createElement('textarea');
input.className = 'field-input field-textarea';
input.rows = 2;
input.placeholder = 'Enter comma-separated values';
input.value = Array.isArray(value) ? value.join(', ') : (value || '');
input.name = fieldPath;
break;
case 'object':
input = document.createElement('textarea');
input.className = 'field-input field-textarea';
input.rows = 4;
input.placeholder = 'Enter JSON object';
input.value = typeof value === 'object' ? JSON.stringify(value, null, 2) : (value || '{}');
input.name = fieldPath;
break;
default:
input = document.createElement('input');
input.className = 'field-input';
input.type = 'text';
input.value = value || '';
input.name = fieldPath;
}
if (input) {
input.placeholder = getPlaceholder(fieldName);
field.appendChild(input);
}
container.appendChild(field);
}
function renderGenericArray(field, value, currentPath, pluginName) {
const arrayContainer = document.createElement('div');
arrayContainer.className = 'array-container';
const arrayHeader = document.createElement('div');
arrayHeader.className = 'array-header';
arrayHeader.innerHTML = `<div class="array-title">Items (${value.length})</div>`;
arrayContainer.appendChild(arrayHeader);
value.forEach((item, index) => {
createArrayItem(arrayContainer, item, currentPath, index, pluginName);
});
const addBtn = document.createElement('button');
addBtn.className = 'add-btn btn btn-sm btn-secondary';
addBtn.textContent = 'Add Item';
addBtn.type = 'button';
addBtn.onclick = () => addArrayItem(arrayContainer, currentPath, pluginName);
field.appendChild(arrayContainer);
field.appendChild(addBtn);
}
function createArrayItem(container, item, path, index, pluginName) {
const itemDiv = document.createElement('div');
itemDiv.className = 'array-item';
const itemContent = document.createElement('div');
itemContent.className = 'array-item-content';
if (typeof item === 'string') {
const input = document.createElement('input');
input.className = 'field-input';
input.value = item;
input.name = `${path}[${index}]`;
input.placeholder = 'Enter value...';
itemContent.appendChild(input);
} else if (typeof item === 'object' && item !== null) {
const textarea = document.createElement('textarea');
textarea.className = 'field-input field-textarea';
textarea.value = JSON.stringify(item, null, 2);
textarea.name = `${path}[${index}]`;
textarea.placeholder = 'Enter JSON object...';
textarea.rows = Math.min(Math.max(3, Object.keys(item).length + 1), 8);
itemContent.appendChild(textarea);
} else {
const input = document.createElement('input');
input.className = 'field-input';
input.value = String(item);
input.name = `${path}[${index}]`;
input.placeholder = 'Enter value...';
itemContent.appendChild(input);
}
const controls = document.createElement('div');
controls.className = 'array-item-controls';
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-sm btn-danger remove-btn';
removeBtn.textContent = 'Remove';
removeBtn.type = 'button';
removeBtn.onclick = () => {
itemDiv.remove();
updateArrayTitle(container);
reindexArrayItems(container, path);
};
controls.appendChild(removeBtn);
itemDiv.appendChild(itemContent);
itemDiv.appendChild(controls);
container.appendChild(itemDiv);
}
function addManagedArrayItem(pluginName, fieldName) {
let container = document.getElementById(`managed-array-${fieldName}`);
if (!container) {
console.error(`Container not found for field ${fieldName}`);
return;
}
const index = container.querySelectorAll('.managed-array-item').length;
const pluginType = getPluginType(pluginName);
const itemSchema = pluginType.itemSchema;
const emptyItem = {};
Object.keys(itemSchema).forEach(key => {
emptyItem[key] = getDefaultValueForType(itemSchema[key]);
});
renderManagedArrayItem(container, pluginName, fieldName, emptyItem, index, itemSchema);
updateManagedArrayTitle(pluginName, fieldName);
}
function removeManagedArrayItem(button, pluginName, fieldName) {
const item = button.closest('.managed-array-item');
const container = item.parentElement;
item.remove();
reindexManagedArrayItems(container);
updateManagedArrayTitle(pluginName, fieldName);
}
function reindexManagedArrayItems(container) {
const items = container.querySelectorAll('.managed-array-item');
items.forEach((item, newIndex) => {
item.setAttribute('data-index', newIndex);
const itemNumber = item.querySelector('.item-number');
if (itemNumber) {
itemNumber.textContent = `#${newIndex + 1}`;
}
const inputs = item.querySelectorAll('.field-input');
inputs.forEach(input => {
const name = input.name;
if (name && name.includes('[') && name.includes(']')) {
const parts = name.split('[');
const fieldName = parts[0];
const propertyName = name.split('.').pop();
input.name = `${fieldName}[${newIndex}].${propertyName}`;
}
});
});
}
function reindexArrayItems(container, path) {
const items = container.querySelectorAll('.array-item');
items.forEach((item, newIndex) => {
const input = item.querySelector('.field-input');
if (input && input.name) {
input.name = `${path}[${newIndex}]`;
}
});
}
function addArrayItem(container, path, pluginName) {
const index = container.querySelectorAll('.array-item').length;
createArrayItem(container, '', path, index, pluginName);
updateArrayTitle(container);
}
function updateManagedArrayTitle(pluginName, fieldName) {
const container = document.getElementById(`managed-array-${fieldName}`);
if (!container) return;
const count = container.querySelectorAll('.managed-array-item').length;
const countSpan = container.parentElement.querySelector('.managed-array-header .item-count');
if (countSpan) countSpan.textContent = count;
}
function updateArrayTitle(container) {
const title = container.querySelector('.array-title');
if (title) {
const count = container.querySelectorAll('.array-item').length;
title.textContent = `Items (${count})`;
}
}
function addObjectSetting(container, pluginName) {
const settingName = prompt('Enter setting name:');
if (!settingName) return;
const settingType = prompt('Enter setting type (string/number/boolean/object/array):', 'string');
const defaultValue = getDefaultValueForType(settingType);
createField(settingName, defaultValue, container, '', pluginName);
}
function addNestedField(container, parentPath) {
const fieldName = prompt('Enter field name:');
if (!fieldName) return;
const fieldType = prompt('Enter field type (string/number/boolean):', 'string');
const defaultValue = getDefaultValueForType(fieldType);
createField(fieldName, defaultValue, container, parentPath);
}
function collectSettings(form) {
const settings = {};
const inputs = form.querySelectorAll('.field-input, input[type="checkbox"]');
inputs.forEach(input => {
const name = input.name;
if (!name) return;
let value = input.type === 'checkbox' ? input.checked : input.value;
if (input.type === 'number') {
value = parseFloat(value) || 0;
} else if (input.tagName === 'TEXTAREA') {
if (name.includes('ascii') || name.includes('colors')) {
value = value.split('\n').filter(line => line.trim() !== '');
} else if (name.includes('[') && name.includes(']')) {
if (value.trim().startsWith('{') || value.trim().startsWith('[')) {
try {
value = JSON.parse(value);
} catch (e) {
if (name.includes('technologies')) {
value = value.split(',').map(tech => tech.trim()).filter(tech => tech !== '');
}
}
} else if (name.includes('technologies')) {
value = value.split(',').map(tech => tech.trim()).filter(tech => tech !== '');
}
}
}
setNestedValue(settings, name, value);
});
// Handle managed arrays
const managedArrays = form.querySelectorAll('.managed-array-items');
managedArrays.forEach(arrayContainer => {
const arrayName = arrayContainer.id.replace('managed-array-', '');
const items = arrayContainer.querySelectorAll('.managed-array-item');
if (!settings[arrayName]) {
settings[arrayName] = [];
}
items.forEach((item, index) => {
const itemData = {};
const itemInputs = item.querySelectorAll('.field-input');
itemInputs.forEach(input => {
const fieldName = input.name.split('.').pop();
let value = input.value;
if (input.tagName === 'TEXTAREA' && fieldName === 'technologies') {
value = value.split(',').map(tech => tech.trim()).filter(tech => tech !== '');
}
itemData[fieldName] = value;
});
if (Object.keys(itemData).length > 0) {
settings[arrayName][index] = itemData;
}
});
});
return settings;
}
function setNestedValue(obj, path, value) {
const keys = path.split(/[\.\[\]]+/).filter(k => k !== '');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const nextKey = keys[i + 1];
if (!isNaN(nextKey) && nextKey !== '') {
if (!(key in current)) current[key] = [];
if (!Array.isArray(current[key])) current[key] = [];
current = current[key];
} else {
if (!(key in current)) current[key] = {};
if (typeof current[key] !== 'object' || Array.isArray(current[key])) {
current[key] = {};
}
current = current[key];
}
}
const lastKey = keys[keys.length - 1];
if (Array.isArray(current)) {
const index = parseInt(lastKey);
if (!isNaN(index)) {
current[index] = value;
}
} else {
current[lastKey] = value;
}
}
// Helper functions
function isImageField(key, value) {
const imageKeys = ['image', 'profileimage', 'avatar', 'cover', 'icon', 'logo', 'photo', 'picture', 'imagecropped', 'favicon'];
const keyLower = key.toLowerCase();
const isImageKey = imageKeys.some(imgKey => keyLower.includes(imgKey));
const isImageValue = typeof value === 'string' &&
(value.match(/\.(jpg|jpeg|png|gif|webp|svg|ico)$/i) ||
value.includes('/static/') ||
value.includes('/media/') ||
value.includes('http://') ||
value.includes('https://'));
return isImageKey || isImageValue;
}
function getDefaultValueForType(fieldType) {
switch (fieldType) {
case 'array': return [];
case 'object': return {};
case 'number': return 0;
case 'boolean': return false;
case 'text': return '';
case 'image': return '';
default: return '';
}
}
function formatFieldName(key) {
return key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.trim();
}
function getPlaceholder(key) {
const placeholders = {
url: 'https://example.com',
email: 'user@example.com',
name: 'Enter name...',
title: 'Enter title...',
description: 'Enter description...',
bio: 'Enter biography...',
content: 'Enter content...',
username: 'Enter username...',
apikey: 'Enter API key...',
api_key: 'Enter API key...',
token: 'Enter token...',
steamid: 'Enter Steam ID...',
image: '/static/images/example.jpg',
icon: 'telegram or /static/icons/telegram.svg',
sectiontitle: 'Section Title',
webring_url: 'https://webring.example.com',
sourcecodeur: 'https://github.com/user/repo',
hostname: 'localhost',
github: 'https://github.com/user/repo',
live: 'https://example.com',
text: 'Enter text...',
source: 'Source...',
category: 'Category',
refreshInterval: '300'
};
const keyLower = key.toLowerCase();
for (const [k, v] of Object.entries(placeholders)) {
if (keyLower.includes(k)) return v;
}
return 'Enter value...';
}
function getDefaultSectionTitle(pluginName) {
const titles = {
'profile': 'Profile',
'social': 'Links',
'techstack': 'Technologies',
'projects': 'Projects',
'lastfm': 'Music',
'beatleader': 'BeatLeader Stats',
'steam': 'Gaming Activity',
'neofetch': 'System Information',
'webring': 'webring',
'visitors': 'Visitors',
'services': 'Local Services',
'code': 'Coding Stats',
'info': 'Page Info',
'personal': 'Personal Info',
'meme': 'Random Meme'
};
return titles[pluginName] || formatFieldName(pluginName);
}
// Initialize functions
function initSortable() {
new Sortable(document.getElementById('plugins-container'), {
animation: 150,
ghostClass: 'sortable-ghost',
handle: '.plugin-header',
onEnd: function(evt) {
updatePluginOrder();
}
});
}
function initSearch() {
const searchInput = document.getElementById('plugin-search');
const plugins = document.querySelectorAll('.plugin');
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
plugins.forEach(plugin => {
const name = plugin.dataset.plugin.toLowerCase();
const description = plugin.querySelector('.plugin-description').textContent.toLowerCase();
const matches = name.includes(query) || description.includes(query);
plugin.style.display = matches ? 'block' : 'none';
});
});
}
function updatePluginOrder() {
const plugins = Array.from(document.querySelectorAll('.plugin'));
const pluginsData = plugins.map((plugin, index) => {
const pluginName = plugin.dataset.plugin;
const form = plugin.querySelector('.plugin-form');
const toggle = plugin.querySelector('.plugin-toggle');
return {
name: pluginName,
enabled: toggle.checked,
order: index,
settings: collectSettings(form)
};
});
fetch('/admin/api/plugins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(pluginsData)
})
.then(response => response.json())
.then(result => {
if (result.success) {
showNotification('Plugin order updated successfully!', 'success');
plugins.forEach((plugin, index) => {
const orderInput = plugin.querySelector('.order-input');
if (orderInput) {
orderInput.value = index;
}
});
}
})
.catch(err => {
showNotification('Failed to update order: ' + err.message, 'error');
});
}
// Form submission handlers
document.addEventListener('DOMContentLoaded', function() {
// Handle individual plugin form submissions
document.querySelectorAll('.plugin-form').forEach(form => {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const pluginName = this.dataset.plugin;
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.innerHTML = '<div class="loading"></div> Saving...';
submitBtn.disabled = true;
try {
const formData = new FormData();
const settings = collectSettings(this);
const toggle = document.querySelector(`.plugin-toggle[data-plugin="${pluginName}"]`);
const orderInput = document.querySelector(`.plugin[data-plugin="${pluginName}"] .order-input`);
formData.append('plugin', pluginName);
formData.append('enabled', toggle.checked);
formData.append('order', orderInput ? orderInput.value : '0');
formData.append('settings', JSON.stringify(settings));
const response = await fetch('/admin/api/plugin', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
showNotification(result.message, 'success');
// Update the settings in memory
currentPluginData[pluginName] = result.settings || settings;
// Refresh the form with new data
const container = document.getElementById(`settings-${pluginName}`);
if (container) {
container.innerHTML = '';
initSettingsEditor(pluginName, currentPluginData[pluginName]);
}
} else {
const error = await response.text();
throw new Error(error);
}
} catch (err) {
showNotification('Error: ' + err.message, 'error');
} finally {
submitBtn.textContent = originalText;
submitBtn.disabled = false;
}
});
});
// Handle toggle changes
document.querySelectorAll('.plugin-toggle').forEach(toggle => {
toggle.addEventListener('change', function() {
const form = document.querySelector(`.plugin-form[data-plugin="${this.dataset.plugin}"]`);
if (form) {
form.dispatchEvent(new Event('submit', { bubbles: true }));
}
});
});
// Handle order changes
document.querySelectorAll('.order-input').forEach(input => {
input.addEventListener('change', function() {
const plugin = this.closest('.plugin');
const form = plugin.querySelector('.plugin-form');
if (form) {
plugin.dataset.order = this.value;
form.dispatchEvent(new Event('submit', { bubbles: true }));
updatePluginOrder();
}
});
});
});
// Utility functions
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
Object.assign(notification.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '8px 16px',
borderRadius: '4px',
color: 'white',
fontWeight: '500',
zIndex: '10000',
transform: 'translateX(400px)',
transition: 'transform 0.3s ease',
maxWidth: '300px'
});
switch (type) {
case 'success':
notification.style.background = '#4CAF50';
break;
case 'error':
notification.style.background = '#f44336';
break;
case 'info':
notification.style.background = '#2196F3';
break;
default:
notification.style.background = '#333';
}
document.body.appendChild(notification);
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
notification.style.transform = 'translateX(400px)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
function saveAllPlugins() {
const forms = document.querySelectorAll('.plugin-form');
let saved = 0;
const total = forms.length;
if (total === 0) {
showNotification('No plugins to save!', 'info');
return;
}
showNotification('Saving all plugins...', 'info');
forms.forEach(form => {
form.dispatchEvent(new Event('submit', { bubbles: true }));
saved++;
if (saved === total) {
setTimeout(() => {
showNotification('All plugins saved successfully!', 'success');
}, 1000);
}
});
}
function previewSite() {
window.open('/', '_blank');
}
function refreshData() {
showNotification('Refreshing data...', 'info');
setTimeout(() => {
window.location.reload();
}, 1000);
}
async function reloadPlugin(pluginName) {
try {
const response = await fetch(`/admin/api/plugin/reload?plugin=${pluginName}`);
if (response.ok) {
const data = await response.json();
// Update the plugin data
currentPluginData[pluginName] = data.settings;
// Refresh the form
const container = document.getElementById(`settings-${pluginName}`);
if (container) {
container.innerHTML = '';
initSettingsEditor(pluginName, data.settings);
}
// Update toggle and order
const toggle = document.querySelector(`.plugin-toggle[data-plugin="${pluginName}"]`);
if (toggle) {
toggle.checked = data.enabled;
}
const orderInput = document.querySelector(`.plugin[data-plugin="${pluginName}"] .order-input`);
if (orderInput) {
orderInput.value = data.order;
}
showNotification(`Plugin ${pluginName} reloaded successfully`, 'success');
} else {
throw new Error('Failed to reload plugin');
}
} catch (err) {
showNotification('Error reloading plugin: ' + err.message, 'error');
}
}
async function reloadConfig() {
try {
const response = await fetch('/admin/api/refresh', {
method: 'POST'
});
if (response.ok) {
showNotification('Configuration reloaded from disk', 'success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
throw new Error('Failed to reload configuration');
}
} catch (err) {
showNotification('Error: ' + err.message, 'error');
}
}
function isBareIconName(val) {
return typeof val === 'string' && val.trim() !== '' && !/[\/.]/.test(val); // no slash, no dot
}
function resolveIconURL(val) {
if (!val) return '';
return isBareIconName(val) ? `/static/icons/${val}.svg` : val;
}
// Export functions for global access
window.addManagedArrayItem = addManagedArrayItem;
window.removeManagedArrayItem = removeManagedArrayItem;
window.saveAllPlugins = saveAllPlugins;
window.previewSite = previewSite;
window.refreshData = refreshData;
window.reloadPlugin = reloadPlugin;
window.reloadConfig = reloadConfig;
window.initSettingsEditor = initSettingsEditor;