Files
bookmarkly2/tabs_settings.php
T

706 lines
32 KiB
PHP

<?php
$page_title = 'Manage Tabs - Bookmarkly';
// Add tabs-settings-page class to body for CSS targeting
$GLOBALS['body_class'] = 'tabs-settings-page';
require_once 'include/auth.php';
// --- Security Check: Only Admin can access this page ---
if (!isAdmin()) {
header('Location: index.php');
exit;
}
// Fetch all users for various UI elements
$users_stmt = $pdo->query("SELECT id, username, role FROM users ORDER BY username");
$users = $users_stmt->fetchAll();
require_once 'include/header.php';
?>
<div class="container mt-4">
<h1>Manage Tabs</h1>
<!-- Add Tab Form -->
<div class="card mb-4">
<div class="card-header">Add New Tab</div>
<div class="card-body">
<form id="add-tab-form">
<div class="row">
<div class="col-md-6 mb-3">
<label for="new-tab-name" class="form-label">Tab Name</label>
<input type="text" class="form-control" id="new-tab-name" required>
</div>
<div class="col-md-4 mb-3">
<label for="new-tab-user" class="form-label">Assign to User (Owner)</label>
<select id="new-tab-user" class="form-select">
<option value="">None (Public)</option>
<?php foreach ($users as $user): ?>
<?php if ($user['role'] === 'user' || $user['role'] === 'admin'): ?>
<option value="<?php echo $user['id']; ?>"><?php echo htmlspecialchars($user['username']); ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex align-items-end mb-3">
<button type="submit" class="btn btn-primary w-100">Add Tab</button>
</div>
</div>
</form>
</div>
</div>
<!-- Tab List & Permissions Matrix -->
<div class="card">
<div class="card-header">Existing Tabs & Permissions</div>
<div class="card-body">
<div id="tabs-matrix-container">
<!-- Tabs and matrix will be loaded here via JavaScript -->
</div>
</div>
</div>
</div>
<!-- Edit Tab Modal -->
<div class="modal fade" id="editTabModal" tabindex="-1" aria-labelledby="editTabModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editTabModalLabel">Edit Tab</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-tab-id">
<div class="mb-3">
<label for="edit-tab-name" class="form-label">Tab Name</label>
<input type="text" class="form-control" id="edit-tab-name" required>
</div>
<div class="mb-3">
<label for="edit-tab-owner" class="form-label">Owner</label>
<select id="edit-tab-owner" class="form-select">
<option value="">None (Public)</option>
<?php foreach ($users as $user): ?>
<?php if ($user['role'] === 'user' || $user['role'] === 'admin'): ?>
<option value="<?php echo $user['id']; ?>"><?php echo htmlspecialchars($user['username']); ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="edit-tab-column-count" class="form-label">Column Count (Tablet/Desktop)</label>
<select id="edit-tab-column-count" class="form-select">
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<div class="mb-3">
<label for="edit-tab-login-url" class="form-label"><?php echo translate('tab_unique_url_label'); ?></label>
<div class="input-group">
<input type="text" class="form-control" id="edit-tab-login-url" readonly style="cursor: pointer;" data-bs-toggle="tooltip" title="<?php echo translate('tooltip_copy_to_clipboard'); ?>">
<button class="btn btn-outline-secondary" id="generate-token-btn-modal" type="button" title="<?php echo translate('tab_generate_token_button'); ?>"><i class="bi bi-arrow-clockwise"></i></button>
<button class="btn btn-outline-danger" id="clear-token-btn-modal" type="button" title="<?php echo translate('tab_clear_token_button'); ?>"><i class="bi bi-trash"></i></button>
</div>
</div>
<div class="d-grid">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#themeSettingsModal">
<i class="bi bi-palette-fill"></i> Edit Tab Theme & Background
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="save-tab-changes">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Theme & Background Settings Modal -->
<div class="modal fade" id="themeSettingsModal" tabindex="-1" aria-labelledby="themeSettingsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="themeSettingsModalLabel">Tab Theme & Background Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted">These settings will override the global theme for this tab only. Leave fields blank to use the global default.</p>
<hr>
<!-- Theme -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit-tab-theme" class="form-label">Theme</label>
<select class="form-select" id="edit-tab-theme">
<option value="">-- Use Global Default --</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="transparent">Transparent</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="edit-tab-theme-transparency" class="form-label">Theme Transparency: <span id="transparency-value-tab">85</span>%</label>
<input type="range" class="form-range" id="edit-tab-theme-transparency" min="0" max="100" value="85">
</div>
</div>
<hr>
<!-- Pexels Search -->
<div class="row g-3 mb-3">
<div class="col-md-9">
<label for="pexels_search_query_tab" class="form-label">Search Pexels for a background</label>
<input type="text" id="pexels_search_query_tab" class="form-control" placeholder="e.g., nature, city, abstract">
</div>
<div class="col-md-3 d-flex align-items-end">
<button class="btn btn-info w-100" type="button" id="pexels_search_button_tab">Search</button>
</div>
</div>
<div id="pexels_results_tab" class="row row-cols-2 row-cols-md-4 g-2 mb-4" style="max-height: 200px; overflow-y: auto;"></div>
<!-- Custom Background Upload -->
<div class="mb-3">
<label for="tab-bg-upload" class="form-label">Or upload your own background</label>
<input class="form-control" type="file" id="tab-bg-upload" accept="image/*">
<div id="upload-status" class="form-text"></div>
</div>
<!-- Background URL -->
<div class="mb-3">
<label for="edit-tab-bg-url" class="form-label">Background Image URL</label>
<input type="text" class="form-control" id="edit-tab-bg-url" placeholder="Paste a URL or upload/search to fill">
<div id="current-bg-info" class="mt-2"></div>
</div>
<!-- Background Brightness -->
<div class="mb-3">
<label for="edit-tab-bg-brightness" class="form-label">Background Brightness: <span id="brightness-value-tab">100</span>%</label>
<input type="range" class="form-range" id="edit-tab-bg-brightness" min="0" max="150" value="100">
</div>
<!-- Background Saturation -->
<div class="mb-3">
<label for="edit-tab-bg-saturation" class="form-label">Background Saturation: <span id="saturation-value-tab">100</span>%</label>
<input type="range" class="form-range" id="edit-tab-bg-saturation" min="0" max="200" value="100">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-danger me-auto" id="remove-tab-background">Remove Background</button>
<button type="button" class="btn btn-primary" data-bs-target="#editTabModal" data-bs-toggle="modal">Apply and Go Back</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const addTabForm = document.getElementById('add-tab-form');
const newTabNameInput = document.getElementById('new-tab-name');
const newTabUserInput = document.getElementById('new-tab-user');
const matrixContainer = document.getElementById('tabs-matrix-container');
const editTabModal = new bootstrap.Modal(document.getElementById('editTabModal'));
const saveTabChangesBtn = document.getElementById('save-tab-changes');
// Background Modal elements
const themeSettingsModal = new bootstrap.Modal(document.getElementById('themeSettingsModal'));
const pexelsSearchBtnTab = document.getElementById('pexels_search_button_tab');
const pexelsQueryInputTab = document.getElementById('pexels_search_query_tab');
const pexelsResultsContainerTab = document.getElementById('pexels_results_tab');
const bgUrlInputTab = document.getElementById('edit-tab-bg-url');
const bgBrightnessInputTab = document.getElementById('edit-tab-bg-brightness');
const bgSaturationInputTab = document.getElementById('edit-tab-bg-saturation');
const removeTabBgBtn = document.getElementById('remove-tab-background');
const bgUploadInput = document.getElementById('tab-bg-upload');
const uploadStatusDiv = document.getElementById('upload-status');
const currentBgInfoDiv = document.getElementById('current-bg-info');
// Theme elements
const themeInput = document.getElementById('edit-tab-theme');
const transparencyInput = document.getElementById('edit-tab-theme-transparency');
const transparencyValueSpan = document.getElementById('transparency-value-tab');
const users = <?php echo json_encode($users); ?>;
const translations = {
defaultTabName: "<?php echo translate('tab_default_name'); ?>"
};
let tabsData = [];
let visibilityData = [];
// Main function to fetch all data and render the matrix
async function initializeMatrix() {
matrixContainer.innerHTML = '<p>Loading...</p>';
try {
const [tabsResponse, visibilityResponse] = await Promise.all([
fetch('api/tabs.php'),
fetch('api/tab_visibility.php')
]);
if (!tabsResponse.ok || !visibilityResponse.ok) {
throw new Error('Failed to fetch initial data.');
}
tabsData = await tabsResponse.json();
visibilityData = await visibilityResponse.json();
renderMatrix(tabsData);
} catch (error) {
console.error('Initialization Error:', error);
matrixContainer.innerHTML = '<p class="text-danger">Failed to load tab permissions.</p>';
}
}
// Render the entire matrix of tabs and user permissions
function renderMatrix(tabs) {
if (tabs.length === 0) {
matrixContainer.innerHTML = '<p>No tabs found.</p>';
return;
}
let html = '<div class="table-responsive"><table class="table table-bordered align-middle">';
// Header Row
html += `
<thead>
<tr class="text-center">
<th style="width: 25%;">Tab Name</th>
`;
users.forEach(user => {
html += `<th>${escapeHTML(user.username)} <small class="text-muted">(${user.role})</small></th>`;
});
html += '<th style="width: 15%;">Actions</th>';
html += '</tr></thead>';
// Body Rows (one for each tab)
html += '<tbody>';
tabs.forEach(tab => {
const displayName = tab.name === 'Default' ? translations.defaultTabName : tab.name;
html += `<tr>`;
html += `<td>
<strong>${escapeHTML(displayName)}</strong>
<div class="btn-group btn-group-sm float-end">
<button class="btn btn-outline-secondary edit-tab-btn" data-tab-id="${tab.id}" title="Edit Tab"><i class="bi bi-pencil"></i></button>
<button class="btn btn-outline-danger delete-tab-btn" data-tab-id="${tab.id}" title="Delete Tab"><i class="bi bi-trash"></i></button>
</div>
</td>`;
users.forEach(user => {
const isVisible = !visibilityData.some(v => v.user_id == user.id && v.tab_id == tab.id);
const isOwner = tab.user_id == user.id;
let radioHtml = `
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="visibility-${tab.id}-${user.id}" id="show-${tab.id}-${user.id}" value="1" ${isVisible ? 'checked' : ''}>
<label class="form-check-label" for="show-${tab.id}-${user.id}"><i class="bi bi-eye"></i></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="visibility-${tab.id}-${user.id}" id="hide-${tab.id}-${user.id}" value="0" ${!isVisible ? 'checked' : ''}>
<label class="form-check-label" for="hide-${tab.id}-${user.id}"><i class="bi bi-eye-slash"></i></label>
</div>
`;
if (isOwner) {
radioHtml += '<div class="text-muted fst-italic" style="font-size: 0.8em;">Owner</div>';
}
html += `<td class="text-center" data-tab-id="${tab.id}" data-user-id="${user.id}">${radioHtml}</td>`;
});
// Actions Cell
const deleteDisabled = tab.id == 1 ? 'disabled' : '';
html += `
<td class="text-center">
<button class="btn btn-sm btn-outline-secondary me-2 edit-tab-btn" data-tab-id="${tab.id}" data-tab-name="${escapeHTML(tab.name)}">Edit</button>
<button class="btn btn-sm btn-outline-danger delete-tab-btn" data-tab-id="${tab.id}" ${deleteDisabled}>Delete</button>
</td>
`;
html += '</tr>';
});
html += '</tbody></table></div>';
matrixContainer.innerHTML = html;
}
// Event Delegation for radio button changes and other actions
matrixContainer.addEventListener('click', async function(event) {
const button = event.target.closest('button');
if (!button) {
// Handle radio button changes if a button was not the source
if (event.target.type === 'radio') {
const td = event.target.closest('td');
const tabId = td.dataset.tabId;
const userId = td.dataset.userId;
const isVisible = event.target.value === '1';
try {
const response = await fetch('api/tab_visibility.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tab_id: tabId, user_id: userId, is_visible: isVisible })
});
if (!response.ok) throw new Error('Failed to update visibility.');
// Update local data and re-render for consistency
const visibilityIndex = visibilityData.findIndex(v => v.user_id == userId && v.tab_id == tabId);
if (isVisible && visibilityIndex > -1) {
visibilityData.splice(visibilityIndex, 1);
} else if (!isVisible && visibilityIndex === -1) {
visibilityData.push({user_id: userId, tab_id: tabId});
}
// No full re-render needed, UI updated instantly
} catch (error) {
console.error('Update Error:', error);
alert('Error updating visibility. Please refresh the page.');
}
}
return;
}
// Handle button clicks
const tabId = button.dataset.tabId;
const tabData = tabsData.find(t => t.id == tabId);
if (button.classList.contains('edit-tab-btn')) {
if (tabData) {
document.getElementById('edit-tab-id').value = tabData.id;
document.getElementById('edit-tab-name').value = tabData.name;
document.getElementById('edit-tab-owner').value = tabData.user_id || '';
document.getElementById('edit-tab-column-count').value = tabData.column_count || '5';
// Populate login url
const loginUrlInput = document.getElementById('edit-tab-login-url');
loginUrlInput.value = tabData.login_token ? `${window.location.origin}/index.php?token=${tabData.login_token}` : '';
// Populate theme & background fields
themeInput.value = tabData.theme || '';
transparencyInput.value = tabData.theme_transparency ?? '85';
bgUrlInputTab.value = tabData.background_image_url || '';
bgBrightnessInputTab.value = tabData.background_brightness ?? '100';
bgSaturationInputTab.value = tabData.background_saturation ?? '100';
updateSliderLabels();
updateCurrentBgInfo();
themeSettingsModal.hide(); // Ensure it's hidden before showing main modal
editTabModal.show();
}
}
if (button.classList.contains('delete-tab-btn')) {
if (tabData && confirm(`Are you sure you want to delete the tab "${tabData.name}"? This cannot be undone.`)) {
try {
await fetch('api/tabs.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: tabId })
});
await initializeMatrix(); // Refresh the whole matrix
} catch (error) {
console.error('Delete Error:', error);
alert('Error deleting tab.');
}
}
}
});
// Generate Token
async function generateToken(tabId) {
try {
const formData = new FormData();
formData.append('tab_id', tabId);
const response = await fetch('api/generate_token.php', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed to generate token.');
const data = await response.json();
if (data.success && data.token) {
const loginUrlInput = document.getElementById('edit-tab-login-url');
const newUrl = `${window.location.origin}/index.php?token=${data.token}`;
loginUrlInput.value = newUrl;
// Update tabsData to reflect the new token
const tab = tabsData.find(t => t.id == tabId);
if (tab) {
tab.login_token = data.token;
}
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (error) {
console.error('Token Generation Error:', error);
alert(`Error generating token: ${error.message}`);
}
}
// Clear Token
async function clearToken(tabId) {
try {
const formData = new FormData();
formData.append('tab_id', tabId);
const response = await fetch('api/clear_token.php', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed to clear token.');
const data = await response.json();
if (data.success) {
const loginUrlInput = document.getElementById('edit-tab-login-url');
loginUrlInput.value = '';
// Update tabsData to reflect the cleared token
const tab = tabsData.find(t => t.id == tabId);
if (tab) {
tab.login_token = null;
}
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (error) {
console.error('Token Clearing Error:', error);
alert(`Error clearing token: ${error.message}`);
}
}
document.getElementById('generate-token-btn-modal').addEventListener('click', function() {
const tabId = document.getElementById('edit-tab-id').value;
if (tabId && confirm('<?php echo translate('tab_generate_token_confirm'); ?>')) {
generateToken(tabId);
}
});
document.getElementById('clear-token-btn-modal').addEventListener('click', function() {
const tabId = document.getElementById('edit-tab-id').value;
if (tabId && confirm('<?php echo translate('tab_clear_token_confirm'); ?>')) {
clearToken(tabId);
}
});
// Handle copying the login URL to clipboard
const loginUrlInput = document.getElementById('edit-tab-login-url');
const tooltip = new bootstrap.Tooltip(loginUrlInput, {
trigger: 'hover'
});
loginUrlInput.addEventListener('click', function() {
const url = this.value;
if (url) {
navigator.clipboard.writeText(url).then(() => {
// Success feedback
const originalTitle = this.getAttribute('data-bs-original-title');
this.setAttribute('data-bs-original-title', '<?php echo translate('tooltip_copied'); ?>');
tooltip.show();
// Reset tooltip title after a delay
setTimeout(() => {
this.setAttribute('data-bs-original-title', originalTitle);
tooltip.hide();
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
});
// Handle saving changes from the Edit Tab modal
saveTabChangesBtn.addEventListener('click', async function() {
const tabId = document.getElementById('edit-tab-id').value;
const name = document.getElementById('edit-tab-name').value.trim();
const ownerId = document.getElementById('edit-tab-owner').value;
const columnCount = document.getElementById('edit-tab-column-count').value;
// Get theme and background values from the other modal's inputs
const theme = document.getElementById('edit-tab-theme').value;
const transparency = document.getElementById('edit-tab-theme-transparency').value;
const backgroundUrl = document.getElementById('edit-tab-bg-url').value.trim();
const backgroundBrightness = document.getElementById('edit-tab-bg-brightness').value;
const backgroundSaturation = document.getElementById('edit-tab-bg-saturation').value;
if (!name) return;
try {
await fetch('api/tabs.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: tabId,
name: name,
user_id: ownerId,
column_count: columnCount,
theme: theme,
theme_transparency: transparency,
background_image_url: backgroundUrl,
background_brightness: backgroundBrightness,
background_saturation: backgroundSaturation
})
});
editTabModal.hide();
await initializeMatrix(); // Refresh matrix with new data
} catch (error) {
console.error('Error updating tab:', error);
alert('Error updating tab.');
}
});
// Handle Add Tab form submission
addTabForm.addEventListener('submit', async function(event) {
event.preventDefault();
const name = newTabNameInput.value.trim();
const userId = newTabUserInput.value;
if (!name) return;
try {
await fetch('api/tabs.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, user_id: userId })
});
newTabNameInput.value = '';
newTabUserInput.value = '';
await initializeMatrix(); // Re-render the whole matrix
} catch (error) {
console.error('Error adding tab:', error);
}
});
// --- Background Modal Logic ---
bgUploadInput.addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
const formData = new FormData();
formData.append('background', file);
// We need the tab ID to potentially name the file, let's get it from the main edit modal
const tabId = document.getElementById('edit-tab-id').value;
formData.append('tab_id', tabId);
uploadStatusDiv.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Loading...</span></div> Uploading...';
try {
const response = await fetch('api/upload_tab_background.php', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Unknown upload error' }));
throw new Error(errorData.message || 'Upload failed');
}
const data = await response.json();
if (data.success && data.path) {
bgUrlInputTab.value = data.path;
uploadStatusDiv.textContent = 'Upload successful!';
bgUploadInput.value = ''; // Clear the input
} else {
throw new Error(data.message || 'Upload failed to return a path.');
}
} catch (error) {
console.error('Upload error:', error);
uploadStatusDiv.textContent = `Error: ${error.message}`;
}
});
function updateSliderLabels() {
document.getElementById('brightness-value-tab').textContent = bgBrightnessInputTab.value;
document.getElementById('saturation-value-tab').textContent = bgSaturationInputTab.value;
transparencyValueSpan.textContent = transparencyInput.value;
}
function updateCurrentBgInfo() {
const path = bgUrlInputTab.value;
if (path && path.startsWith('data/images/backgrounds/')) {
currentBgInfoDiv.innerHTML = `
<small class="form-text text-muted d-block mt-2">
Current: <a href="${path}" target="_blank">${path.split('/').pop()}</a>
<button type="button" class="btn btn-sm btn-secondary ms-2" onclick="optimizeImage('${path}')">Optimize</button>
</small>`;
} else {
currentBgInfoDiv.innerHTML = '';
}
}
bgBrightnessInputTab.addEventListener('input', updateSliderLabels);
bgSaturationInputTab.addEventListener('input', updateSliderLabels);
transparencyInput.addEventListener('input', updateSliderLabels);
bgUrlInputTab.addEventListener('input', updateCurrentBgInfo);
removeTabBgBtn.addEventListener('click', function() {
bgUrlInputTab.value = '';
bgBrightnessInputTab.value = '100';
bgSaturationInputTab.value = '100';
updateSliderLabels();
updateCurrentBgInfo();
pexelsResultsContainerTab.innerHTML = '<div class="col-12"><div class="alert alert-info">Background removed.</div></div>';
});
const performPexelsSearch = async () => {
const query = pexelsQueryInputTab.value;
if (!query) {
alert('Please enter a search term.');
return;
}
pexelsResultsContainerTab.innerHTML = '<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>';
try {
const response = await fetch(`api/pexels.php?query=${encodeURIComponent(query)}`);
const data = await response.json();
pexelsResultsContainerTab.innerHTML = '';
if (data.error) throw new Error(data.error);
if (data.photos && data.photos.length > 0) {
data.photos.forEach(photo => {
const imgContainer = document.createElement('div');
imgContainer.className = 'col';
const img = document.createElement('img');
img.src = photo.src.medium;
img.className = 'img-thumbnail';
img.style.cursor = 'pointer';
img.alt = photo.alt;
img.addEventListener('click', () => {
bgUrlInputTab.value = photo.src.original;
updateCurrentBgInfo();
pexelsResultsContainerTab.innerHTML = '<div class="col-12"><div class="alert alert-success">Background selected! Go back to apply.</div></div>';
});
imgContainer.appendChild(img);
pexelsResultsContainerTab.appendChild(imgContainer);
});
} else {
pexelsResultsContainerTab.innerHTML = '<div class="col-12"><div class="alert alert-warning">No images found.</div></div>';
}
} catch (error) {
console.error('Pexels search error:', error);
pexelsResultsContainerTab.innerHTML = `<div class="col-12"><div class="alert alert-danger">${error.message}</div></div>`;
}
};
pexelsSearchBtnTab.addEventListener('click', performPexelsSearch);
pexelsQueryInputTab.addEventListener('keypress', (e) => {
if (e.key === 'Enter') performPexelsSearch();
});
document.getElementById('themeSettingsModal').addEventListener('hidden.bs.modal', function () {
// Clear search results when modal is closed to keep it clean
pexelsResultsContainerTab.innerHTML = '';
pexelsQueryInputTab.value = '';
uploadStatusDiv.textContent = '';
bgUploadInput.value = '';
});
function escapeHTML(str) {
return str ? str.replace(/[&<>"']/g, match => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[match])) : '';
}
// Initial load
initializeMatrix();
});
</script>
<?php require_once 'include/footer.php'; ?>