diff --git a/bot.py b/bot.py
index bfec921..e914c70 100644
--- a/bot.py
+++ b/bot.py
@@ -124,7 +124,7 @@ class MusicBot(commands.Cog):
print(f"Error loading config: {e}")
return None
- def save_channel_id(self, guild_id: int, channel_id: int):
+ def save_channel_id(self, guild_id: int, channel_id: Optional[int]):
config_path = "config/config.json"
try:
import json
@@ -133,7 +133,11 @@ class MusicBot(commands.Cog):
if os.path.exists(config_path):
with open(config_path, "r") as f:
data = json.load(f)
- data[str(guild_id)] = channel_id
+ if channel_id is None:
+ if str(guild_id) in data:
+ del data[str(guild_id)]
+ else:
+ data[str(guild_id)] = channel_id
with open(config_path, "w") as f:
json.dump(data, f, indent=4)
except Exception as e:
diff --git a/static/app.js b/static/app.js
index f8f81e8..a6b348e 100644
--- a/static/app.js
+++ b/static/app.js
@@ -16,8 +16,10 @@ let wsConnectedOnce = false;
// DOM Elements
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
-const voiceSelect = document.getElementById('voice-channel-select');
-const textChannelSelect = document.getElementById('text-channel-select');
+const btnOpenSettings = document.getElementById('btn-open-settings');
+const btnCloseSettings = document.getElementById('btn-close-settings');
+const settingsModal = document.getElementById('settings-modal');
+const settingsGuildsList = document.getElementById('settings-guilds-list');
const btnConnect = document.getElementById('btn-connect');
const trackTitle = document.getElementById('track-title');
const trackArtist = document.getElementById('track-artist');
@@ -162,8 +164,14 @@ function bindEvents() {
// Add Song Form
addForm.addEventListener('submit', addTrack);
- // Text Channel Select
- textChannelSelect.addEventListener('change', changeTextChannel);
+ // Settings Modal Events
+ if (btnOpenSettings) btnOpenSettings.addEventListener('click', openSettingsModal);
+ if (btnCloseSettings) btnCloseSettings.addEventListener('click', closeSettingsModal);
+ if (settingsModal) {
+ settingsModal.addEventListener('click', (e) => {
+ if (e.target === settingsModal) closeSettingsModal();
+ });
+ }
// Upload Events
if (btnSelectFile && fileInput) {
@@ -296,30 +304,9 @@ function updateUI(status) {
}
}
- // Text selector population (only if select doesn't have focus/interaction)
- if (document.activeElement !== textChannelSelect) {
- const currentSelectedText = textChannelSelect.value;
- textChannelSelect.innerHTML = '';
-
- if (status.text_channels && status.text_channels.length > 0) {
- status.text_channels.forEach(channel => {
- const opt = document.createElement('option');
- opt.value = channel.channel_id;
- opt.textContent = `${channel.guild_name} - #${channel.channel_name}`;
- textChannelSelect.appendChild(opt);
- });
-
- if (status.active_text_channel_id) {
- textChannelSelect.value = status.active_text_channel_id;
- } else if (currentSelectedText && status.text_channels.some(c => c.channel_id === currentSelectedText)) {
- textChannelSelect.value = currentSelectedText;
- }
- } else {
- const opt = document.createElement('option');
- opt.value = "";
- opt.textContent = "Brak dostępnych kanałów";
- textChannelSelect.appendChild(opt);
- }
+ // Settings Guilds Population
+ if (status.guilds) {
+ renderSettingsGuilds(status.guilds);
}
// 2. Playback State
@@ -753,19 +740,73 @@ async function setVolume(vol) {
}
}
-// Change Text Notification Channel
-async function changeTextChannel() {
- const channelId = textChannelSelect.value;
- if (!channelId) return;
+// Settings Modal Functions
+function openSettingsModal() {
+ if (settingsModal) settingsModal.style.display = 'flex';
+}
+
+function closeSettingsModal() {
+ if (settingsModal) settingsModal.style.display = 'none';
+}
+
+function renderSettingsGuilds(guilds) {
+ if (!settingsGuildsList) return;
+ // Don't re-render if user is currently interacting with a select inside settings
+ if (settingsModal && settingsModal.style.display !== 'none' && document.activeElement && document.activeElement.classList.contains('guild-log-select')) {
+ return;
+ }
+
+ if (!guilds || guilds.length === 0) {
+ settingsGuildsList.innerHTML = '
Brak serwerów Discord w pamięci bota.
';
+ lucide.createIcons();
+ return;
+ }
+
+ let html = '';
+ guilds.forEach(guild => {
+ let optionsHtml = '';
+ if (guild.text_channels && guild.text_channels.length > 0) {
+ guild.text_channels.forEach(tc => {
+ const isSelected = guild.active_notification_channel_id === tc.channel_id ? 'selected' : '';
+ optionsHtml += ``;
+ });
+ } else {
+ optionsHtml = '';
+ }
+
+ html += `
+
+
+
+
+
${guild.guild_name}
+
ID: ${guild.guild_id}
+
+
+
+
+
+
+ `;
+ });
+
+ settingsGuildsList.innerHTML = html;
+ lucide.createIcons();
+}
+
+async function saveGuildLogChannel(guildId, channelId) {
try {
const res = await fetch('/api/settings/text_channel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ channel_id: channelId })
+ body: JSON.stringify({ guild_id: guildId, channel_id: channelId || "" })
});
if (!res.ok) throw new Error('Settings change failed');
- showToast('Zapisano kanał powiadomień na Discordzie!', 'success');
+ showToast('Zapisano kanał powiadomień dla serwera!', 'success');
} catch (err) {
showToast('Nie udało się zapisać kanału powiadomień', 'error');
}
diff --git a/static/index.html b/static/index.html
index b276b99..c8b78bd 100644
--- a/static/index.html
+++ b/static/index.html
@@ -27,10 +27,14 @@
ŻyrarBOT
Kontroler Web
-
-
-
-
Rozłączony
+
@@ -41,7 +45,7 @@
Kanał Głosowy
-
+
@@ -49,13 +53,6 @@
Połącz
-
-
Kanał Tekstowy Logów
-
-
-
@@ -217,6 +214,29 @@
+
+
+
+
+
+
+
Konfiguracja Serwerów Discord (Kanały Logów)
+
Wybierz kanał tekstowy na każdym serwerze Discord, na który bot ma wysyłać powiadomienia o odtwarzanych utworach i zmianach w kolejce.
+
+
+
+
+
Ładowanie listy serwerów...
+
+
+
+
+
+
+
diff --git a/static/style.css b/static/style.css
index 600a532..e8f40fa 100644
--- a/static/style.css
+++ b/static/style.css
@@ -923,3 +923,121 @@ body {
.toast.info i {
color: var(--secondary);
}
+
+/* Settings Modal */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(8px);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ animation: fadeIn 0.2s ease-out;
+}
+
+.modal-card {
+ width: 100%;
+ max-width: 650px;
+ max-height: 85vh;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-card);
+ border: 1px solid var(--border-glass);
+ border-radius: 24px;
+ box-shadow: var(--shadow-premium);
+ overflow: hidden;
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 20px 24px;
+ border-bottom: 1px solid var(--border-glass);
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.modal-body {
+ padding: 24px;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.settings-section {
+ margin-bottom: 24px;
+}
+
+.settings-section h3 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: var(--text-main);
+ margin-bottom: 8px;
+}
+
+.settings-desc {
+ font-size: 0.9rem;
+ color: var(--text-muted);
+ margin-bottom: 16px;
+ line-height: 1.5;
+}
+
+.guild-setting-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid var(--border-glass);
+ margin-bottom: 12px;
+ gap: 16px;
+}
+
+.guild-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex: 1;
+ min-width: 0;
+}
+
+.guild-icon {
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ background: rgba(139, 92, 246, 0.15);
+ color: var(--primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ flex-shrink: 0;
+}
+
+.guild-name {
+ font-weight: 600;
+ color: var(--text-main);
+ font-size: 0.95rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.guild-id {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+}
+
+.guild-control {
+ width: 240px;
+ flex-shrink: 0;
+}
diff --git a/web.py b/web.py
index f95fd9c..8221823 100644
--- a/web.py
+++ b/web.py
@@ -35,6 +35,7 @@ class ConnectRequest(BaseModel):
class TextChannelRequest(BaseModel):
channel_id: str
+ guild_id: Optional[str] = None
class PlayFileRequest(BaseModel):
filename: str
@@ -77,19 +78,39 @@ async def get_status():
if not music_cog:
return {"error": "MusicBot cog not loaded"}
+ all_guilds = []
+ for g in bot.guilds:
+ g_tc = []
+ for tc in g.text_channels:
+ if tc.permissions_for(g.me).send_messages:
+ g_tc.append({
+ "channel_id": str(tc.id),
+ "channel_name": tc.name
+ })
+ active_notif = music_cog.get_notification_channel(g.id)
+ all_guilds.append({
+ "guild_id": str(g.id),
+ "guild_name": g.name,
+ "text_channels": g_tc,
+ "active_notification_channel_id": str(active_notif.id) if active_notif else None
+ })
+
guild = get_active_guild()
if not guild:
return {
"playing": False,
"paused": False,
"loop_single": False,
+ "loop_playlist": False,
"volume": 100,
"current": None,
"queue": [],
+ "history": [],
"voice_channels": [],
"active_voice_channel": None,
"text_channels": [],
- "active_text_channel_id": None
+ "active_text_channel_id": None,
+ "guilds": all_guilds
}
guild_id = guild.id
@@ -161,6 +182,7 @@ async def get_status():
"active_voice_channel": active_vc,
"text_channels": text_channels,
"active_text_channel_id": active_tc_id,
+ "guilds": all_guilds,
"history": [
{
"id": item["id"],
@@ -590,22 +612,35 @@ async def api_set_text_channel(req: TextChannelRequest):
if not music_cog:
raise HTTPException(status_code=500, detail="MusicBot cog not loaded")
+ if not req.channel_id or req.channel_id == "0" or req.channel_id == "":
+ if not req.guild_id:
+ raise HTTPException(status_code=400, detail="guild_id required to reset channel")
+ guild_id = int(req.guild_id)
+ if guild_id in music_cog.last_channels:
+ del music_cog.last_channels[guild_id]
+ music_cog.save_channel_id(guild_id, None)
+ await broadcast_status()
+ return {"status": "success", "channel_id": None, "guild_id": str(guild_id)}
+
channel = bot.get_channel(int(req.channel_id))
if not channel:
raise HTTPException(status_code=404, detail="Text channel not found")
guild_id = channel.guild.id
+ if req.guild_id and int(req.guild_id) != guild_id:
+ raise HTTPException(status_code=400, detail="Channel does not belong to the specified guild")
+
music_cog.last_channels[guild_id] = channel
music_cog.save_channel_id(guild_id, int(req.channel_id))
# Send test message to confirm
try:
- await channel.send(f"🔔 Ten kanał został ustawiony jako główny kanał powiadomień dla ŻyrarBOT z panelu webowego.")
+ await channel.send(f"🔔 Ten kanał został ustawiony jako główny kanał powiadomień dla ŻyrarBOT na serwerze **{channel.guild.name}** z panelu webowego.")
except Exception as e:
print(f"Failed to send test message: {e}")
await broadcast_status()
- return {"status": "success", "channel_id": req.channel_id}
+ return {"status": "success", "channel_id": req.channel_id, "guild_id": str(guild_id)}
# WebSocket Background Loop and Startup Hook
async def status_broadcast_loop():