Autosave: 20260215-194817

This commit is contained in:
Flatlogic Bot 2026-02-15 19:48:17 +00:00
parent 98888d0370
commit e3984686cb
4 changed files with 359 additions and 49 deletions

View File

@ -121,6 +121,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = db()->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND ur.role_id = ? AND r.server_id = ?");
$stmt->execute([$target_user_id, $role_id, $server_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'reorder') {
$orders = $data['orders'] ?? [];
foreach ($orders as $order) {
$stmt = db()->prepare("UPDATE roles SET position = ? WHERE id = ? AND server_id = ?");
$stmt->execute([$order['position'], $order['id'], $server_id]);
}
echo json_encode(['success' => true]);
} elseif ($action === 'set_user_roles') {
$target_user_id = $data['user_id'] ?? 0;
$role_ids = $data['role_ids'] ?? [];
// Begin transaction
$db = db();
$db->beginTransaction();
try {
// Remove all existing roles for this user in this server
$stmt = $db->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND r.server_id = ?");
$stmt->execute([$target_user_id, $server_id]);
// Add new roles
if (!empty($role_ids)) {
$stmt = $db->prepare("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)");
foreach ($role_ids as $rid) {
// Verify role belongs to server
$check = $db->prepare("SELECT id FROM roles WHERE id = ? AND server_id = ?");
$check->execute([$rid, $server_id]);
if ($check->fetch()) {
$stmt->execute([$target_user_id, $rid]);
}
}
}
$db->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
exit;
}

View File

@ -230,10 +230,30 @@ body {
}
.sortable-ghost {
background-color: var(--hover) !important;
opacity: 0.5;
opacity: 0.4;
background-color: var(--blurple) !important;
}
.role-drag-handle:hover {
opacity: 1 !important;
color: var(--blurple);
}
.member-context-menu {
animation: menuFadeIn 0.1s ease-out;
}
@keyframes menuFadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.member-menu-action:hover {
background-color: var(--blurple) !important;
color: white !important;
}
.add-channel-btn {
cursor: pointer;
font-size: 1.2em;

View File

@ -628,17 +628,63 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
// Start DM
const dmBtn = e.target.closest('.start-dm-btn');
if (dmBtn) {
const userId = dmBtn.dataset.userId;
const formData = new FormData();
formData.append('user_id', userId);
const resp = await fetch('api_v1_dms.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
window.location.href = `?server_id=dms&channel_id=${result.channel_id}`;
}
// Member Menu
const memberItem = e.target.closest('.member-item');
if (memberItem) {
const userId = memberItem.dataset.userId;
const username = memberItem.dataset.username;
const avatar = memberItem.dataset.avatar;
// Create or show member menu
document.querySelector('.member-context-menu')?.remove();
const menu = document.createElement('div');
menu.className = 'member-context-menu bg-dark border border-secondary rounded p-2';
menu.style.position = 'fixed';
menu.style.zIndex = '1000';
menu.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)';
menu.style.minWidth = '150px';
const rect = memberItem.getBoundingClientRect();
menu.style.top = `${rect.top}px`;
menu.style.left = `${rect.left - 160}px`;
menu.innerHTML = `
<div class="mb-2 p-1 border-bottom border-secondary d-flex align-items-center">
<div class="message-avatar me-2" style="width: 24px; height: 24px; ${avatar ? `background-image: url('${avatar}');` : ''}"></div>
<span class="small fw-bold">${escapeHTML(username)}</span>
</div>
<button class="btn btn-sm btn-dark w-100 text-start mb-1 member-menu-action" data-action="message">Message</button>
${(window.isServerOwner || window.canManageServer) ? `<button class="btn btn-sm btn-dark w-100 text-start member-menu-action" data-action="edit-roles">Éditer son rôle</button>` : ''}
`;
document.body.appendChild(menu);
// Close menu on click outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('mousedown', closeMenu);
}
};
document.addEventListener('mousedown', closeMenu);
menu.querySelectorAll('.member-menu-action').forEach(btn => {
btn.onclick = async () => {
const action = btn.dataset.action;
if (action === 'message') {
const formData = new FormData();
formData.append('user_id', userId);
const resp = await fetch('api_v1_dms.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
window.location.href = `?server_id=dms&channel_id=${result.channel_id}`;
}
} else if (action === 'edit-roles') {
openEditUserRolesModal(userId, username, avatar);
}
menu.remove();
};
});
}
});
@ -1010,10 +1056,88 @@ document.addEventListener('DOMContentLoaded', () => {
serverPermissions = data.permissions_list;
if (rolesList) renderRoles(data.roles);
if (membersList) renderMembers(data.members);
updateGlobalUI(data.members);
}
} catch (e) { console.error(e); }
}
function renderRoleIconJS(icon, size = '12px') {
if (!icon) return '';
const isUrl = icon.startsWith('http') || icon.startsWith('/');
if (isUrl) {
return `<img src="${escapeHTML(icon)}" class="role-icon ms-1" style="width: ${size}; height: ${size}; vertical-align: middle; object-fit: contain;">`;
} else {
return `<span class="ms-1" style="font-size: ${size}; vertical-align: middle;">${escapeHTML(icon)}</span>`;
}
}
function updateGlobalUI(members) {
// Update members sidebar
const sidebar = document.querySelector('.members-sidebar');
if (sidebar) {
const countEl = sidebar.querySelector('div[style*="text-transform: uppercase"]');
if (countEl) countEl.textContent = `Members — ${members.length}`;
// We need to keep the "Members - X" div and replace everything else
const header = sidebar.firstElementChild;
sidebar.innerHTML = '';
sidebar.appendChild(header);
members.forEach(m => {
const item = document.createElement('div');
item.className = 'channel-item member-item';
item.dataset.userId = m.id;
item.dataset.username = m.username;
item.dataset.avatar = m.avatar_url || '';
item.style.color = 'var(--text-primary)';
item.style.marginBottom = '8px';
item.style.cursor = 'pointer';
const roleIconHtml = renderRoleIconJS(m.role_icon, '12px');
const avatarBg = m.avatar_url ? `background-image: url('${m.avatar_url}');` : '';
const statusColor = m.status === 'online' ? '#23a559' : '#80848e';
item.innerHTML = `
<div class="message-avatar" style="width: 32px; height: 32px; background-color: ${statusColor}; position: relative; ${avatarBg}">
${m.status === 'online' ? `<div style="position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; background-color: #23a559; border-radius: 50%; border: 2px solid var(--bg-members);"></div>` : ''}
</div>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; ${m.role_color ? `color: ${m.role_color};` : ''}">
${escapeHTML(m.username)}
${roleIconHtml}
</span>
`;
sidebar.appendChild(item);
});
}
// Update chat colors
document.querySelectorAll('.message-author').forEach(authorEl => {
const username = authorEl.childNodes[0].textContent.trim();
const member = members.find(m => m.username === username);
if (member) {
authorEl.style.color = member.role_color || 'inherit';
// Try to update icon if it exists or add it if it doesn't
let iconEl = authorEl.querySelector('.role-icon, span.ms-1');
const newIconHtml = renderRoleIconJS(member.role_icon, '12px');
if (newIconHtml) {
if (iconEl) {
const temp = document.createElement('div');
temp.innerHTML = newIconHtml;
iconEl.replaceWith(temp.firstChild);
} else {
const temp = document.createElement('div');
temp.innerHTML = newIconHtml;
// Insert after the text node
authorEl.insertBefore(temp.firstChild, authorEl.childNodes[1]);
}
} else if (iconEl) {
iconEl.remove();
}
}
});
}
function renderRoles(roles) {
rolesList.innerHTML = '';
if (roles.length === 0) {
@ -1021,10 +1145,14 @@ document.addEventListener('DOMContentLoaded', () => {
}
roles.forEach(role => {
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded';
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded role-sortable-item';
item.dataset.id = role.id;
const isUrl = role.icon_url && (role.icon_url.startsWith('http') || role.icon_url.startsWith('/'));
item.innerHTML = `
<div class="d-flex align-items-center">
<div class="role-drag-handle me-3" style="cursor: grab; opacity: 0.5;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="5" x2="8" y2="5.01"></line><line x1="16" y1="5" x2="16" y2="5.01"></line><line x1="8" y1="12" x2="8" y2="12.01"></line><line x1="16" y1="12" x2="16" y2="12.01"></line><line x1="8" y1="19" x2="8" y2="19.01"></line><line x1="16" y1="19" x2="16" y2="19.01"></line></svg>
</div>
<div style="width: 14px; height: 14px; border-radius: 50%; background-color: ${role.color}; margin-right: 12px; box-shadow: 0 0 5px ${role.color}88;"></div>
<span class="fw-medium">${role.name}</span>
${role.icon_url ? (isUrl ? `<img src="${role.icon_url}" class="ms-1" style="width: 12px; height: 12px; object-fit: contain;">` : `<span class="ms-1" style="font-size: 12px;">${role.icon_url}</span>`) : ''}
@ -1036,28 +1164,49 @@ document.addEventListener('DOMContentLoaded', () => {
`;
rolesList.appendChild(item);
});
// Initialize Sortable for roles
if (typeof Sortable !== 'undefined' && rolesList) {
new Sortable(rolesList, {
animation: 150,
handle: '.role-drag-handle',
ghostClass: 'sortable-ghost',
onEnd: () => saveRolePositions()
});
}
}
async function saveRolePositions() {
const orders = [];
const items = rolesList.querySelectorAll('.role-sortable-item');
// Invert the order because we ORDER BY position DESC in SQL
let position = items.length - 1;
items.forEach(item => {
orders.push({
id: item.dataset.id,
position: position--
});
});
try {
await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'reorder',
server_id: activeServerId,
orders: orders
})
});
} catch (e) { console.error(e); }
}
function renderMembers(members) {
membersList.innerHTML = '';
members.forEach(member => {
const memberRoles = member.role_ids ? member.role_ids.split(',') : [];
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-2 rounded bg-dark';
let rolesHtml = '';
serverRoles.forEach(role => {
const isAssigned = memberRoles.includes(role.id.toString());
rolesHtml += `
<div class="form-check form-check-inline">
<input class="form-check-input role-assign-check" type="checkbox"
data-user-id="${member.id}" data-role-id="${role.id}"
${isAssigned ? 'checked' : ''}>
<label class="form-check-label small" style="color: ${role.color}">${role.name}</label>
</div>
`;
});
const isIconUrl = member.role_icon && (member.role_icon.startsWith('http') || member.role_icon.startsWith('/'));
const roleIconHtml = member.role_icon ? (isIconUrl ? `<img src="${member.role_icon}" class="role-icon ms-1" style="width: 12px; height: 12px; vertical-align: middle; object-fit: contain;">` : `<span class="ms-1" style="font-size: 12px; vertical-align: middle;">${member.role_icon}</span>`) : '';
@ -1069,16 +1218,28 @@ document.addEventListener('DOMContentLoaded', () => {
${escapeHTML(member.username)}
${roleIconHtml}
</div>
<div class="member-roles-assign-list">
${rolesHtml || '<span class="text-muted small">No roles available</span>'}
<div class="text-muted small">
${member.role_names ? member.role_names.split(',').join(', ') : 'No roles'}
</div>
</div>
</div>
${(window.isServerOwner || window.canManageServer) ? `
<button class="btn btn-sm btn-outline-light edit-user-roles-settings-btn" data-id="${member.id}" data-username="${member.username}" data-avatar="${member.avatar_url || ''}">Roles</button>
` : ''}
`;
membersList.appendChild(item);
});
}
// Add listener for the button in members list tab
membersList?.addEventListener('click', (e) => {
const btn = e.target.closest('.edit-user-roles-settings-btn');
if (btn) {
openEditUserRolesModal(btn.dataset.id, btn.dataset.username, btn.dataset.avatar);
}
});
// Role Editing Modal Logic
rolesList?.addEventListener('click', (e) => {
if (e.target.classList.contains('edit-role-btn-v2')) {
@ -1108,7 +1269,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
document.getElementById('save-role-btn')?.addEventListener('click', async () => {
document.getElementById('save-role-btn')?.addEventListener('click', async (e) => {
const btn = e.target;
const originalText = btn.textContent;
const id = document.getElementById('edit-role-id').value;
const name = document.getElementById('edit-role-name').value;
const color = document.getElementById('edit-role-color').value;
@ -1127,31 +1290,92 @@ document.addEventListener('DOMContentLoaded', () => {
});
const data = await resp.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('roleEditorModal')).hide();
btn.textContent = 'Saved ✅';
btn.classList.replace('btn-primary', 'btn-success');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.replace('btn-success', 'btn-primary');
}, 2000);
loadRoles();
}
} catch (e) { console.error(e); }
});
membersList?.addEventListener('change', async (e) => {
if (e.target.classList.contains('role-assign-check')) {
const userId = e.target.dataset.userId;
const roleId = e.target.dataset.roleId;
const action = e.target.checked ? 'assign' : 'unassign';
async function openEditUserRolesModal(userId, username, avatar) {
const modal = document.getElementById('editUserRolesModal');
document.getElementById('edit-user-roles-user-id').value = userId;
document.getElementById('edit-user-roles-username').textContent = username;
const avatarEl = document.getElementById('edit-user-roles-avatar');
avatarEl.style.backgroundImage = avatar ? `url('${avatar}')` : 'none';
const list = document.getElementById('user-roles-selection-list');
list.innerHTML = '<div class="text-center p-3 text-muted">Loading roles...</div>';
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
try {
// We need to fetch roles and the current user's roles
// We can reuse loadRoles or make a specific call
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
const data = await resp.json();
try {
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, server_id: activeServerId, user_id: userId, role_id: roleId })
if (data.success) {
const member = data.members.find(m => m.id == userId);
const assignedRoles = member && member.role_ids ? member.role_ids.split(',') : [];
list.innerHTML = '';
// Sort roles by position descending for display
data.roles.sort((a, b) => b.position - a.position).forEach(role => {
const isChecked = assignedRoles.includes(role.id.toString());
const item = document.createElement('div');
item.className = 'list-group-item bg-dark text-white border-secondary p-2 d-flex align-items-center';
item.innerHTML = `
<input class="form-check-input me-3 user-role-checkbox" type="checkbox" value="${role.id}" id="user-role-${role.id}" ${isChecked ? 'checked' : ''}>
<label class="form-check-label flex-grow-1" for="user-role-${role.id}" style="color: ${role.color}; cursor: pointer;">
${role.name}
</label>
`;
list.appendChild(item);
});
const data = await resp.json();
if (!data.success) {
alert(data.error || 'Failed to update role');
e.target.checked = !e.target.checked;
if (data.roles.length === 0) {
list.innerHTML = '<div class="text-center p-3 text-muted">No roles defined for this server.</div>';
}
} catch (e) { console.error(e); }
}
}
} catch (e) { console.error(e); }
}
document.getElementById('save-user-roles-btn')?.addEventListener('click', async (e) => {
const btn = e.target;
const originalText = btn.textContent;
const userId = document.getElementById('edit-user-roles-user-id').value;
const roleIds = Array.from(document.querySelectorAll('.user-role-checkbox:checked')).map(cb => cb.value);
try {
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'set_user_roles',
server_id: activeServerId,
user_id: userId,
role_ids: roleIds
})
});
const data = await resp.json();
if (data.success) {
btn.textContent = 'Saved ✅';
btn.classList.replace('btn-primary', 'btn-success');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.replace('btn-success', 'btn-primary');
}, 2000);
loadRoles();
} else {
alert(data.error || 'Failed to update roles');
}
} catch (e) { console.error(e); }
});
addRoleBtn?.addEventListener('click', async () => {

View File

@ -776,7 +776,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
Members <?php echo count($members); ?>
</div>
<?php foreach($members as $m): ?>
<div class="channel-item start-dm-btn" data-user-id="<?php echo $m['id']; ?>" style="color: var(--text-primary); margin-bottom: 8px;">
<div class="channel-item member-item" data-user-id="<?php echo $m['id']; ?>" data-username="<?php echo htmlspecialchars($m['username']); ?>" data-avatar="<?php echo htmlspecialchars($m['avatar_url'] ?? ''); ?>" style="color: var(--text-primary); margin-bottom: 8px; cursor: pointer;">
<div class="message-avatar" style="width: 32px; height: 32px; background-color: <?php echo $m['status'] == 'online' ? '#23a559' : '#80848e'; ?>; position: relative; <?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>">
<?php if($m['status'] == 'online'): ?>
<div style="position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; background-color: #23a559; border-radius: 50%; border: 2px solid var(--bg-members);"></div>
@ -1379,6 +1379,35 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- Edit User Roles Modal -->
<div class="modal fade" id="editUserRolesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Member Roles</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center mb-4">
<div id="edit-user-roles-avatar" class="message-avatar me-3" style="width: 48px; height: 48px;"></div>
<div>
<h5 id="edit-user-roles-username" class="mb-0">Username</h5>
<div class="text-muted small">Select roles to assign to this member</div>
</div>
</div>
<input type="hidden" id="edit-user-roles-user-id">
<div id="user-roles-selection-list" class="list-group list-group-flush bg-dark rounded">
<!-- Roles checkboxes populated by JS -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="save-user-roles-btn" class="btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;