This commit is contained in:
Flatlogic Bot 2026-02-15 16:11:50 +00:00
parent 40f605d106
commit 5d6fd46690
7 changed files with 458 additions and 90 deletions

View File

@ -17,6 +17,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Handle JSON input
$json = json_decode(file_get_contents('php://input'), true);
if ($json) {
$action = $json['action'] ?? '';
if ($action === 'reorder') {
$server_id = $json['server_id'] ?? 0;
$orders = $json['orders'] ?? []; // Array of {id, position, category_id}
$user_id = $_SESSION['user_id'];
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("UPDATE channels SET position = ?, category_id = ? WHERE id = ? AND server_id = ?");
foreach ($orders as $o) {
$stmt->execute([$o['position'], $o['category_id'] ?: null, $o['id'], $server_id]);
}
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
}
exit;
}
}
$action = $_POST['action'] ?? 'create';
$server_id = $_POST['server_id'] ?? 0;
$user_id = $_SESSION['user_id'];
@ -30,6 +52,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$theme_color = $_POST['theme_color'] ?? null;
if ($theme_color === '') $theme_color = null;
$icon = $_POST['icon'] ?? null;
if ($icon === '') $icon = null;
$category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
// Check if user has permission to manage channels
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
@ -38,8 +63,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($chan && Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
$stmt = db()->prepare("UPDATE channels SET name = ?, type = ?, status = ?, allow_file_sharing = ?, theme_color = ?, message_limit = ? WHERE id = ?");
$stmt->execute([$name, $type, $status, $allow_file_sharing, $theme_color, $message_limit, $channel_id]);
$stmt = db()->prepare("UPDATE channels SET name = ?, type = ?, status = ?, allow_file_sharing = ?, theme_color = ?, message_limit = ?, icon = ?, category_id = ? WHERE id = ?");
$stmt->execute([$name, $type, $status, $allow_file_sharing, $theme_color, $message_limit, $icon, $category_id, $channel_id]);
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
@ -72,9 +97,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$theme_color = $_POST['theme_color'] ?? null;
if ($theme_color === '') $theme_color = null;
$icon = $_POST['icon'] ?? null;
if ($icon === '') $icon = null;
$category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing, theme_color, message_limit) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$server_id, $name, $type, $allow_file_sharing, $theme_color, $message_limit]);
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing, theme_color, message_limit, icon, category_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$server_id, $name, $type, $allow_file_sharing, $theme_color, $message_limit, $icon, $category_id]);
$channel_id = db()->lastInsertId();
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);

View File

