718 lines
33 KiB
PHP
718 lines
33 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="row">
|
|
<div class="col-md-6 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="col-md-6 mb-3">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="edit-tab-show-search-bar" name="edit-tab-show-search-bar">
|
|
<label class="form-check-label" for="edit-tab-show-search-bar"><?php echo translate('tab_show_search_bar'); ?></label>
|
|
</div>
|
|
<div class="form-text"><?php echo translate('tab_show_search_bar_help'); ?></div>
|
|
</div>
|
|
</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';
|
|
document.getElementById('edit-tab-show-search-bar').checked = (tabData.show_search_bar ?? 1) == 1;
|
|
|
|
// 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;
|
|
const showSearchBar = document.getElementById('edit-tab-show-search-bar').checked ? 1 : 0;
|
|
|
|
// 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,
|
|
show_search_bar: showSearchBar,
|
|
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, show_search_bar: 1 })
|
|
});
|
|
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 => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[match])) : '';
|
|
}
|
|
|
|
// Initial load
|
|
initializeMatrix();
|
|
});
|
|
</script>
|
|
|
|
<?php require_once 'include/footer.php'; ?>
|