mirror of
https://github.com/Alexander-D-Karpov/about.git
synced 2026-03-16 22:06:08 +03:00
1300 lines
44 KiB
JavaScript
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; |