@ -178,17 +178,6 @@ body {
color: var(--text-primary);
}
.channel-item::before {
content: "#";
font-size: 1.2em;
font-weight: 300;
}
.voice-item::before {
content: "🔊";
font-size: 0.9em;
}
.server-icon.add-btn {
color: #23a559;
}
@ -203,13 +192,48 @@ body {
font-size: 0.75em;
text-transform: uppercase;
font-weight: bold;
margin-bottom: 8px;
padding-left: 8px;
margin-bottom: 4px;
padding: 16px 8px 4px 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.channel-category .channel-settings-btn,
.channel-category .add-channel-btn {
opacity: 0;
transition: opacity 0.2s;
font-size: 1.2em;
cursor: pointer;
}
.channel-category:hover .channel-settings-btn,
.channel-category:hover .add-channel-btn {
opacity: 1;
}
.channel-item-container .channel-settings-btn {
opacity: 0;
transition: opacity 0.2s;
}
.channel-item-container:hover .channel-settings-btn {
opacity: 1;
}
.category-group .channel-item-container {
margin-left: 8px;
}
.category-group {
min-height: 5px;
}
.sortable-ghost {
background-color: var(--hover) !important;
opacity: 0.5;
}
.add-channel-btn {
cursor: pointer;
font-size: 1.2em;
@ -400,7 +424,27 @@ body {
width: 240px;
background-color: var(--bg-members);
padding: 24px 8px;
display: none; /* Hidden on mobile/small screens */
display: flex;
flex-direction: column;
}
.members-sidebar.hidden {
display: none;
}
@media (max-width: 992px) {
.members-sidebar {
display: none;
}
.members-sidebar.show {
display: flex;
position: absolute;
right: 0;
top: 48px;
bottom: 0;
z-index: 100;
box-shadow: -2px 0 10px rgba(0,0,0,0.5);
}
}
/* Reactions */

View File

@ -53,18 +53,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Notification(`Mention in #${window.currentChannelName}`, {
body: `${data.username}: ${data.content}`,
icon: data.avatar_url || ''
if (e.target.classList.contains('move-rule-btn')) {
const id = e.target.dataset.id;
const dir = e.target.dataset.dir;
const resp = await fetch('api_v1_rules.php', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, dir })
});
if ((await resp.json()).success) location.reload();
}
});
});
}
}
}
@ -594,10 +583,21 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
document.addEventListener('click', (e) => {
document.addEventListener('click', async (e) => {
if (!e.target.closest('.search-container')) {
searchResults.style.display = 'none';
}
if (e.target.classList.contains('move-rule-btn')) {
const id = e.target.dataset.id;
const dir = e.target.dataset.dir;
const resp = await fetch('api_v1_rules.php', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, dir })
});
if ((await resp.json()).success) location.reload();
}
});
// Roles Management
@ -615,6 +615,7 @@ document.addEventListener('DOMContentLoaded', () => {
modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || '';
modal.querySelector('#edit-channel-status').value = btn.dataset.status || '';
modal.querySelector('#edit-channel-theme').value = btn.dataset.theme || '#5865f2';
modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || '';
modal.querySelector('#delete-channel-id').value = channelId;
// Show/Hide RSS tab
@ -1348,6 +1349,19 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e) { console.error(e); }
});
// Toggle members sidebar
const toggleMembersBtn = document.getElementById('toggle-members-btn');
const membersSidebar = document.querySelector('.members-sidebar');
if (toggleMembersBtn && membersSidebar) {
toggleMembersBtn.addEventListener('click', () => {
if (window.innerWidth > 992) {
membersSidebar.classList.toggle('hidden');
} else {
membersSidebar.classList.toggle('show');
}
});
}
// User Settings - Save
const saveSettingsBtn = document.getElementById('save-settings-btn');
saveSettingsBtn?.addEventListener('click', async () => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

View File

@ -9,6 +9,11 @@ class Permissions {
const ADMINISTRATOR = 32;
public static function hasPermission($user_id, $server_id, $permission) {
$stmt = db()->prepare("SELECT is_admin FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
if ($user && $user['is_admin']) return true;
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();

395
index.php
View File

@ -63,7 +63,7 @@ if ($is_dm_view) {
$active_server_id = $_GET['server_id'] ?? ($servers[0]['id'] ?? 1);
// Fetch channels
$stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ?");
$stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ? ORDER BY position ASC, id ASC");
$stmt->execute([$active_server_id]);
$channels = $stmt->fetchAll();
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 1);
@ -192,8 +192,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/discord.css?v=<?php echo time(); ?>">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;
window.activeServerId = "<?php echo $active_server_id; ?>";
@ -266,7 +268,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
}
?>
</div>
<div class="channels-list">
<div class="channels-list" id="sidebar-channels-list">
<?php if ($is_dm_view): ?>
<?php foreach($dm_channels as $dm): ?>
<a href="?server_id=dms&channel_id=<?php echo $dm['id']; ?>"
@ -278,25 +280,37 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</a>
<?php endforeach; ?>
<?php else: ?>
<div class="channel-category">
<span>Text Channels</span>
<?php if ($can_manage_channels): ?>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="chat">+</span>
<?php endif; ?>
</div>
<?php foreach($channels as $c): if($c['type'] === 'voice') continue; ?>
<div class="channel-item-container d-flex align-items-center">
<?php
// Separate categories and channels
$categories = array_filter($channels, function($c) { return $c['type'] === 'category'; });
$top_level_channels = array_filter($channels, function($c) {
return $c['type'] !== 'category' && (empty($c['category_id']) || !in_array($c['category_id'], array_column($GLOBALS['channels'] ?? [], 'id')));
});
// Helper to render a channel item
function renderChannelItem($c, $active_channel_id, $active_server_id, $can_manage_channels) {
?>
<div class="channel-item-container d-flex align-items-center" data-id="<?php echo $c['id']; ?>">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
class="channel-item flex-grow-1 <?php echo $c['id'] == $active_channel_id ? 'active' : ''; ?>">
<span class="me-1">
<?php
if ($c['type'] === 'announcement') echo '📢';
elseif ($c['type'] === 'rules') echo '📜';
elseif ($c['type'] === 'forum') echo '🏛️';
else echo '#';
?>
class="channel-item flex-grow-1 <?php echo ($c['id'] == $active_channel_id) ? 'active' : ''; ?> <?php echo ($c['type'] === 'voice') ? 'voice-item' : ''; ?>" <?php echo ($c['type'] === 'voice') ? 'data-channel-id="'.$c['id'].'"' : ''; ?>>
<span class="d-flex align-items-center">
<span class="me-1" style="width: 20px; display: inline-block; text-align: center;">
<?php
if ($c['type'] === 'announcement') echo '<i class="fa-solid fa-bullhorn"></i>';
elseif ($c['type'] === 'rules') echo '<i class="fa-solid fa-gavel"></i>';
elseif ($c['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($c['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
?>
</span>
<?php if (!empty($c['icon'])): ?>
<span class="me-1 custom-channel-icon"><i class="fa-solid <?php echo htmlspecialchars($c['icon']); ?>"></i></span>
<?php endif; ?>
<span class="channel-name-text"><?php echo htmlspecialchars($c['name']); ?></span>
</span>
<?php echo htmlspecialchars($c['name']); ?>
<?php if ($c['type'] === 'voice' && !empty($c['status'])): ?>
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
<?php endif; ?>
</a>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
@ -307,42 +321,59 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
data-files="<?php echo $c['allow_file_sharing']; ?>"
data-limit="<?php echo $c['message_limit']; ?>"
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
data-icon="<?php echo htmlspecialchars($c['icon'] ?? ''); ?>"
data-category="<?php echo $c['category_id'] ?? ''; ?>"
data-theme="<?php echo $c['theme_color']; ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php
}
<div class="channel-category" style="margin-top: 16px;">
<span>Voice Channels</span>
<?php if ($can_manage_channels): ?>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="voice">+</span>
<?php endif; ?>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'voice') continue; ?>
<div class="channel-item-container d-flex align-items-center">
<div class="channel-item voice-item flex-grow-1" data-channel-id="<?php echo $c['id']; ?>">
<span>🔊 <?php echo htmlspecialchars($c['name']); ?></span>
<?php if (!empty($c['status'])): ?>
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
// Render top level channels
if (!empty($top_level_channels)) {
foreach($top_level_channels as $c) {
renderChannelItem($c, $active_channel_id, $active_server_id, $can_manage_channels);
}
}
// Render categories and their channels
foreach($categories as $cat) {
?>
<div class="category-wrapper" data-id="<?php echo $cat['id']; ?>">
<div class="channel-category d-flex align-items-center mt-3" data-id="<?php echo $cat['id']; ?>">
<span class="category-name flex-grow-1 text-uppercase fw-bold" style="font-size: 0.7em; cursor: pointer; color: var(--text-muted);"><?php echo htmlspecialchars($cat['name']); ?></span>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $cat['id']; ?>"
data-name="<?php echo htmlspecialchars($cat['name']); ?>"
data-type="category"
data-files="0"
data-limit="0"
data-status=""
data-icon=""
data-category=""
data-theme="">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
<span class="add-channel-btn ms-1" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="chat" data-category-id="<?php echo $cat['id']; ?>">+</span>
<?php endif; ?>
</div>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="<?php echo htmlspecialchars($c['name']); ?>"
data-type="<?php echo $c['type']; ?>"
data-files="<?php echo $c['allow_file_sharing']; ?>"
data-limit="<?php echo $c['message_limit']; ?>"
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
data-theme="<?php echo $c['theme_color']; ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
<?php endif; ?>
<div class="category-group" data-category-id="<?php echo $cat['id']; ?>">
<?php
foreach($channels as $c) {
if ($c['type'] !== 'category' && $c['category_id'] == $cat['id']) {
renderChannelItem($c, $active_channel_id, $active_server_id, $can_manage_channels);
}
}
?>
</div>
</div>
<?php endforeach; ?>
<?php
}
?>
<?php endif; ?>
</div>
<div class="user-panel">
@ -370,10 +401,28 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Chat Area -->
<div class="chat-container">
<div class="chat-header">
<span style="color: var(--text-muted); margin-right: 8px;"><?php echo $is_dm_view ? '@' : '#'; ?></span>
<span style="color: var(--text-muted); margin-right: 8px; width: 20px; display: inline-block; text-align: center;">
<?php
if ($is_dm_view) {
echo '@';
} else {
if ($active_channel['type'] === 'announcement') echo '<i class="fa-solid fa-bullhorn"></i>';
elseif ($active_channel['type'] === 'rules') echo '<i class="fa-solid fa-gavel"></i>';
elseif ($active_channel['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($active_channel['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
}
?>
</span>
<?php if (!$is_dm_view && !empty($active_channel['icon'])): ?>
<span class="me-2 custom-channel-icon"><i class="fa-solid <?php echo htmlspecialchars($active_channel['icon']); ?>"></i></span>
<?php endif; ?>
<span class="flex-grow-1"><?php echo htmlspecialchars($current_channel_name); ?></span>
<div class="d-flex align-items-center">
<button id="toggle-members-btn" class="btn btn-link text-muted p-1 me-2" title="Toggle Members List">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</button>
<button id="pinned-messages-btn" class="btn btn-link text-muted p-1 me-2" title="Pinned Messages">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</button>
@ -674,6 +723,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<input type="text" id="chat-input" class="chat-input" placeholder="Message #<?php echo htmlspecialchars($current_channel_name); ?>" autocomplete="off">
</div>
</form>
<?php endif; ?>
</div>
</div>
@ -753,7 +803,6 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
</form>
<?php endif; ?>
</div>
<div class="modal-footer">
@ -947,25 +996,48 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
<form action="api_v1_channels.php" method="POST">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="type" id="channel-type-input" value="chat">
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Type</label>
<select name="type" class="form-select bg-dark text-white border-secondary mb-3" id="channel-type-select">
<select name="type" class="form-select bg-dark text-white border-secondary mb-3" id="add-channel-type">
<option value="chat">Traditional Chat</option>
<option value="announcement">Announcements</option>
<option value="rules">Rules</option>
<option value="forum">Forum</option>
<option value="voice">Voice Channel</option>
<option value="category">Category (Separator)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
<div class="input-group">
<span class="input-group-text bg-dark border-0 text-muted" id="channel-hash-prefix">#</span>
<span class="input-group-text bg-dark border-0 text-muted" id="add-channel-prefix">#</span>
<input type="text" name="name" class="form-control" placeholder="new-channel" required>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Icon</label>
<select name="icon" class="form-select bg-dark text-white border-secondary mb-3">
<option value="">Aucune icône personnalisée</option>
<option value="fa-hashtag"># Hashtag</option>
<option value="fa-volume-up">🔊 Voice</option>
<option value="fa-bullhorn">📢 Announcements</option>
<option value="fa-gavel">🔨 Rules</option>
<option value="fa-comments">💬 Forum</option>
<option value="fa-lock">🔒 Private</option>
<option value="fa-star"> Star</option>
<option value="fa-heart">❤️ Heart</option>
<option value="fa-gamepad">🎮 Gaming</option>
<option value="fa-music">🎵 Music</option>
<option value="fa-video">📹 Video</option>
<option value="fa-info-circle"> Info</option>
<option value="fa-question-circle"> Help</option>
<option value="fa-book">📖 Library</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-code">💻 Coding</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="add-channel-files" value="1" checked>
<label class="form-check-label text-white" for="add-channel-files">Allow File Sharing</label>
@ -975,10 +1047,6 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<input type="number" name="message_limit" class="form-control" placeholder="e.g. 50 (Leave empty for no limit)">
<div class="form-text text-muted" style="font-size: 0.8em;">Automatically keeps only the last X messages in this channel.</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Theme Color</label>
<input type="color" name="theme_color" class="form-control form-control-color w-100" value="#5865f2" title="Choose channel theme color">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Cancel</button>
@ -1024,12 +1092,54 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<option value="rules">Rules</option>
<option value="forum">Forum</option>
<option value="voice">Voice Channel</option>
<option value="category">Category (Separator)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Parent Category</label>
<select name="category_id" id="edit-channel-category-id" class="form-select bg-dark text-white border-secondary">
<option value="">No Category (Top level)</option>
<?php
foreach($channels as $cat) {
if ($cat['type'] === 'category') {
echo '<option value="'.$cat['id'].'">'.htmlspecialchars($cat['name']).'</option>';
}
}
?>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
<input type="text" name="name" id="edit-channel-name" class="form-control" required>
<div class="input-group">
<span class="input-group-text bg-dark border-0 text-muted" id="edit-channel-prefix">#</span>
<input type="text" name="name" id="edit-channel-name" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Icon</label>
<select name="icon" id="edit-channel-icon" class="form-select bg-dark text-white border-secondary mb-3">
<option value="">Aucune icône personnalisée</option>
<option value="fa-hashtag"># Hashtag</option>
<option value="fa-volume-up">🔊 Voice</option>
<option value="fa-bullhorn">📢 Announcements</option>
<option value="fa-gavel">🔨 Rules</option>
<option value="fa-comments">💬 Forum</option>
<option value="fa-lock">🔒 Private</option>
<option value="fa-star"> Star</option>
<option value="fa-heart">❤️ Heart</option>
<option value="fa-gamepad">🎮 Gaming</option>
<option value="fa-music">🎵 Music</option>
<option value="fa-video">📹 Video</option>
<option value="fa-info-circle"> Info</option>
<option value="fa-question-circle"> Help</option>
<option value="fa-book">📖 Library</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-code">💻 Coding</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
</div>
<div class="mb-3" id="edit-channel-status-container" style="display: none;">
@ -1219,14 +1329,181 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>
// Handle channel type in modal
// Handle channel type and icon in modals
const addChannelType = document.getElementById('add-channel-type');
const addChannelPrefix = document.getElementById('add-channel-prefix');
const addChannelIcon = document.querySelector('#addChannelModal select[name="icon"]');
const editChannelType = document.getElementById('edit-channel-type');
const editChannelPrefix = document.getElementById('edit-channel-prefix');
const editChannelIcon = document.getElementById('edit-channel-icon');
function getPrefixForType(type) {
if (type === 'voice') return '<i class="fa-solid fa-volume-up"></i>';
if (type === 'announcement') return '<i class="fa-solid fa-bullhorn"></i>';
if (type === 'rules') return '<i class="fa-solid fa-gavel"></i>';
if (type === 'forum') return '<i class="fa-solid fa-comments"></i>';
return '#';
}
function updatePrefix(typeSelect, iconSelect, prefixSpan) {
if (!prefixSpan || !typeSelect) return;
let prefix = getPrefixForType(typeSelect.value);
if (iconSelect && iconSelect.value) {
prefix += ` <i class="fa-solid ${iconSelect.value}"></i>`;
}
prefixSpan.innerHTML = prefix;
}
document.querySelectorAll('.add-channel-btn').forEach(btn => {
btn.addEventListener('click', function() {
const type = this.getAttribute('data-type');
document.getElementById('channel-type-input').value = type;
document.getElementById('channel-hash-prefix').textContent = type === 'text' ? '#' : '🔊';
const categoryId = this.getAttribute('data-category-id');
if (addChannelType) {
addChannelType.value = type;
updatePrefix(addChannelType, addChannelIcon, addChannelPrefix);
}
// Add category_id hidden field or select it if we add it to addChannelModal
// Let's add a hidden field to addChannelModal for category_id
let catInput = document.getElementById('add-channel-category-id');
if (!catInput) {
catInput = document.createElement('input');
catInput.type = 'hidden';
catInput.name = 'category_id';
catInput.id = 'add-channel-category-id';
document.querySelector('#addChannelModal form').appendChild(catInput);
}
catInput.value = categoryId || '';
});
});
if (addChannelType) {
addChannelType.addEventListener('change', () => updatePrefix(addChannelType, addChannelIcon, addChannelPrefix));
}
if (addChannelIcon) {
addChannelIcon.addEventListener('change', () => updatePrefix(addChannelType, addChannelIcon, addChannelPrefix));
}
if (editChannelType) {
editChannelType.addEventListener('change', () => updatePrefix(editChannelType, editChannelIcon, editChannelPrefix));
}
if (editChannelIcon) {
editChannelIcon.addEventListener('change', () => updatePrefix(editChannelType, editChannelIcon, editChannelPrefix));
}
// Initial update when opening edit modal
document.addEventListener('click', function(e) {
const btn = e.target.closest('.channel-settings-btn');
if (btn) {
// Fill basic fields to ensure they are present even if main.js fails
const modal = document.getElementById('editChannelModal');
if (modal) {
const idInput = document.getElementById('edit-channel-id');
const nameInput = document.getElementById('edit-channel-name');
const typeSelect = document.getElementById('edit-channel-type');
const iconSelect = document.getElementById('edit-channel-icon');
const categorySelect = document.getElementById('edit-channel-category-id');
if (idInput) idInput.value = btn.dataset.id || '';
if (nameInput) nameInput.value = btn.dataset.name || '';
if (typeSelect) typeSelect.value = btn.dataset.type || 'chat';
if (iconSelect) iconSelect.value = btn.dataset.icon || '';
if (categorySelect) categorySelect.value = btn.dataset.category || '';
// Also fill delete ID
const deleteIdInput = document.getElementById('delete-channel-id');
if (deleteIdInput) deleteIdInput.value = btn.dataset.id || '';
}
setTimeout(() => updatePrefix(editChannelType, editChannelIcon, editChannelPrefix), 100);
}
});
// SortableJS Implementation for Channels
<?php if ($can_manage_channels): ?>
document.addEventListener('DOMContentLoaded', function() {
// Sortable for groups (channels inside categories)
const groups = document.querySelectorAll('.category-group');
groups.forEach(group => {
new Sortable(group, {
group: 'channels',
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: function() {
saveChannelOrders();
}
});
});
// Sortable for categories themselves and top-level channels
const sidebar = document.getElementById('sidebar-channels-list');
new Sortable(sidebar, {
animation: 150,
draggable: '.category-wrapper, .channel-item-container:not(.category-group .channel-item-container)',
ghostClass: 'sortable-ghost',
onEnd: function() {
saveChannelOrders();
}
});
});
async function saveChannelOrders() {
const orders = [];
let position = 0;
const sidebar = document.getElementById('sidebar-channels-list');
// Iterate over top-level items
const topLevelItems = sidebar.children;
Array.from(topLevelItems).forEach(item => {
if (item.classList.contains('category-wrapper')) {
// It's a category
const catId = item.dataset.id;
orders.push({
id: catId,
position: position++,
category_id: null
});
// Now add all channels inside this category
const subChannels = item.querySelectorAll('.category-group .channel-item-container');
subChannels.forEach(sub => {
orders.push({
id: sub.dataset.id,
position: position++,
category_id: catId
});
});
} else if (item.classList.contains('channel-item-container')) {
// It's a top level channel
orders.push({
id: item.dataset.id,
position: position++,
category_id: null
});
}
});
try {
const resp = await fetch('api_v1_channels.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'reorder',
server_id: <?php echo $active_server_id; ?>,
orders: orders
})
});
const data = await resp.json();
if (!data.success) {
console.error('Failed to save channel order:', data.error);
}
} catch (e) {
console.error('Error saving channel order:', e);
}
}
<?php endif; ?>
</script>
</body>
</html>