added way to delete channels
This commit is contained in:
parent
a90a9f0a0d
commit
4ae6004239
21
db.ts
21
db.ts
|
|
@ -386,15 +386,20 @@ export function deleteChannelFromDb(id: string): void {
|
||||||
|
|
||||||
// Queue persistence functions
|
// Queue persistence functions
|
||||||
export function saveChannelQueue(channelId: string, trackIds: string[]): void {
|
export function saveChannelQueue(channelId: string, trackIds: string[]): void {
|
||||||
// Delete existing queue
|
db.query("BEGIN").run();
|
||||||
db.query("DELETE FROM channel_queue WHERE channel_id = ?").run(channelId);
|
try {
|
||||||
|
db.query("DELETE FROM channel_queue WHERE channel_id = ?").run(channelId);
|
||||||
|
|
||||||
// Insert new queue
|
const insert = db.query(
|
||||||
const insert = db.query(
|
"INSERT INTO channel_queue (channel_id, track_id, position) VALUES (?, ?, ?)"
|
||||||
"INSERT INTO channel_queue (channel_id, track_id, position) VALUES (?, ?, ?)"
|
);
|
||||||
);
|
for (let i = 0; i < trackIds.length; i++) {
|
||||||
for (let i = 0; i < trackIds.length; i++) {
|
insert.run(channelId, trackIds[i], i);
|
||||||
insert.run(channelId, trackIds[i], i);
|
}
|
||||||
|
db.query("COMMIT").run();
|
||||||
|
} catch (e) {
|
||||||
|
db.query("ROLLBACK").run();
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,28 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Delete a channel
|
||||||
|
M.deleteChannel = async function(channelId) {
|
||||||
|
const channel = M.channels?.find(c => c.id === channelId);
|
||||||
|
if (!channel) return;
|
||||||
|
if (channel.isDefault) {
|
||||||
|
M.showToast("Cannot delete default channel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Delete channel "${channel.name}"?`)) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/channels/${channelId}`, { method: "DELETE" });
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
M.showToast(err.error || "Failed to delete channel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
M.showToast(`Channel "${channel.name}" deleted`);
|
||||||
|
} catch (e) {
|
||||||
|
M.showToast("Failed to delete channel");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// New channel creation with slideout input
|
// New channel creation with slideout input
|
||||||
M.createNewChannel = async function() {
|
M.createNewChannel = async function() {
|
||||||
const header = M.$("#channels-panel .panel-header");
|
const header = M.$("#channels-panel .panel-header");
|
||||||
|
|
@ -129,14 +151,29 @@
|
||||||
const listenersHtml = Object.entries(counts).map(([name, count]) =>
|
const listenersHtml = Object.entries(counts).map(([name, count]) =>
|
||||||
`<div class="listener">${name}${count > 1 ? ` <span class="listener-mult">x${count}</span>` : ""}</div>`
|
`<div class="listener">${name}${count > 1 ? ` <span class="listener-mult">x${count}</span>` : ""}</div>`
|
||||||
).join("");
|
).join("");
|
||||||
|
|
||||||
|
// Show delete button for non-default channels if user is admin or creator
|
||||||
|
const canDelete = !ch.isDefault && M.currentUser &&
|
||||||
|
(M.currentUser.isAdmin || ch.createdBy === M.currentUser.id);
|
||||||
|
const deleteBtn = canDelete ? `<button class="btn-delete-channel" title="Delete channel">×</button>` : "";
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="channel-header">
|
<div class="channel-header">
|
||||||
<span class="channel-name">${ch.name}</span>
|
<span class="channel-name">${ch.name}</span>
|
||||||
|
${deleteBtn}
|
||||||
<span class="listener-count">${ch.listenerCount}</span>
|
<span class="listener-count">${ch.listenerCount}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="channel-listeners">${listenersHtml}</div>
|
<div class="channel-listeners">${listenersHtml}</div>
|
||||||
`;
|
`;
|
||||||
div.querySelector(".channel-header").onclick = () => M.switchChannel(ch.id);
|
const headerEl = div.querySelector(".channel-header");
|
||||||
|
headerEl.querySelector(".channel-name").onclick = () => M.switchChannel(ch.id);
|
||||||
|
const delBtn = headerEl.querySelector(".btn-delete-channel");
|
||||||
|
if (delBtn) {
|
||||||
|
delBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
M.deleteChannel(ch.id);
|
||||||
|
};
|
||||||
|
}
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe
|
||||||
#channels-list .channel-header:hover { background: #222; }
|
#channels-list .channel-header:hover { background: #222; }
|
||||||
#channels-list .channel-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; }
|
#channels-list .channel-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; }
|
||||||
#channels-list .listener-count { font-size: 0.65rem; color: #666; flex-shrink: 0; margin-left: 0.3rem; }
|
#channels-list .listener-count { font-size: 0.65rem; color: #666; flex-shrink: 0; margin-left: 0.3rem; }
|
||||||
|
#channels-list .btn-delete-channel { background: none; border: none; color: #666; font-size: 0.9rem; cursor: pointer; padding: 0 0.2rem; line-height: 1; opacity: 0; transition: opacity 0.15s; }
|
||||||
|
#channels-list .channel-header:hover .btn-delete-channel { opacity: 1; }
|
||||||
|
#channels-list .btn-delete-channel:hover { color: #e44; }
|
||||||
#channels-list .channel-listeners { display: flex; flex-direction: column; margin-left: 0.5rem; border-left: 1px solid #333; padding-left: 0.3rem; }
|
#channels-list .channel-listeners { display: flex; flex-direction: column; margin-left: 0.5rem; border-left: 1px solid #333; padding-left: 0.3rem; }
|
||||||
#channels-list .listener { font-size: 0.65rem; color: #aaa; padding: 0.05rem 0; position: relative; }
|
#channels-list .listener { font-size: 0.65rem; color: #aaa; padding: 0.05rem 0; position: relative; }
|
||||||
#channels-list .listener::before { content: ""; position: absolute; left: -0.3rem; top: 50%; width: 0.2rem; height: 1px; background: #333; }
|
#channels-list .listener::before { content: ""; position: absolute; left: -0.3rem; top: 50%; width: 0.2rem; height: 1px; background: #333; }
|
||||||
|
|
|
||||||
12
server.ts
12
server.ts
|
|
@ -475,6 +475,18 @@ serve({
|
||||||
if (!user.is_admin && channel.createdBy !== user.id) {
|
if (!user.is_admin && channel.createdBy !== user.id) {
|
||||||
return Response.json({ error: "Access denied" }, { status: 403 });
|
return Response.json({ error: "Access denied" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move connected clients to default channel before deleting
|
||||||
|
const defaultChannel = [...channels.values()].find(c => c.isDefault);
|
||||||
|
if (defaultChannel && channel.clients.size > 0) {
|
||||||
|
for (const ws of channel.clients) {
|
||||||
|
channel.removeClient(ws);
|
||||||
|
ws.data.channelId = defaultChannel.id;
|
||||||
|
defaultChannel.addClient(ws);
|
||||||
|
ws.send(JSON.stringify({ type: "switched", channelId: defaultChannel.id }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
channels.delete(channelId);
|
channels.delete(channelId);
|
||||||
deleteChannelFromDb(channelId);
|
deleteChannelFromDb(channelId);
|
||||||
broadcastChannelList();
|
broadcastChannelList();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue