1/** 2 * Settings System (Discord/Vencord-inspired) 3 * Manages user settings, themes, and preferences 4 */ 5 6window.ChatroomSettings = (function() { 7 'use strict'; 8 9 function emitDevLog() { 10 if (typeof window !== 'undefined' && typeof window.devLog === 'function') { 11 window.devLog.apply(window, arguments); 12 } 13 } 14 15 let settingsRef = null; 16 let currentUsername = null; 17 let settingsBooted = false; 18 let usernamePollId = null; 19 let usernamePollTimeoutId = null; 20 const PALETTE_THEMES = ['default', 'midnight', 'forest', 'sunset', 'banana', 'ocean', 'rose', 'sakura', 'purple']; 21 22 const CHAT_PALETTE_OPTIONS = [ 23 { value: 'default', label: 'Default' }, 24 { value: 'midnight', label: 'Midnight' }, 25 { value: 'forest', label: 'Forest' }, 26 { value: 'sunset', label: 'Sunset' }, 27 { value: 'banana', label: 'Banana' }, 28 { value: 'ocean', label: 'Ocean' }, 29 { value: 'rose', label: 'Rose' }, 30 { value: 'sakura', label: 'Sakura' }, 31 { value: 'purple', label: 'Purple (Legacy)' } 32 ]; 33 34 const CHAT_SETTINGS_TAB_DEFS = [ 35 { id: 'appearance', label: 'Appearance', icon: 'fa-solid fa-palette' }, 36 { id: 'messages', label: 'Messages', icon: 'fa-solid fa-comments' }, 37 { id: 'notifications', label: 'Notifications', icon: 'fa-solid fa-bell' }, 38 { id: 'voice', label: 'Voice & Chat', icon: 'fa-solid fa-microphone' }, 39 { id: 'discord', label: 'Discord', icon: 'fa-brands fa-discord' }, 40 { id: 'advanced', label: 'Advanced', icon: 'fa-solid fa-sliders' }, 41 { id: 'moderation', label: 'Moderation', icon: 'fa-solid fa-shield-halved', modOnly: true } 42 ]; 43 44 let activeChatSettingsTab = 'appearance'; 45 46 let settings = { 47 theme: 'default', 48 colorMode: 'dark', 49 messageGrouping: true, 50 showTimestamps: true, 51 showAvatars: true, 52 fontSize: 'medium', 53 emojiSize: 'medium', 54 animations: true, 55 soundEffects: false, 56 notifications: false, 57 desktopNotifications: false, 58 enterToSend: true, 59 muteOnJoinVoice: false, 60 playJoinLeaveSounds: true, 61 autoScrollNewMessages: true, 62 showLinkPreviews: true, 63 showOnlineStatus: true, 64 confirmBeforeLeavingVoice: true, 65 useLanyardStatus: false, 66 discordUserId: '' 67 }; 68 69 /** 70 * Initialize settings system 71 */ 72 function init() { 73 if (settingsBooted) { 74 applySettings(); 75 return true; 76 } 77 settingsBooted = true; 78 79 if (typeof firebase === 'undefined' || !firebase.database) { 80 console.error('Firebase not available for settings'); 81 loadLocalSettings(); 82 return false; 83 } 84 85 settingsRef = firebase.database().ref('chatroom/settings'); 86 87 // Wait for username 88 usernamePollId = setInterval(() => { 89 if (window.__sharedUsername) { 90 currentUsername = window.__sharedUsername.get(); 91 if (currentUsername) { 92 clearInterval(usernamePollId); 93 usernamePollId = null; 94 loadSettings(); 95 setupUI(); 96 } 97 } 98 }, 100); 99 100 usernamePollTimeoutId = setTimeout(() => { 101 usernamePollTimeoutId = null; 102 if (usernamePollId) { 103 clearInterval(usernamePollId); 104 usernamePollId = null; 105 } 106 if (!currentUsername) { 107 currentUsername = 'guest_' + Date.now(); 108 loadLocalSettings(); 109 setupUI(); 110 } 111 }, 5000); 112 113 return true; 114 } 115 116 /** 117 * Load settings from Firebase 118 */ 119 function loadSettings() { 120 // First load from localStorage (fast) 121 loadLocalSettings(); 122 123 // Then sync from Firebase (if available) 124 if (!settingsRef || !currentUsername) { 125 return; 126 } 127 128 settingsRef.child(currentUsername).once('value', (snapshot) => { 129 if (snapshot.exists()) { 130 const saved = snapshot.val(); 131 Object.assign(settings, saved); 132 // Update localStorage with Firebase data 133 try { 134 localStorage.setItem('chatroom_settings', JSON.stringify(settings)); 135 } catch (e) { 136 console.error('Error saving settings to localStorage:', e); 137 } 138 } 139 hydrateThemeFromSite(); 140 syncDiscordLinkToProfile(); 141 applySettings(); 142 }); 143 } 144 145 function migrateThemeSettings() { 146 if (settings.theme === 'dark') { 147 settings.theme = 'default'; 148 settings.colorMode = 'dark'; 149 } else if (settings.theme === 'light') { 150 settings.theme = 'default'; 151 settings.colorMode = 'light'; 152 } 153 if (!settings.colorMode) { 154 settings.colorMode = 'dark'; 155 } 156 if (!PALETTE_THEMES.includes(settings.theme)) { 157 settings.theme = 'default'; 158 } 159 } 160 161 /** Sitewide palette + light/dark are owned by SiteThemePalettes / SiteThemeToggle. */ 162 function hydrateThemeFromSite() { 163 migrateThemeSettings(); 164 if (window.SiteThemePalettes && typeof window.SiteThemePalettes.readMergedThemeSettings === 'function') { 165 const siteTheme = window.SiteThemePalettes.readMergedThemeSettings(); 166 settings.theme = siteTheme.palette; 167 settings.colorMode = siteTheme.colorMode; 168 return; 169 } 170 syncColorModeFromSiteSettings(); 171 } 172 173 function pushThemeToSite(source, patch) { 174 const src = source || 'chatroom-settings'; 175 const palette = patch && patch.palette !== undefined ? patch.palette : settings.theme; 176 const colorMode = patch && patch.colorMode !== undefined 177 ? (patch.colorMode === 'light' ? 'light' : 'dark') 178 : (settings.colorMode === 'light' ? 'light' : 'dark'); 179 if (window.SiteThemeToggle) { 180 if ((!patch || patch.palette !== undefined) && typeof window.SiteThemeToggle.setPalette === 'function') { 181 window.SiteThemeToggle.setPalette(palette, src); 182 } 183 if ((!patch || patch.colorMode !== undefined) && typeof window.SiteThemeToggle.setColorMode === 'function') { 184 window.SiteThemeToggle.setColorMode(colorMode === 'dark', src); 185 } 186 return; 187 } 188 if (window.SiteThemePalettes && typeof window.SiteThemePalettes.writeMergedThemeSettings === 'function') { 189 const writePatch = {}; 190 if (!patch || patch.palette !== undefined) writePatch.palette = palette; 191 if (!patch || patch.colorMode !== undefined) writePatch.colorMode = colorMode; 192 if (Object.keys(writePatch).length === 0) { 193 writePatch.palette = palette; 194 writePatch.colorMode = colorMode; 195 } 196 window.SiteThemePalettes.writeMergedThemeSettings(writePatch); 197 } 198 } 199 200 function syncColorModeFromSiteSettings() { 201 try { 202 const raw = localStorage.getItem('puppywifey_settings'); 203 if (!raw) return; 204 const site = JSON.parse(raw); 205 if (typeof site.darkMode === 'boolean') { 206 settings.colorMode = site.darkMode ? 'dark' : 'light'; 207 } 208 } catch (e) { /* ignore */ } 209 } 210 211 /** 212 * Load settings from localStorage 213 */ 214 function loadLocalSettings() { 215 try { 216 const saved = localStorage.getItem('chatroom_settings'); 217 if (saved) { 218 Object.assign(settings, JSON.parse(saved)); 219 } 220 } catch (e) { 221 console.error('Error loading local settings:', e); 222 } 223 hydrateThemeFromSite(); 224 applySettings(); 225 } 226 227 /** 228 * Save settings 229 */ 230 function saveSettings() { 231 // Save to localStorage first (immediate) 232 try { 233 localStorage.setItem('chatroom_settings', JSON.stringify(settings)); 234 } catch (e) { 235 console.error('Error saving local settings:', e); 236 } 237 238 // Then save to Firebase (async) 239 if (settingsRef && currentUsername) { 240 settingsRef.child(currentUsername).set(settings).catch(error => { 241 console.error('Error saving settings to Firebase:', error); 242 }); 243 } 244 245 syncDiscordLinkToProfile(); 246 applySettings(); 247 } 248 249 function syncDiscordLinkToProfile() { 250 if (!window.ChatroomProfilePicture || !window.ChatroomProfilePicture.setDiscordLink) return; 251 const id = String(settings.discordUserId || '').replace(/\D/g, ''); 252 const enabled = settings.useLanyardStatus === true && id.length >= 17; 253 window.ChatroomProfilePicture.setDiscordLink(enabled ? id : '', enabled); 254 } 255 256 function getChatroomPageWrap() { 257 if (window.ChatroomSpa && typeof window.ChatroomSpa.getPageWrap === 'function') { 258 return window.ChatroomSpa.getPageWrap(); 259 } 260 if (window.CitrusRouteScope && typeof window.CitrusRouteScope.isCitrusHost === 'function' && 261 window.CitrusRouteScope.isCitrusHost()) { 262 var scoped = document.querySelector( 263 '.citrus-window__content.page-chatroom #page-wrap, ' + 264 '.citrus-fullscreen__content.page-chatroom #page-wrap, ' + 265 '.citrus-window__content #page-wrap[data-chatroom-layout], ' + 266 '.citrus-fullscreen__content #page-wrap[data-chatroom-layout], ' + 267 '.citrus-window__content [data-citrus-app-root]#page-wrap, ' + 268 '.citrus-fullscreen__content [data-citrus-app-root]#page-wrap' 269 ); 270 if (scoped) return scoped; 271 } 272 return document.getElementById('page-wrap'); 273 } 274 275 function getCitrusChatContainer() { 276 return document.querySelector( 277 '.citrus-window__content.page-chatroom, .citrus-fullscreen__content.page-chatroom, ' + 278 '.citrus-window__content #page-wrap[data-chatroom-layout], ' + 279 '.citrus-fullscreen__content #page-wrap[data-chatroom-layout]' 280 ); 281 } 282 283 function hasMountedCitrusChatShell() { 284 if (window.ChatroomSpa && typeof window.ChatroomSpa.hasMountedCitrusChat === 'function') { 285 return window.ChatroomSpa.hasMountedCitrusChat(); 286 } 287 return !!getCitrusChatContainer(); 288 } 289 290 /** 291 * Apply settings to UI 292 */ 293 function getChatroomThemeRoot() { 294 return getChatroomPageWrap() || document.documentElement; 295 } 296 297 function isChatroomRouteActive() { 298 if (window.CitrusRouteScope && typeof window.CitrusRouteScope.isCitrusHost === 'function' && 299 window.CitrusRouteScope.isCitrusHost()) { 300 if (typeof window.CitrusRouteScope.hasClass === 'function' && 301 window.CitrusRouteScope.hasClass('page-chatroom')) { 302 return true; 303 } 304 var wrap = getChatroomPageWrap(); 305 return !!(wrap && isChatroomWrapInCitrusWindow(wrap)); 306 } 307 return !!(document.body && document.body.classList.contains('page-chatroom')); 308 } 309 310 function isChatroomWrapInCitrusWindow(wrap) { 311 if (!wrap || !wrap.closest) return false;
312 return !!wrap.closest( 313 '.citrus-window__content.page-chatroom, .citrus-fullscreen__content.page-chatroom, ' + 314 '.citrus-window__content, .citrus-fullscreen__content' 315 ); 316 } 317 318 function clearChatroomThemeAttrsFromShell() { 319 var wrap = getChatroomPageWrap(); 320 var targets = [wrap, document.body, document.documentElement]; 321 var attrs = [ 322 'data-chatroom-theme', 323 'data-chatroom-color-mode', 324 'data-chatroom-font-size', 325 'data-chatroom-emoji-size' 326 ]; 327 var classes = [ 328 'animations', 329 'message-grouping', 330 'show-timestamps', 331 'show-avatars', 332 'show-link-previews', 333 'show-online-status', 334 'hide-online-status' 335 ]; 336 var i; 337 var j; 338 for (i = 0; i < targets.length; i++) { 339 var el = targets[i]; 340 if (!el) continue; 341 for (j = 0; j < attrs.length; j++) { 342 el.removeAttribute(attrs[j]); 343 } 344 for (j = 0; j < classes.length; j++) { 345 el.classList.remove(classes[j]); 346 } 347 } 348 if (document.body) { 349 document.body.style.fontSize = ''; 350 document.body.style.fontFamily = ''; 351 } 352 } 353 354 function applySettings(opts) { 355 opts = opts || {}; 356 hydrateThemeFromSite(); 357 const theme = PALETTE_THEMES.includes(settings.theme) ? settings.theme : 'default'; 358 const colorMode = settings.colorMode === 'light' ? 'light' : 'dark'; 359 settings.theme = theme; 360 settings.colorMode = colorMode; 361 362 var routeActive = isChatroomRouteActive(); 363 if (!routeActive) { 364 var activeWrap = getChatroomPageWrap(); 365 routeActive = isChatroomWrapInCitrusWindow(activeWrap); 366 } 367 if (!routeActive && getCitrusChatContainer()) { 368 routeActive = true; 369 } 370 if (!routeActive && hasMountedCitrusChatShell()) { 371 routeActive = true; 372 } 373 374 if (!routeActive) { 375 if (window.ChatroomSpa && typeof window.ChatroomSpa.clearChatroomTheme === 'function') { 376 window.ChatroomSpa.clearChatroomTheme(null, { force: true }); 377 } else { 378 clearChatroomThemeAttrsFromShell(); 379 } 380 if (typeof window.refreshSidebarNavChrome === 'function') { 381 window.refreshSidebarNavChrome(); 382 } else if (typeof window.ensureSidebarNavStyles === 'function') { 383 window.ensureSidebarNavStyles(); 384 } 385 document.dispatchEvent(new CustomEvent('chatroom-messages-layout-refresh')); 386 emitDevLog('â Settings applied (chatroom theme skipped â not on chatroom route)'); 387 return; 388 } 389 390 var themeRoot = getChatroomThemeRoot(); 391 themeRoot.setAttribute('data-chatroom-theme', theme); 392 themeRoot.setAttribute('data-chatroom-color-mode', colorMode); 393 if (document.body && document.body.classList.contains('page-citrusos')) { 394 document.body.setAttribute('data-chatroom-theme', theme); 395 document.body.setAttribute('data-chatroom-color-mode', colorMode); 396 } 397 398 if (window.ChatroomSpa && typeof window.ChatroomSpa.scheduleChatroomThemeRecovery === 'function') { 399 window.ChatroomSpa.scheduleChatroomThemeRecovery(); 400 if (typeof window.ChatroomSpa.refreshChatroomScrollLayout === 'function') { 401 window.ChatroomSpa.refreshChatroomScrollLayout(); 402 } 403 } else if (window.ChatroomSpa && typeof window.ChatroomSpa.scheduleChatroomThemeApply === 'function') { 404 window.ChatroomSpa.scheduleChatroomThemeApply(); 405 if (typeof window.ChatroomSpa.refreshChatroomScrollLayout === 'function') { 406 window.ChatroomSpa.refreshChatroomScrollLayout(); 407 } 408 } else if (window.ChatroomSpa && typeof window.ChatroomSpa.applyChatroomTheme === 'function') { 409 window.ChatroomSpa.applyChatroomTheme(); 410 if (typeof window.ChatroomSpa.refreshChatroomScrollLayout === 'function') { 411 window.ChatroomSpa.refreshChatroomScrollLayout(); 412 } 413 } 414 415 // Apply message grouping and other toggles (scoped to chatroom shell) 416 themeRoot.classList.toggle('message-grouping', settings.messageGrouping !== false); 417 themeRoot.classList.toggle('show-timestamps', settings.showTimestamps === true); 418 themeRoot.classList.toggle('show-avatars', settings.showAvatars === true); 419 themeRoot.classList.toggle('animations', settings.animations === true); 420 421 // Apply font size 422 if (settings.fontSize) { 423 themeRoot.setAttribute('data-chatroom-font-size', settings.fontSize); 424 } else { 425 themeRoot.setAttribute('data-chatroom-font-size', 'medium'); 426 } 427 428 // Apply emoji size 429 if (settings.emojiSize) { 430 themeRoot.setAttribute('data-chatroom-emoji-size', settings.emojiSize); 431 } else { 432 themeRoot.setAttribute('data-chatroom-emoji-size', 'medium'); 433 } 434 435 // Toggles for chatroom behavior (used by other scripts) 436 themeRoot.classList.toggle('show-link-previews', settings.showLinkPreviews === true); 437 themeRoot.classList.toggle('show-online-status', settings.showOnlineStatus !== false); 438 themeRoot.classList.toggle('hide-online-status', settings.showOnlineStatus === false); 439 440 if (window.ChatroomSelectionStyle && typeof window.ChatroomSelectionStyle.scheduleReinject === 'function') { 441 window.ChatroomSelectionStyle.scheduleReinject(); 442 } else if (window.ChatroomSelectionStyle && typeof window.ChatroomSelectionStyle.inject === 'function') { 443 window.ChatroomSelectionStyle.inject(); 444 } 445 446 document.dispatchEvent(new CustomEvent('chatroom-messages-layout-refresh')); 447 448 emitDevLog('â Settings applied:', { 449 theme: settings.theme, 450 colorMode: settings.colorMode, 451 fontSize: settings.fontSize, 452 }); 453 } 454 455 function isCitrusOsHost() { 456 return !!(document.body && document.body.classList.contains('page-citrusos')); 457 } 458 459 function isSettingsInAuxWindow() { 460 const modal = document.getElementById('settings-modal'); 461 return !!(modal && modal.closest('.chatroom-citrus-aux-host')); 462 } 463
464 function ensureCitrusSettingsCss() { 465 if (!isCitrusOsHost()) return; 466 if (document.querySelector('link[data-citrus-chat-settings-css]')) return; 467 ['/css/citrusSettings.css', '/css/citrusChatroomSettings.css'].forEach(function (href) { 468 if (document.querySelector('link[href="' + href + '"]')) return; 469 const link = document.createElement('link'); 470 link.rel = 'stylesheet'; 471 link.href = href; 472 link.setAttribute('data-citrus-chat-settings-css', '1'); 473 document.head.appendChild(link); 474 }); 475 } 476 477 function getCategoryPanel(category) { 478 if (isCitrusOsHost()) { 479 return document.querySelector('[data-chatroom-settings-panel="' + category + '"]'); 480 } 481 return document.getElementById('chatroom-settings-panel'); 482 } 483 484 function setActiveChatSettingsTab(category) { 485 if (!isCitrusOsHost()) return; 486 activeChatSettingsTab = category || 'appearance'; 487 document.querySelectorAll('[data-chatroom-settings-tab]').forEach(function (btn) { 488 const on = btn.getAttribute('data-chatroom-settings-tab') === activeChatSettingsTab; 489 btn.classList.toggle('is-active', on); 490 btn.setAttribute('aria-selected', on ? 'true' : 'false'); 491 }); 492 document.querySelectorAll('[data-chatroom-settings-panel]').forEach(function (panelEl) { 493 const on = panelEl.getAttribute('data-chatroom-settings-panel') === activeChatSettingsTab; 494 panelEl.classList.toggle('is-active', on); 495 panelEl.hidden = !on; 496 }); 497 } 498 499 function syncModerationTabVisibility() { 500 const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator(); 501 const legacyBtn = document.getElementById('moderation-category-btn'); 502 if (legacyBtn) { 503 legacyBtn.style.display = isModerator ? 'block' : 'none'; 504 } 505 const citrusBtn = document.querySelector('[data-chatroom-settings-tab="moderation"]'); 506 if (citrusBtn) { 507 citrusBtn.hidden = !isModerator; 508 } 509 } 510 511 function settingsFa(icon) { 512 return '<i class="fa-solid fa-' + icon + ' fa-width-auto settings-row__icon" aria-hidden="true"></i>'; 513 } 514 515 function escapeSettingsHtml(text) { 516 if (text == null) return ''; 517 const div = document.createElement('div'); 518 div.textContent = String(text); 519 return div.innerHTML; 520 } 521 522 function createChatroomSettingsSection(title, faIcon) { 523 const section = document.createElement('section'); 524 section.className = 'settings-section chatroom-settings-section' + 525 (isCitrusOsHost() ? ' settings-section--compact' : ''); 526 const heading = document.createElement('h3'); 527 heading.className = 'settings-section__title'; 528 heading.innerHTML = settingsFa(faIcon) + ' <span>' + escapeSettingsHtml(title) + '</span>'; 529 section.appendChild(heading); 530 return section; 531 } 532 533 function createChatroomToggle(opts) { 534 const wrap = document.createElement('div'); 535 wrap.className = 'chatroom-settings-toggle-wrap'; 536 const label = document.createElement('label'); 537 label.className = 'settings-row settings-row--toggle'; 538 label.innerHTML = 539 '<span class="settings-row__label">' + 540 settingsFa(opts.icon) + 541 '<span>' + escapeSettingsHtml(opts.label) + '</span></span>' + 542 '<span class="settings-switch">' + 543 '<input type="checkbox" id="' + opts.id + '" class="settings-switch__input"' + 544 (opts.checked ? ' checked' : '') + '>' + 545 '<span class="settings-switch__track" aria-hidden="true"></span></span>'; 546 label.querySelector('input').addEventListener('change', function (e) { 547 opts.onChange(e.target.checked); 548 }); 549 wrap.appendChild(label); 550 if (opts.hint) { 551 const hint = document.createElement('p'); 552 hint.className = 'settings-section__hint chatroom-settings-hint'; 553 hint.textContent = opts.hint; 554 wrap.appendChild(hint); 555 } 556 return wrap; 557 } 558 559 function createChatroomSelectField(opts) { 560 const field = document.createElement('div'); 561 field.className = 'settings-field chatroom-settings-field'; 562 const label = document.createElement('span'); 563 label.className = 'settings-field__label'; 564 label.innerHTML = settingsFa(opts.icon) + ' <span>
564' + escapeSettingsHtml(opts.label) + '</span>'; 565 field.appendChild(label); 566 const select = document.createElement('select'); 567 select.id = opts.id; 568 select.className = 'settings-select chatroom-settings-select'; 569 opts.options.forEach(function (opt) { 570 const option = document.createElement('option'); 571 option.value = opt.value; 572 option.textContent = opt.label; 573 if (opt.value === opts.value) option.selected = true; 574 select.appendChild(option); 575 }); 576 select.addEventListener('change', function () { 577 opts.onChange(select.value); 578 }); 579 field.appendChild(select); 580 if (opts.hint) { 581 const hint = document.createElement('p'); 582 hint.className = 'settings-section__hint chatroom-settings-hint'; 583 hint.textContent = opts.hint; 584 field.appendChild(hint); 585 } 586 return field; 587 } 588 589 function createVoiceVolumeSlider(opts) { 590 const wrap = document.createElement('div'); 591 wrap.className = 'voice-settings-volume'; 592 const label = document.createElement('label'); 593 label.className = 'voice-settings-volume__label'; 594 label.setAttribute('for', opts.id); 595 label.textContent = opts.label; 596 wrap.appendChild(label); 597 const slider = document.createElement('input'); 598 slider.type = 'range'; 599 slider.id = opts.id; 600 slider.className = 'voice-settings-volume__slider'; 601 slider.min = '0'; 602 slider.max = '200'; 603 slider.step = '1'; 604 slider.value = String(opts.value); 605 slider.addEventListener('input', function () { 606 opts.onInput(Number(slider.value)); 607 }); 608 wrap.appendChild(slider); 609 return wrap; 610 } 611 612 let voiceDeviceSettingsRoot = null; 613 let voiceDeviceListsLoaded = false; 614 615 async function refreshVoiceDeviceSelects() { 616 const VC = window.ChatroomVoiceChat; 617 if (!VC || !voiceDeviceSettingsRoot) return; 618 619 const inputSelect = voiceDeviceSettingsRoot.querySelector('#voice-setting-input-device'); 620 const outputSelect = voiceDeviceSettingsRoot.querySelector('#voice-setting-output-device'); 621 if (!inputSelect || !outputSelect) return; 622 623 if (VC.ensureDeviceLabels && !voiceDeviceListsLoaded) { 624 await VC.ensureDeviceLabels(); 625 } 626 627 let deviceLists = { inputs: [], outputs: [] }; 628 if (VC.enumerateVoiceDevices) { 629 deviceLists = await VC.enumerateVoiceDevices(); 630 } 631 632 const saved = VC.getVoiceDeviceSettings ? VC.getVoiceDeviceSettings() : { 633 inputDeviceId: '', 634 outputDeviceId: '', 635 inputVolume: 100, 636 outputVolume: 100 637 }; 638 639 function fillSelect(select, items, savedId, defaultLabel) { 640 const previous = select.value; 641 select.innerHTML = ''; 642 const defaultOpt = document.createElement('option'); 643 defaultOpt.value = ''; 644 defaultOpt.textContent = defaultLabel; 645 select.appendChild(defaultOpt); 646 items.forEach(function (item) { 647 const opt = document.createElement('option'); 648 opt.value = item.deviceId; 649 opt.textContent = item.label; 650 if (item.deviceId === savedId) opt.selected = true; 651 select.appendChild(opt); 652 }); 653 if (previous) { 654 for (let i = 0; i < select.options.length; i++) { 655 if (select.options[i].value === previous) { 656 select.value = previous; 657 break; 658 } 659 } 660 } 661 } 662 663 fillSelect(inputSelect, deviceLists.inputs, saved.inputDeviceId, 'Default microphone'); 664 fillSelect(outputSelect, deviceLists.outputs, saved.outputDeviceId, 'Default speaker'); 665 voiceDeviceListsLoaded = true; 666 } 667 668 function buildVoiceDeviceSettingsRoot() { 669 const VC = window.ChatroomVoiceChat; 670 const saved = VC && VC.getVoiceDeviceSettings ? VC.getVoiceDeviceSettings() : { 671 inputDeviceId: '', 672 outputDeviceId: '', 673 inputVolume: 100, 674 outputVolume: 100 675 }; 676 677 const devicesSection = document.createElement('div'); 678 devicesSection.className = 'voice-settings-devices'; 679 680 const grid = document.createElement('div'); 681 grid.className = 'voice-settings-devices__grid'; 682 683 const inputCol = document.createElement('div'); 684 inputCol.className = 'voice-settings-devices__col'; 685 const outputCol = document.createElement('div'); 686 outputCol.className = 'voice-settings-devices__col'; 687 688 const inputSelect = document.createElement('select'); 689 inputSelect.id = 'voice-setting-input-device'; 690 inputSelect.className = 'settings-select chatroom-settings-select'; 691 inputSelect.innerHTML = '<option value="">Default microphone</option>'; 692 693 const outputSelect = document.createElement('select'); 694 outputSelect.id = 'voice-setting-output-device'; 695 outputSelect.className = 'settings-select chatroom-settings-select'; 696 outputSelect.innerHTML = '<option value="">Default speaker</option>'; 697 698 inputCol.innerHTML = '<span class="voice-settings-devices__heading">Microphone</span>'; 699 inputCol.appendChild(inputSelect); 700 outputCol.innerHTML = '<span class="voice-settings-devices__heading">
700Speaker</span>'; 701 outputCol.appendChild(outputSelect); 702 703 grid.appendChild(inputCol); 704 grid.appendChild(outputCol); 705 devicesSection.appendChild(grid); 706 707 const volumeGrid = document.createElement('div'); 708 volumeGrid.className = 'voice-settings-devices__grid voice-settings-devices__grid--volumes'; 709 volumeGrid.appendChild(createVoiceVolumeSlider({ 710 id: 'voice-setting-input-volume', 711 label: 'Input volume', 712 value: saved.inputVolume, 713 onInput: function (val) { 714 if (VC && VC.setInputVolume) VC.setInputVolume(val); 715 } 716 })); 717 volumeGrid.appendChild(createVoiceVolumeSlider({ 718 id: 'voice-setting-output-volume', 719 label: 'Output volume', 720 value: saved.outputVolume, 721 onInput: function (val) { 722 if (VC && VC.setOutputVolume) VC.setOutputVolume(val); 723 } 724 })); 725 devicesSection.appendChild(volumeGrid); 726 727 const micTestRow = document.createElement('div'); 728 micTestRow.className = 'voice-settings-mic-test'; 729 const micTestBtn = document.createElement('button'); 730 micTestBtn.type = 'button'; 731 micTestBtn.id = 'voice-setting-mic-test-btn'; 732 micTestBtn.className = 'voice-settings-mic-test__btn'; 733 micTestBtn.textContent = 'Mic Test'; 734 const micMeter = document.createElement('div'); 735 micMeter.className = 'voice-settings-mic-test__meter'; 736 micMeter.setAttribute('aria-hidden', 'true'); 737 for (let i = 0; i < 32; i++) { 738 const bar = document.createElement('span'); 739 bar.className = 'voice-settings-mic-test__bar'; 740 micMeter.appendChild(bar); 741 } 742 micTestRow.appendChild(micTestBtn); 743 micTestRow.appendChild(micMeter); 744 devicesSection.appendChild(micTestRow); 745 746 inputSelect.addEventListener('change', async function () { 747 if (VC && VC.applyInputDevice) { 748 const wasTesting = VC.isMicTestRunning && VC.isMicTestRunning(); 749 try { 750 await VC.applyInputDevice(inputSelect.value); 751 if (wasTesting) { 752 micTestBtn.textContent = 'Stop Test'; 753 micTestBtn.classList.add('is-active'); 754 } 755 } catch (err) { 756 console.error('Could not switch microphone:', err); 757 alert('Could not switch microphone. Check permissions and try again.'); 758 if (wasTesting) { 759 micTestBtn.textContent = 'Mic Test'; 760 micTestBtn.classList.remove('is-active'); 761 } 762 } 763 } 764 }); 765 766 outputSelect.addEventListener('change', async function () { 767 if (VC && VC.applyOutputDevice) { 768 try { 769 await VC.applyOutputDevice(outputSelect.value); 770 } catch (err) { 771 console.error('Could not switch speaker:', err); 772 } 773 } 774 }); 775 776 if (typeof HTMLMediaElement.prototype.setSinkId !== 'function') { 777 outputSelect.disabled = true; 778 outputCol.title = 'Speaker selection is not supported in this browser.'; 779 } 780 781 const meterBars = micMeter.querySelectorAll('.voice-settings-mic-test__bar'); 782 micTestBtn.addEventListener('click', async function () { 783 if (!VC || !VC.startMicTest) return; 784 if (VC.isMicTestRunning && VC.isMicTestRunning()) { 785 VC.stopMicTest(); 786 micTestBtn.textContent = 'Mic Test'; 787 micTestBtn.classList.remove('is-active');
788 meterBars.forEach(function (bar) { 789 bar.style.height = '4px'; 790 bar.classList.remove('is-active'); 791 }); 792 return; 793 } 794 try { 795 micTestBtn.textContent = 'Stop Test'; 796 micTestBtn.classList.add('is-active'); 797 await VC.startMicTest(function (level) { 798 const activeBars = Math.round(level * meterBars.length); 799 meterBars.forEach(function (bar, index) { 800 if (index < activeBars) { 801 bar.style.height = (4 + Math.round(level * 28)) + 'px'; 802 bar.classList.add('is-active'); 803 } else { 804 bar.style.height = '4px'; 805 bar.classList.remove('is-active'); 806 } 807 }); 808 }); 809 } catch (err) { 810 console.error('Mic test failed:', err); 811 alert('Could not start mic test. Check microphone permissions.'); 812 micTestBtn.textContent = 'Mic Test'; 813 micTestBtn.classList.remove('is-active'); 814 } 815 }); 816 817 return devicesSection; 818 } 819 820 function appendVoiceDeviceSettings(voiceSection) { 821 if (!voiceDeviceSettingsRoot) { 822 voiceDeviceSettingsRoot = buildVoiceDeviceSettingsRoot(); 823 } 824 voiceSection.appendChild(voiceDeviceSettingsRoot); 825 void refreshVoiceDeviceSelects(); 826 } 827 828 function bindSidebarCategoryIcons() { 829 const map = { 830 appearance: ['palette', 'Appearance'], 831 messages: ['comments', 'Messages'], 832 notifications: ['bell', 'Notifications'], 833 voice: ['microphone', 'Voice & Chat'], 834 discord: ['tower-broadcast', 'Discord Status'], 835 advanced: ['sliders', 'Advanced'], 836 moderation: ['shield-halved', 'Moderation'] 837 }; 838 document.querySelectorAll('.settings-category[data-category]').forEach(function (btn) { 839 const entry = map[btn.dataset.category] || ['gear', btn.dataset.category]; 840 btn.innerHTML = settingsFa(entry[0]) + ' <span>' + escapeSettingsHtml(entry[1]) + '</span>'; 841 btn.classList.add('chatroom-settings-category'); 842 }); 843 } 844 845 function dispatchColorModeSync() { 846 document.dispatchEvent(new CustomEvent('pupwife-color-mode', { 847 detail: { 848 colorMode: settings.colorMode, 849 darkMode: settings.colorMode === 'dark', 850 source: 'chatroom-settings' 851 } 852 })); 853 } 854 855 function setColorMode(mode, options) { 856 const opts = options || {}; 857 settings.colorMode = mode === 'light' ? 'light' : 'dark'; 858 pushThemeToSite('chatroom-settings', { colorMode: settings.colorMode }); 859 saveSettings(); 860 const toggle = document.getElementById('setting-color-mode'); 861 if (toggle) { 862 toggle.checked = settings.colorMode === 'dark'; 863 } 864 if (!opts.skipDispatch) { 865 dispatchColorModeSync(); 866 } 867 } 868 869 function getColorMode() { 870 return settings.colorMode === 'light' ? 'light' : 'dark'; 871 } 872 873 /** 874 * Setup settings UI 875 */ 876 function setupUI() { 877 const settingsBtn = document.getElementById('settings-btn'); 878 const settingsModal = document.getElementById('settings-modal'); 879 880 if (settingsBtn) { 881 settingsBtn.addEventListener('click', () => { 882 showSettings(); 883 }); 884 } 885 886 // Close settings on outside click 887 if (settingsModal) { 888 settingsModal.addEventListener('click', (e) => { 889 // Only close if clicking directly on the modal backdrop, not on content 890 if (e.target === settingsModal || e.target.classList.contains('settings-modal')) { 891 hideSettings(); 892 } 893 }); 894 } 895 } 896 897 /** 898 * Rebuild settings shell when an older cached modal is missing newer categories. 899 */ 900 function ensureSettingsModal() { 901 let settingsModal = document.getElementById('settings-modal'); 902 const wantsCitrus = isCitrusOsHost(); 903 const hasCitrusShell = !!(settingsModal && settingsModal.classList.contains('chatroom-settings--citrus')); 904 const hasDiscordCategory = !!document.querySelector('.settings-category[data-category="discord"]'); 905 const needsRebuild = settingsModal && ( 906 (wantsCitrus && !hasCitrusShell) || 907 (!wantsCitrus && hasCitrusShell) || 908 (!wantsCitrus && !hasDiscordCategory) 909 ); 910 if (needsRebuild) { 911 settingsModal.remove(); 912 settingsModal = null; 913 } 914 if (!settingsModal) { 915 createSettingsModal(); 916 settingsModal = document.getElementById('settings-modal'); 917 } 918 return settingsModal;
919 } 920 921 /** 922 * Show settings panel 923 */ 924 function showSettings() { 925 ensureCitrusSettingsCss(); 926 const settingsModal = ensureSettingsModal(); 927 if (settingsModal) { 928 if (isCitrusOsHost()) { 929 settingsModal.hidden = false; 930 settingsModal.style.display = 'flex'; 931 setActiveChatSettingsTab(activeChatSettingsTab); 932 showCategory(activeChatSettingsTab); 933 } else { 934 settingsModal.style.display = 'flex'; 935 } 936 populateSettingsForm(); 937 } 938 939 const colorToggle = document.getElementById('setting-color-mode'); 940 if (colorToggle) { 941 colorToggle.checked = getColorMode() === 'dark'; 942 } 943 944 syncModerationTabVisibility(); 945 946 if (!isCitrusOsHost()) { 947 document.body.classList.add('settings-open'); 948 const mainContent = document.querySelector('.main-content-area'); 949 if (mainContent) { 950 mainContent.classList.add('settings-open'); 951 } 952 const sidebar = document.querySelector('.sidebar'); 953 if (sidebar) { 954 sidebar.classList.add('settings-open'); 955 } 956 } 957 958 // Re-attach close button handler in case modal was reused 959 // Use event delegation on the modal - get the modal again (might be different instance) 960 const modalForClose = document.getElementById('settings-modal'); 961 if (modalForClose) { 962 // Add delegation handler directly (don't clone, just add listener) 963 modalForClose.addEventListener('click', function modalClickHandler(e) { 964 const closeBtn = document.getElementById('chatroom-settings-close'); 965 if (closeBtn && (e.target === closeBtn || closeBtn.contains(e.target))) { 966 e.preventDefault(); 967 e.stopPropagation(); 968 e.stopImmediatePropagation(); 969 emitDevLog('Close button clicked via delegation (showSettings)'); 970 hideSettings(); 971 return false; 972 } 973 }, true); 974 975 // Also attach directly to button 976 setTimeout(() => { 977 const closeBtn = document.getElementById('chatroom-settings-close'); 978 if (closeBtn) { 979 closeBtn.addEventListener('click', function directHandler(e) { 980 e.preventDefault(); 981 e.stopPropagation(); 982 e.stopImmediatePropagation(); 983 emitDevLog('Close button clicked directly (showSettings)'); 984 hideSettings(); 985 return false; 986 }, true); 987 } 988 }, 0); 989 } 990 991 // Add Escape key handler 992 const escapeHandler = (e) => { 993 if (e.key === 'Escape') { 994 hideSettings(); 995 document.removeEventListener('keydown', escapeHandler); 996 } 997 }; 998 document.addEventListener('keydown', escapeHandler); 999 } 1000 1001 /** 1002 * Hide settings panel 1003 */ 1004 function hideSettings() { 1005 if (isCitrusOsHost() && isSettingsInAuxWindow()) { 1006 if (window.ChatroomCitrusMenus && typeof window.ChatroomCitrusMenus.closeAuxPanel === 'function') { 1007 window.ChatroomCitrusMenus.closeAuxPanel('settings'); 1008 return; 1009 } 1010 } 1011 1012 const settingsModal = document.getElementById('settings-modal'); 1013 if (settingsModal) { 1014 settingsModal.style.display = 'none'; 1015 settingsModal.hidden = true; 1016 } 1017 1018 document.body.classList.remove('settings-open'); 1019 const mainContent = document.querySelector('.main-content-area'); 1020 if (mainContent) { 1021 mainContent.classList.remove('settings-open'); 1022 } 1023 const sidebar = document.querySelector('.sidebar'); 1024 if (sidebar) { 1025 sidebar.classList.remove('settings-open'); 1026 } 1027 } 1028 1029 /** 1030 * Create settings modal 1031 */ 1032 function createSettingsModal() { 1033 if (isCitrusOsHost()) {
1034 createCitrusSettingsModal(); 1035 return; 1036 } 1037 createLegacySettingsModal(); 1038 } 1039 1040 function createCitrusSettingsModal() { 1041 const modal = document.createElement('div'); 1042 modal.id = 'settings-modal'; 1043 modal.className = 'settings-modal chatroom-settings--citrus'; 1044 modal.hidden = true; 1045 1046 const shell = document.createElement('div'); 1047 shell.className = 'citrus-settings-window chatroom-settings-shell'; 1048 1049 const tabsRoot = document.createElement('div'); 1050 tabsRoot.className = 'citrus-settings-tabs'; 1051 tabsRoot.setAttribute('data-chatroom-settings-tabs', '1'); 1052 1053 const tabsBody = document.createElement('div'); 1054 tabsBody.className = 'citrus-settings-tabs__body'; 1055 1056 const nav = document.createElement('nav'); 1057 nav.className = 'citrus-settings-tabs__nav'; 1058 nav.setAttribute('role', 'tablist'); 1059 nav.setAttribute('aria-label', 'Chat settings categories'); 1060 1061 const panelsWrap = document.createElement('div'); 1062 panelsWrap.className = 'citrus-settings-tabs__panels'; 1063 1064 CHAT_SETTINGS_TAB_DEFS.forEach(function (def, index) { 1065 const btnId = 'chatroom-settings-tab-' + def.id; 1066 const panelId = 'chatroom-settings-panel-' + def.id; 1067 1068 const btn = document.createElement('button'); 1069 btn.type = 'button'; 1070 btn.className = 'citrus-settings-tabs__tab' + (index === 0 ? ' is-active' : ''); 1071 btn.id = btnId; 1072 btn.setAttribute('data-chatroom-settings-tab', def.id); 1073 btn.setAttribute('role', 'tab'); 1074 btn.setAttribute('aria-controls', panelId); 1075 btn.setAttribute('aria-selected', index === 0 ? 'true' : 'false'); 1076 if (def.modOnly) { 1077 btn.hidden = true; 1078 } 1079 btn.innerHTML = 1080 '<i class="' + def.icon + ' citrus-settings-tabs__tab-icon" aria-hidden="true"></i>' + 1081 '<span class="citrus-settings-tabs__tab-label">' + escapeSettingsHtml(def.label) + '</span>'; 1082 btn.addEventListener('click', function () { 1083 showCategory(def.id); 1084 }); 1085 1086 const tabPanel = document.createElement('div'); 1087 tabPanel.className = 'citrus-settings-tabs__panel' + (index === 0 ? ' is-active' : ''); 1088 tabPanel.id = panelId; 1089 tabPanel.setAttribute('data-chatroom-settings-panel', def.id); 1090 tabPanel.setAttribute('role', 'tabpanel'); 1091 tabPanel.setAttribute('aria-labelledby', btnId); 1092 tabPanel.hidden = index !== 0; 1093 1094 nav.appendChild(btn); 1095 panelsWrap.appendChild(tabPanel); 1096 }); 1097 1098 tabsBody.appendChild(nav); 1099 tabsBody.appendChild(panelsWrap); 1100 tabsRoot.appendChild(tabsBody); 1101 shell.appendChild(tabsRoot); 1102 1103 const scope = document.createElement('div'); 1104 scope.className = 'citrus-window--settings citrus-window--settings-host-proxy'; 1105 scope.appendChild(shell); 1106 modal.appendChild(scope); 1107 document.body.appendChild(modal); 1108 1109 modal.addEventListener('keydown', function (e) { 1110 if (e.key === 'Escape') { 1111 hideSettings(); 1112 } 1113 }); 1114 1115 syncModerationTabVisibility(); 1116 showCategory('appearance'); 1117 } 1118 1119 function createLegacySettingsModal() { 1120 const modal = document.createElement('div'); 1121 modal.id = 'settings-modal'; 1122 modal.className = 'settings-modal'; 1123 modal.innerHTML = ` 1124 <div class="settings-content"> 1125 <div class="settings-header"> 1126 <h2>${window.ChatroomIcons ? window.ChatroomIcons.icon('gear') + ' ' : ''}Settings</h2> 1127 <button class="settings-close cr-icon-btn" id="chatroom-settings-close" type="button" aria-label="Close settings" onclick="if(window.ChatroomSettings){window.ChatroomSettings.hideSettings();} return false;"></button> 1128 </div> 1129 <div class="settings-body"> 1130 <div class="settings-sidebar"> 1131 <button class="settings-category active" data-category="appearance">${window.ChatroomIcons ? window.ChatroomIcons.icon('palette') + ' ' : ''}Appearance</button> 1132 <button class="settings-category" data-category="messages">${window.ChatroomIcons ? window.ChatroomIcons.icon('message') + ' ' : ''}Messages</button> 1133 <button class="settings-category" data-category="notifications">${window.ChatroomIcons ? window.ChatroomIcons.icon('bell') + ' ' : ''}Notifications</button> 1134 <button class="settings-category" data-category="voice">${window.ChatroomIcons ? window.ChatroomIcons.icon('microphone') + ' ' : ''}Voice & Chat</button> 1135 <button class="settings-category" data-category="discord">${window.ChatroomIcons ? '<i class="fa-brands fa-discord fa-width-auto" aria-hidden="true"></i> ' : ''}Discord Status</button> 1136 <button class="settings-category" data-category="advanced">${window.ChatroomIcons ? window.ChatroomIcons.icon('gear') + ' ' : ''}Advanced</button> 1137 <button class="settings-category" data-category="moderation" id="moderation-category-btn" style="display: none;">${window.ChatroomIcons ? window.ChatroomIcons.icon('crown') + ' ' : ''}Moderation</button> 1138 </div> 1139 <div class="settings-panel" id="chatroom-settings-panel"> 1140 <!-- Settings content will be populated here --> 1141 </div> 1142 </div> 1143 </div> 1144 `; 1145 document.body.appendChild(modal); 1146 1147 var chatCloseBtn = document.getElementById('chatroom-settings-close'); 1148 if (chatCloseBtn && window.ChatroomIcons) { 1149 window.ChatroomIcons.set(chatCloseBtn, 'xmark'); 1150 } 1151 1152 // Use event delegation on the modal itself to catch close button clicks 1153 modal.addEventListener('click', function modalClickHandler(e) { 1154 // Check if the click is on the close button or its children 1155 const closeBtn = document.getElementById('chatroom-settings-close'); 1156 if (closeBtn && (e.target === closeBtn || closeBtn.contains(e.target))) { 1157 e.preventDefault(); 1158 e.stopPropagation(); 1159 e.stopImmediatePropagation(); 1160 emitDevLog('Close button clicked via delegation'); 1161 hideSettings(); 1162 return false;
1163 } 1164 }, true); // Use capture phase 1165 1166 // Also attach directly to the button as backup 1167 setTimeout(() => { 1168 const closeBtn = document.getElementById('chatroom-settings-close'); 1169 if (closeBtn) { 1170 closeBtn.addEventListener('click', function directCloseHandler(e) { 1171 e.preventDefault(); 1172 e.stopPropagation(); 1173 e.stopImmediatePropagation(); 1174 emitDevLog('Close button clicked directly'); 1175 hideSettings(); 1176 return false; 1177 }, true); 1178 1179 closeBtn.addEventListener('mousedown', function mousedownHandler(e) { 1180 e.preventDefault(); 1181 e.stopPropagation(); 1182 emitDevLog('Close button mousedown'); 1183 hideSettings(); 1184 return false; 1185 }); 1186 } 1187 }, 0); 1188 1189 // Category buttons 1190 document.querySelectorAll('.settings-category').forEach(btn => { 1191 btn.addEventListener('click', () => { 1192 document.querySelectorAll('.settings-category').forEach(b => b.classList.remove('active')); 1193 btn.classList.add('active'); 1194 showCategory(btn.dataset.category); 1195 }); 1196 }); 1197 1198 showCategory('appearance'); 1199 bindSidebarCategoryIcons(); 1200 populateSettingsForm(); 1201 1202 // Show/hide moderation category based on moderator status 1203 const moderationBtn = document.getElementById('moderation-category-btn'); 1204 if (moderationBtn) { 1205 const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator(); 1206 moderationBtn.style.display = isModerator ? 'block' : 'none'; 1207 } 1208 1209 // Add Escape key handler 1210 const escapeHandler = (e) => { 1211 if (e.key === 'Escape') { 1212 hideSettings(); 1213 document.removeEventListener('keydown', escapeHandler); 1214 } 1215 }; 1216 document.addEventListener('keydown', escapeHandler); 1217 } 1218 1219 /** 1220 * Show settings category 1221 */ 1222 function showCategory(category) { 1223 if (isCitrusOsHost()) {
1224 setActiveChatSettingsTab(category); 1225 } else { 1226 document.querySelectorAll('.settings-category').forEach(function (btn) { 1227 btn.classList.toggle('active', btn.dataset.category === category); 1228 }); 1229 } 1230 1231 const panel = getCategoryPanel(category); 1232 if (!panel) return; 1233 1234 let html = ''; 1235 1236 switch (category) { 1237 case 'appearance': { 1238 panel.innerHTML = ''; 1239 const appearanceSection = createChatroomSettingsSection('Appearance', 'palette'); 1240 appearanceSection.appendChild(createChatroomSelectField({ 1241 id: 'setting-theme', 1242 icon: 'swatchbook', 1243 label: 'Color palette', 1244 value: settings.theme, 1245 options: CHAT_PALETTE_OPTIONS, 1246 onChange: function (val) { 1247 settings.theme = val; 1248 pushThemeToSite('chatroom-settings', { palette: val }); 1249 saveSettings(); 1250 } 1251 })); 1252 1253 const colorModeCard = document.createElement('div'); 1254 colorModeCard.className = 'settings-card chatroom-settings-theme-card'; 1255 colorModeCard.innerHTML = 1256 '<p class="settings-card__label">' + settingsFa('circle-half-stroke') + ' Light / dark</p>' + 1257 '<div class="settings-theme-card' + (settings.colorMode === 'dark' ? ' is-dark' : '') + '" id="chatroom-color-mode-card">' + 1258 '<i class="fa-solid fa-sun settings-theme-card__icon settings-theme-card__icon--sun" aria-hidden="true"></i>' + 1259 '<label class="settings-switch" title="Toggle light or dark mode">' + 1260 '<input type="checkbox" id="setting-color-mode" class="settings-switch__input"' + 1261 (settings.colorMode === 'dark' ? ' checked' : '') + '>' + 1262 '<span class="settings-switch__track" aria-hidden="true"></span></label>' + 1263 '<i class="fa-solid fa-moon settings-theme-card__icon settings-theme-card__icon--moon" aria-hidden="true"></i></div>'; 1264 appearanceSection.appendChild(colorModeCard); 1265 const colorModeInput = colorModeCard.querySelector('#setting-color-mode'); 1266 const colorModeCardEl = colorModeCard.querySelector('#chatroom-color-mode-card'); 1267 if (colorModeInput) { 1268 colorModeInput.addEventListener('change', function (e) { 1269 setColorMode(e.target.checked ? 'dark' : 'light'); 1270 if (colorModeCardEl) { 1271 colorModeCardEl.classList.toggle('is-dark', e.target.checked); 1272 } 1273 }); 1274 } 1275 1276 appearanceSection.appendChild(createChatroomSelectField({ 1277 id: 'setting-font-size', 1278 icon: 'text-height', 1279 label: 'Font size', 1280 value: settings.fontSize, 1281 options: [ 1282 { value: 'small', label: 'Small' }, 1283 { value: 'medium', label: 'Medium' }, 1284 { value: 'large', label: 'Large' } 1285 ], 1286 onChange: function (val) { 1287 settings.fontSize = val; 1288 saveSettings(); 1289 } 1290 })); 1291 appearanceSection.appendChild(createChatroomSelectField({ 1292 id: 'setting-emoji-size', 1293 icon: 'face-smile', 1294 label: 'Emoji size', 1295 value: settings.emojiSize, 1296 options: [ 1297 { value: 'small', label: 'Small' }, 1298 { value: 'medium', label: 'Medium' }, 1299 { value: 'large', label: 'Large' } 1300 ], 1301 onChange: function (val) { 1302 settings.emojiSize = val; 1303 saveSettings(); 1304 } 1305 })); 1306 appearanceSection.appendChild(createChatroomToggle({ 1307 id: 'setting-animations', 1308 icon: 'wand-magic-sparkles', 1309 label: 'Enable animations', 1310 checked: settings.animations === true, 1311 onChange: function (val) { 1312 settings.animations = val; 1313 saveSettings(); 1314 } 1315 })); 1316 panel.appendChild(appearanceSection); 1317 return; 1318 } 1319 1320 case 'messages': { 1321 panel.innerHTML = ''; 1322 const messagesSection = createChatroomSettingsSection('Messages', 'comments'); 1323 messagesSection.appendChild(createChatroomToggle({ 1324 id: 'setting-message-grouping', 1325 icon: 'layer-group', 1326 label: 'Group consecutive messages', 1327 checked: settings.messageGrouping === true, 1328 hint: 'Hides repeat avatars and names when the same person sends back-to-back messages.', 1329 onChange: function (val) { 1330 settings.messageGrouping = val; 1331 saveSettings(); 1332 } 1333 })); 1334 messagesSection.appendChild(createChatroomToggle({ 1335 id: 'setting-show-timestamps', 1336 icon: 'clock', 1337 label: 'Show timestamps', 1338 checked: settings.showTimestamps === true, 1339 onChange: function (val) { 1340 settings.showTimestamps = val; 1341 saveSettings(); 1342 } 1343 })); 1344 messagesSection.appendChild(createChatroomToggle({ 1345 id: 'setting-show-avatars', 1346 icon: 'circle-user', 1347 label: 'Show avatars', 1348 checked: settings.showAvatars === true, 1349 onChange: function (val) { 1350 settings.showAvatars = val; 1351 saveSettings(); 1352 } 1353 })); 1354 messagesSection.appendChild(createChatroomToggle({ 1355 id: 'setting-enter-to-send', 1356 icon: 'keyboard', 1357 label: 'Enter to send', 1358 checked: settings.enterToSend !== false, 1359 hint: 'On: Enter sends, Shift+Enter adds a line. Off: Shift+Enter sends.', 1360 onChange: function (val) { 1361 settings.enterToSend = val; 1362 saveSettings(); 1363 } 1364 })); 1365 messagesSection.appendChild(createChatroomToggle({ 1366 id: 'setting-auto-scroll', 1367 icon: 'arrow-down', 1368 label: 'Auto-scroll to new messages', 1369 checked: settings.autoScrollNewMessages !== false, 1370 onChange: function (val) { 1371 settings.autoScrollNewMessages = val; 1372 saveSettings(); 1373 } 1374 })); 1375 messagesSection.appendChild(createChatroomToggle({ 1376 id: 'setting-link-previews', 1377 icon: 'link', 1378 label: 'Show link previews', 1379 checked: settings.showLinkPreviews === true, 1380 onChange: function (val) { 1381 settings.showLinkPreviews = val; 1382 saveSettings(); 1383 } 1384 })); 1385 panel.appendChild(messagesSection); 1386 return; 1387 } 1388 1389 case 'voice': { 1390 panel.innerHTML = ''; 1391 const voiceSection = createChatroomSettingsSection('Voice & chat', 'microphone'); 1392 voiceSection.appendChild(createChatroomToggle({ 1393 id: 'setting-mute-on-join', 1394 icon: 'microphone-slash', 1395 label: 'Mute mic when joining voice', 1396 checked: settings.muteOnJoinVoice === true, 1397 hint: 'Start muted when you join a voice channel.', 1398 onChange: function (val) { 1399 settings.muteOnJoinVoice = val; 1400 saveSettings(); 1401 } 1402 })); 1403 voiceSection.appendChild(createChatroomToggle({ 1404 id: 'setting-join-leave-sounds', 1405 icon: 'volume-high', 1406 label: 'Join / leave sounds', 1407 checked: settings.playJoinLeaveSounds !== false,
1408 hint: 'Play a sound when someone joins or leaves voice.', 1409 onChange: function (val) { 1410 settings.playJoinLeaveSounds = val; 1411 saveSettings(); 1412 } 1413 })); 1414 voiceSection.appendChild(createChatroomToggle({ 1415 id: 'setting-confirm-leave-voice', 1416 icon: 'circle-question', 1417 label: 'Confirm before leaving voice', 1418 checked: settings.confirmBeforeLeavingVoice !== false, 1419 onChange: function (val) { 1420 settings.confirmBeforeLeavingVoice = val; 1421 saveSettings(); 1422 } 1423 })); 1424 voiceSection.appendChild(createChatroomToggle({ 1425 id: 'setting-online-status', 1426 icon: 'signal', 1427 label: 'Show online status', 1428 checked: settings.showOnlineStatus !== false, 1429 onChange: function (val) { 1430 settings.showOnlineStatus = val; 1431 saveSettings(); 1432 } 1433 })); 1434 panel.appendChild(voiceSection); 1435 const deviceSection = createChatroomSettingsSection('Audio devices', 'headphones'); 1436 panel.appendChild(deviceSection); 1437 appendVoiceDeviceSettings(deviceSection); 1438 return; 1439 } 1440 1441 case 'notifications': { 1442 panel.innerHTML = ''; 1443 const notifSection = createChatroomSettingsSection('Notifications', 'bell'); 1444 notifSection.appendChild(createChatroomToggle({ 1445 id: 'setting-notifications', 1446 icon: 'bell', 1447 label: 'Enable notifications', 1448 checked: settings.notifications === true, 1449 onChange: function (val) { 1450 settings.notifications = val; 1451 saveSettings(); 1452 if (window.ChatroomNotifications && window.ChatroomNotifications.updateSettings) { 1453 window.ChatroomNotifications.updateSettings(val); 1454 } 1455 } 1456 })); 1457 notifSection.appendChild(createChatroomToggle({ 1458 id: 'setting-desktop-notifications', 1459 icon: 'desktop', 1460 label: 'Desktop notifications', 1461 checked: settings.desktopNotifications === true, 1462 hint: 'System notifications when tabbed out or on other pages.', 1463 onChange: async function (val) { 1464 if (val && typeof Notification !== 'undefined' && Notification.permission === 'default') { 1465 const permission = await Notification.requestPermission(); 1466 if (permission !== 'granted') { 1467 settings.desktopNotifications = false; 1468 const el = document.getElementById('setting-desktop-notifications'); 1469 if (el) el.checked = false; 1470 return; 1471 } 1472 } 1473 settings.desktopNotifications = !!val; 1474 saveSettings(); 1475 if (window.ChatroomNotifications && window.ChatroomNotifications.updateDesktopSetting) { 1476 window.ChatroomNotifications.updateDesktopSetting(settings.desktopNotifications); 1477 } 1478 } 1479 })); 1480 notifSection.appendChild(createChatroomToggle({ 1481 id: 'setting-sound-effects', 1482 icon: 'volume-high', 1483 label: 'Sound effects', 1484 checked: settings.soundEffects === true, 1485 onChange: function (val) { 1486 settings.soundEffects = val; 1487 saveSettings(); 1488 } 1489 })); 1490 panel.appendChild(notifSection); 1491 return; 1492 } 1493 1494 case 'discord': { 1495 panel.innerHTML = ''; 1496 const discordSection = createChatroomSettingsSection('Discord status (Lanyard)', 'tower-broadcast'); 1497 const help = document.createElement('p'); 1498 help.className = 'settings-discord-help'; 1499 help.innerHTML = 1500 'Show your live Discord custom status and activity in the chatroom. ' + 1501 'You must join the <a href="https://discord.gg/lanyard" target="_blank" rel="noopener noreferrer">Lanyard Discord server</a> ' + 1502 'and keep a visible status on Discord. ' + 1503 '<a href="https://discord.com/developers/applications" target="_blank" rel="noopener noreferrer">Developer mode</a>: right-click your profile, Copy User ID.'; 1504 discordSection.appendChild(help); 1505 1506 discordSection.appendChild(createChatroomToggle({ 1507 id: 'setting-use-lanyard', 1508 icon: 'tower-broadcast', 1509 label: 'Use Discord status in chatroom', 1510 checked: settings.useLanyardStatus === true, 1511 hint: 'Replaces your chatroom status line with Lanyard when connected.', 1512 onChange: function (val) { 1513 settings.useLanyardStatus = val; 1514 saveSettings(); 1515 } 1516 })); 1517 1518 const idWrap = document.createElement('div'); 1519 idWrap.className = 'settings-card chatroom-settings-discord-id'; 1520 idWrap.innerHTML = 1521 '<label class="settings-card__label" for="setting-discord-user-id">' + 1522 settingsFa('id-card') + ' Discord user ID</label>'; 1523 const idInput = document.createElement('input'); 1524 idInput.type = 'text'; 1525 idInput.id = 'setting-discord-user-id'; 1526 idInput.className = 'chatroom-input'; 1527 idInput.inputMode = 'numeric'; 1528 idInput.autocomplete = 'off'; 1529 idInput.placeholder = 'e.g. 905482133257224232'; 1530 idInput.value = settings.discordUserId || ''; 1531 idInput.addEventListener('change', function () { 1532 settings.discordUserId = idInput.value.replace(/\D/g, ''); 1533 idInput.value = settings.discordUserId; 1534 saveSettings(); 1535 });
1536 idWrap.appendChild(idInput); 1537 1538 const previewBtn = document.createElement('button'); 1539 previewBtn.type = 'button'; 1540 previewBtn.className = 'settings-mod-btn settings-mod-btn--primary'; 1541 previewBtn.textContent = 'Test connection'; 1542 const previewOut = document.createElement('p'); 1543 previewOut.className = 'settings-discord-preview'; 1544 previewOut.setAttribute('aria-live', 'polite'); 1545 previewBtn.addEventListener('click', function () { 1546 const id = idInput.value.replace(/\D/g, ''); 1547 if (id.length < 17) { 1548 previewOut.textContent = 'Enter a valid Discord user ID (17 to 20 digits).'; 1549 return; 1550 } 1551 previewOut.textContent = 'Checking Lanyardâ¦'; 1552 if (window.ChatroomLanyard && window.ChatroomLanyard.previewDiscordId) { 1553 window.ChatroomLanyard.previewDiscordId(id).then(function (res) { 1554 previewOut.textContent = res.message || (res.ok ? 'Connected.' : 'No data.'); 1555 }); 1556 } else { 1557 previewOut.textContent = 'Lanyard module not loaded yet.'; 1558 } 1559 }); 1560 idWrap.appendChild(previewBtn); 1561 idWrap.appendChild(previewOut); 1562 discordSection.appendChild(idWrap); 1563 panel.appendChild(discordSection); 1564 return; 1565 } 1566 1567 case 'advanced': 1568 panel.innerHTML = '<h3>' + (window.ChatroomIcons ? window.ChatroomIcons.icon('gear') + ' ' : '') + 'Advanced</h3>'; 1569 1570 // Message Cleanup Section 1571 const cleanupItem = document.createElement('div'); 1572 cleanupItem.className = 'setting-item'; 1573 cleanupItem.style.marginTop = 'var(--gap)'; 1574 cleanupItem.style.paddingTop = 'var(--gap)'; 1575 cleanupItem.style.borderTop = 'var(--border-thickness) solid var(--border)'; 1576 1577 cleanupItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">ðï¸ Message Cleanup</label>'; 1578 1579 const cleanupDesc = document.createElement('small'); 1580 cleanupDesc.textContent = 'Automatically delete old messages and images to save Firebase storage. Messages older than the selected period will be permanently deleted.'; 1581 cleanupDesc.style.display = 'block'; 1582 cleanupDesc.style.marginTop = '4px'; 1583 cleanupDesc.style.marginBottom = '12px'; 1584 cleanupItem.appendChild(cleanupDesc); 1585 1586 // Retention period selector 1587 const retentionLabel = document.createElement('label'); 1588 retentionLabel.textContent = 'Keep messages for:'; 1589 retentionLabel.style.display = 'block'; 1590 retentionLabel.style.marginBottom = '8px'; 1591 cleanupItem.appendChild(retentionLabel); 1592 1593 const retentionSelect = document.createElement('select'); 1594 retentionSelect.id = 'retention-days-select'; 1595 retentionSelect.className = 'setting-item select'; 1596 retentionSelect.style.width = '100%'; 1597 retentionSelect.style.padding = '8px'; 1598 retentionSelect.style.background = 'var(--bg-2)'; 1599 retentionSelect.style.border = 'var(--border-thickness) solid var(--border)'; 1600 retentionSelect.style.color = 'var(--text-2)'; 1601 retentionSelect.style.fontFamily = 'var(--font)'; 1602 retentionSelect.style.fontSize = '14px'; 1603 retentionSelect.style.marginBottom = '12px'; 1604 1605 const retentionOptions = [ 1606 { value: 3, text: '3 days' }, 1607 { value: 7, text: '1 week (recommended)' }, 1608 { value: 14, text: '2 weeks' }, 1609 { value: 30, text: '1 month' }, 1610 { value: 60, text: '2 months' }, 1611 { value: 90, text: '3 months' } 1612 ]; 1613 1614 const currentRetention = window.ChatroomMessageCleanup 1615 ? window.ChatroomMessageCleanup.getRetentionDays() 1616 : 7; 1617 1618 retentionOptions.forEach(opt => { 1619 const option = document.createElement('option'); 1620 option.value = opt.value; 1621 option.textContent = opt.text; 1622 if (opt.value === currentRetention) { 1623 option.selected = true; 1624 } 1625 retentionSelect.appendChild(option); 1626 }); 1627 1628 retentionSelect.addEventListener('change', () => { 1629 const days = parseInt(retentionSelect.value, 10); 1630 if (window.ChatroomMessageCleanup) { 1631 window.ChatroomMessageCleanup.setRetentionDays(days); 1632 // Show feedback 1633 const feedback = document.createElement('div'); 1634 feedback.textContent = `â Retention period set to ${days} day${days !== 1 ? 's' : ''}`; 1635 feedback.style.color = 'var(--green-2)'; 1636 feedback.style.fontSize = '12px'; 1637 feedback.style.marginTop = '4px'; 1638 cleanupItem.appendChild(feedback); 1639 setTimeout(() => { 1640 feedback.remove(); 1641 }, 3000); 1642 } 1643 }); 1644 1645 cleanupItem.appendChild(retentionSelect); 1646 1647 // Manual cleanup button 1648 const cleanupBtn = document.createElement('button'); 1649 cleanupBtn.textContent = 'Run Cleanup Now'; 1650 cleanupBtn.style.padding = '8px 16px'; 1651 cleanupBtn.style.background = 'var(--accent-2)'; 1652 cleanupBtn.style.color = 'var(--text-0)'; 1653 cleanupBtn.style.border = 'var(--border-thickness) solid var(--accent-3)'; 1654 cleanupBtn.style.cursor = 'pointer'; 1655 cleanupBtn.style.fontFamily = 'var(--font)'; 1656 cleanupBtn.style.fontSize = '13px'; 1657 cleanupBtn.style.transition = 'all 0.2s ease'; 1658 cleanupBtn.style.marginRight = '8px'; 1659 cleanupBtn.addEventListener('click', () => { 1660 if (window.ChatroomMessageCleanup) { 1661 cleanupBtn.disabled = true; 1662 cleanupBtn.textContent = 'Cleaning up...'; 1663 window.ChatroomMessageCleanup.manualCleanup(); 1664 setTimeout(() => { 1665 cleanupBtn.disabled = false;
1666 cleanupBtn.textContent = 'Run Cleanup Now'; 1667 }, 5000); 1668 } 1669 }); 1670 cleanupBtn.addEventListener('mouseenter', () => { 1671 if (!cleanupBtn.disabled) { 1672 cleanupBtn.style.background = 'var(--accent-3)'; 1673 } 1674 }); 1675 cleanupBtn.addEventListener('mouseleave', () => { 1676 if (!cleanupBtn.disabled) { 1677 cleanupBtn.style.background = 'var(--accent-2)'; 1678 } 1679 }); 1680 cleanupItem.appendChild(cleanupBtn); 1681 1682 // Status display 1683 const statusDiv = document.createElement('div'); 1684 statusDiv.id = 'cleanup-status'; 1685 statusDiv.style.marginTop = '8px'; 1686 statusDiv.style.fontSize = '12px'; 1687 statusDiv.style.color = 'var(--text-4)'; 1688 if (window.ChatroomMessageCleanup) { 1689 const status = window.ChatroomMessageCleanup.getStatus(); 1690 if (status.lastCleanupTime) { 1691 const lastCleanup = new Date(status.lastCleanupTime); 1692 statusDiv.textContent = `Last cleanup: ${lastCleanup.toLocaleString()}`; 1693 } else { 1694 statusDiv.textContent = 'Cleanup runs automatically every hour'; 1695 } 1696 } 1697 cleanupItem.appendChild(statusDiv); 1698 1699 panel.appendChild(cleanupItem); 1700 1701 const resetItem = document.createElement('div'); 1702 resetItem.className = 'setting-item'; 1703 const resetBtn = window.ChatroomButton ? window.ChatroomButton.create({ 1704 text: 'Reset All Settings', 1705 variant: 'danger', 1706 onClick: () => { 1707 if (confirm('Are you sure you want to reset all settings to default?')) { 1708 settings = { 1709 theme: 'default', 1710 colorMode: 'dark', 1711 messageGrouping: true, 1712 showTimestamps: true, 1713 showAvatars: true, 1714 fontSize: 'medium', 1715 emojiSize: 'medium', 1716 animations: true, 1717 soundEffects: false, 1718 notifications: false, 1719 desktopNotifications: false, 1720 enterToSend: true, 1721 muteOnJoinVoice: false, 1722 playJoinLeaveSounds: true, 1723 autoScrollNewMessages: true, 1724 showLinkPreviews: false, 1725 showOnlineStatus: true, 1726 confirmBeforeLeavingVoice: true 1727 }; 1728 saveSettings(); 1729 showCategory(category); // Refresh current category 1730 } 1731 } 1732 }) : document.createElement('button'); 1733 if (!window.ChatroomButton) { 1734 resetBtn.className = 'setting-reset-btn'; 1735 resetBtn.textContent = 'Reset All Settings'; 1736 resetBtn.id = 'reset-settings-btn'; 1737 } 1738 resetItem.appendChild(resetBtn); 1739 panel.appendChild(resetItem); 1740 1741 return; 1742 break; 1743 1744 case 'moderation': 1745 // Only show if user is moderator 1746 const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator(); 1747 if (!isModerator) { 1748 panel.innerHTML = '<h3>ð Moderation</h3><p>You do not have moderator privileges.</p>'; 1749 return; 1750 } 1751 1752 panel.innerHTML = '<h3>ð Moderation</h3>'; 1753 1754 // Delete Message section 1755 const deleteMessageItem = document.createElement('div'); 1756 deleteMessageItem.className = 'setting-item';
1757 deleteMessageItem.style.marginBottom = 'var(--gap)'; 1758 deleteMessageItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">ðï¸ Delete Messages</label>'; 1759 1760 const deleteMessageDesc = document.createElement('small'); 1761 deleteMessageDesc.textContent = 'Right-click a message (or use ⯠on hover) â Delete. You can delete your own messages; moderators can delete any message.'; 1762 deleteMessageDesc.style.display = 'block'; 1763 deleteMessageDesc.style.marginTop = '4px'; 1764 deleteMessageDesc.style.color = 'var(--text-4)'; 1765 deleteMessageItem.appendChild(deleteMessageDesc); 1766 panel.appendChild(deleteMessageItem); 1767 1768 // Create Channel section 1769 const createChannelItem = document.createElement('div'); 1770 createChannelItem.className = 'setting-item'; 1771 createChannelItem.style.marginBottom = 'var(--gap)'; 1772 createChannelItem.style.paddingTop = 'var(--gap)'; 1773 createChannelItem.style.borderTop = 'var(--border-thickness) solid var(--border)'; 1774 createChannelItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">â Create Channel</label>'; 1775 1776 const createChannelDesc = document.createElement('small'); 1777 createChannelDesc.textContent = 'Create a new text channel with a custom name. Channel names can contain letters, numbers, underscores, and hyphens.'; 1778 createChannelDesc.style.display = 'block'; 1779 createChannelDesc.style.marginTop = '4px'; 1780 createChannelDesc.style.marginBottom = '12px'; 1781 createChannelDesc.style.color = 'var(--text-4)'; 1782 createChannelItem.appendChild(createChannelDesc); 1783 1784 // Channel name input 1785 const channelNameLabel = document.createElement('label'); 1786 channelNameLabel.textContent = 'Channel Name:'; 1787 channelNameLabel.style.display = 'block'; 1788 channelNameLabel.style.marginBottom = '8px'; 1789 createChannelItem.appendChild(channelNameLabel); 1790 1791 const channelNameInput = document.createElement('input'); 1792 channelNameInput.type = 'text'; 1793 channelNameInput.id = 'mod-create-channel-name'; 1794 channelNameInput.placeholder = 'e.g., general, random, announcements'; 1795 channelNameInput.style.width = '100%'; 1796 channelNameInput.style.padding = '8px'; 1797 channelNameInput.style.background = 'var(--bg-2)'; 1798 channelNameInput.style.border = 'var(--border-thickness) solid var(--border)'; 1799 channelNameInput.style.color = 'var(--text-2)'; 1800 channelNameInput.style.fontFamily = 'var(--font)'; 1801 channelNameInput.style.fontSize = '14px'; 1802 channelNameInput.style.marginBottom = '12px'; 1803 channelNameInput.addEventListener('keydown', (e) => { 1804 if (e.key === 'Enter') { 1805 e.preventDefault(); 1806 const createBtn = document.getElementById('mod-create-channel-btn'); 1807 if (createBtn) createBtn.click(); 1808 } 1809 }); 1810 createChannelItem.appendChild(channelNameInput); 1811 1812 // Channel topic input (optional) 1813 const channelTopicLabel = document.createElement('label'); 1814 channelTopicLabel.textContent = 'Channel Topic (optional):'; 1815 channelTopicLabel.style.display = 'block'; 1816 channelTopicLabel.style.marginBottom = '8px'; 1817 createChannelItem.appendChild(channelTopicLabel); 1818 1819 const channelTopicInput = document.createElement('input'); 1820 channelTopicInput.type = 'text'; 1821 channelTopicInput.id = 'mod-create-channel-topic'; 1822 channelTopicInput.placeholder = 'e.g., General discussion'; 1823 channelTopicInput.style.width = '100%'; 1824 channelTopicInput.style.padding = '8px'; 1825 channelTopicInput.style.background = 'var(--bg-2)'; 1826 channelTopicInput.style.border = 'var(--border-thickness) solid var(--border)'; 1827 channelTopicInput.style.color = 'var(--text-2)'; 1828 channelTopicInput.style.fontFamily = 'var(--font)'; 1829 channelTopicInput.style.fontSize = '14px'; 1830 channelTopicInput.style.marginBottom = '12px'; 1831 channelTopicInput.addEventListener('keydown', (e) => { 1832 if (e.key === 'Enter') { 1833 e.preventDefault(); 1834 const createBtn = document.getElementById('mod-create-channel-btn'); 1835 if (createBtn) createBtn.click(); 1836 } 1837 }); 1838 createChannelItem.appendChild(channelTopicInput); 1839 1840 // Create channel button 1841 const createChannelBtn = document.createElement('button'); 1842 createChannelBtn.id = 'mod-create-channel-btn'; 1843 createChannelBtn.className = 'settings-mod-btn settings-mod-btn--primary'; 1844 createChannelBtn.textContent = 'Create Channel'; 1845 createChannelBtn.addEventListener('click', () => { 1846 const name = channelNameInput.value.trim(); 1847 const topic = channelTopicInput.value.trim(); 1848 1849 if (!name) { 1850 alert('Please enter a channel name.'); 1851 return; 1852 } 1853 1854 // Validate channel name (same as in chatroom.js) 1855 const cleanName = name.toLowerCase().replace(/[^a-z0-9_-]/g, ''); 1856 if (cleanName.length < 1 || cleanName.length > 50) { 1857 alert('Channel name must be 1-50 characters (letters, numbers, _, - only)'); 1858 return; 1859 } 1860 1861 // Check if channel already exists 1862 if (firebase && firebase.database) { 1863 const channelsRef = firebase.database().ref(`chatroom/channels/${cleanName}/metadata`); 1864 channelsRef.once('value', (snapshot) => { 1865 if (snapshot.exists()) { 1866 alert('A channel with this name already exists.'); 1867 return; 1868 } 1869 1870 // Create the channel 1871 const currentUsername = window.__sharedUsername ? window.__sharedUsername.get() : 'system'; 1872 channelsRef.set({ 1873 name: name, 1874 topic: topic || '', 1875 created: Date.now(), 1876 createdBy: currentUsername 1877 }).then(() => { 1878 // Add new channel to the global order 1879 const orderRef = firebase.database().ref('chatroom/channelOrder'); 1880 orderRef.once('value', (snapshot) => { 1881 let currentOrder = []; 1882 if (snapshot.exists()) { 1883 currentOrder = snapshot.val() || []; 1884 } 1885 // Add new channel to the end of the order 1886 if (!currentOrder.includes(cleanName)) { 1887 currentOrder.push(cleanName); 1888 orderRef.set(currentOrder).catch(err => { 1889 console.error('Error updating channel order:', err); 1890 }); 1891 } 1892 }); 1893 1894 // Clear inputs 1895 channelNameInput.value = ''; 1896 channelTopicInput.value = ''; 1897 1898 // Show success message 1899 const successMsg = document.createElement('div'); 1900 successMsg.textContent = `â Channel "${name}" created successfully!`; 1901 successMsg.style.color = 'var(--green-2)'; 1902 successMsg.style.fontSize = '12px'; 1903 successMsg.style.marginTop = '8px'; 1904 createChannelItem.appendChild(successMsg); 1905 setTimeout(() => { 1906 successMsg.remove(); 1907 }, 3000); 1908 1909 // Switch to the new channel 1910 if (window.Chatroom && window.Chatroom.loadChannel) { 1911 setTimeout(() => { 1912 window.Chatroom.loadChannel(cleanName); 1913 }, 500); 1914 } 1915 }).catch(error => { 1916 console.error('Error creating channel:', error); 1917 alert('Error creating channel. You may not have permission.'); 1918 }); 1919 }); 1920 } 1921 }); 1922 createChannelItem.appendChild(createChannelBtn); 1923 1924 panel.appendChild(createChannelItem); 1925 1926 // Delete Channel section 1927 const deleteChannelItem = document.createElement('div'); 1928 deleteChannelItem.className = 'setting-item';
1929 deleteChannelItem.style.marginBottom = 'var(--gap)'; 1930 deleteChannelItem.style.paddingTop = 'var(--gap)'; 1931 deleteChannelItem.style.borderTop = 'var(--border-thickness) solid var(--border)'; 1932 deleteChannelItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">ð Delete Channels</label>'; 1933 1934 const deleteChannelDesc = document.createElement('small'); 1935 deleteChannelDesc.textContent = 'Delete channels that are no longer needed. The general channel cannot be deleted.'; 1936 deleteChannelDesc.style.display = 'block'; 1937 deleteChannelDesc.style.marginTop = '4px'; 1938 deleteChannelDesc.style.marginBottom = '12px'; 1939 deleteChannelDesc.style.color = 'var(--text-4)'; 1940 deleteChannelItem.appendChild(deleteChannelDesc); 1941 1942 // Channel selector 1943 const channelLabel = document.createElement('label'); 1944 channelLabel.textContent = 'Select channel to delete:'; 1945 channelLabel.style.display = 'block'; 1946 channelLabel.style.marginBottom = '8px'; 1947 deleteChannelItem.appendChild(channelLabel); 1948 1949 const channelSelect = document.createElement('select'); 1950 channelSelect.id = 'mod-delete-channel-select'; 1951 channelSelect.style.width = '100%'; 1952 channelSelect.style.padding = '8px'; 1953 channelSelect.style.background = 'var(--bg-2)'; 1954 channelSelect.style.border = 'var(--border-thickness) solid var(--border)'; 1955 channelSelect.style.color = 'var(--text-2)'; 1956 channelSelect.style.fontFamily = 'var(--font)'; 1957 channelSelect.style.fontSize = '14px'; 1958 channelSelect.style.marginBottom = '12px'; 1959 1960 // Populate channels 1961 if (firebase && firebase.database) { 1962 const channelsRef = firebase.database().ref('chatroom/channels'); 1963 channelsRef.once('value', (snapshot) => { 1964 const channels = snapshot.val(); 1965 if (channels) { 1966 const defaultOption = document.createElement('option'); 1967 defaultOption.value = ''; 1968 defaultOption.textContent = '-- Select a channel --'; 1969 channelSelect.appendChild(defaultOption); 1970 1971 Object.keys(channels).forEach(channelId => { 1972 if (channelId === 'general') return; // Skip general 1973 const metadata = channels[channelId].metadata; 1974 if (metadata) { 1975 const option = document.createElement('option'); 1976 option.value = channelId; 1977 option.textContent = `# ${metadata.name || channelId}`; 1978 channelSelect.appendChild(option); 1979 } 1980 }); 1981 } 1982 }); 1983 } 1984 1985 deleteChannelItem.appendChild(channelSelect); 1986 1987 // Delete channel button 1988 const deleteChannelBtn = document.createElement('button'); 1989 deleteChannelBtn.className = 'settings-mod-btn settings-mod-btn--danger'; 1990 deleteChannelBtn.textContent = 'Delete Channel'; 1991 deleteChannelBtn.addEventListener('click', () => { 1992 const channelId = channelSelect.value; 1993 if (!channelId) { 1994 alert('Please select a channel to delete.'); 1995 return; 1996 } 1997 if (window.ChatroomModerator && window.ChatroomModerator.deleteChannel) { 1998 window.ChatroomModerator.deleteChannel(channelId); 1999 // Refresh channel list after deletion 2000 setTimeout(() => { 2001 if (firebase && firebase.database) { 2002 const channelsRef = firebase.database().ref('chatroom/channels'); 2003 channelsRef.once('value', (snapshot) => { 2004 const channels = snapshot.val(); 2005 channelSelect.innerHTML = '<option value="">-- Select a channel --</option>'; 2006 if (channels) { 2007 Object.keys(channels).forEac
2007h(chId => { 2008 if (chId === 'general') return; 2009 const metadata = channels[chId].metadata; 2010 if (metadata) { 2011 const option = document.createElement('option'); 2012 option.value = chId; 2013 option.textContent = `# ${metadata.name || chId}`; 2014 channelSelect.appendChild(option); 2015 } 2016 }); 2017 } 2018 }); 2019 } 2020 }, 1000); 2021 } 2022 }); 2023 deleteChannelItem.appendChild(deleteChannelBtn); 2024 2025 panel.appendChild(deleteChannelItem); 2026 2027 // Restore Default Channels section 2028 const restoreChannelsItem = document.createElement('div'); 2029 restoreChannelsItem.className = 'setting-item'; 2030 restoreChannelsItem.style.marginBottom = 'var(--gap)'; 2031 restoreChannelsItem.style.paddingTop = 'var(--gap)'; 2032 restoreChannelsItem.style.borderTop = 'var(--border-thickness) solid var(--border)'; 2033 restoreChannelsItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">ð§ Restore Default Channels</label>'; 2034 2035 const restoreDesc = document.createElement('small'); 2036 restoreDesc.textContent = 'Restore the default channels (general, whimsical-chatting) if they are missing.'; 2037 restoreDesc.style.display = 'block'; 2038 restoreDesc.style.marginTop = '4px'; 2039 restoreDesc.style.marginBottom = '12px'; 2040 restoreDesc.style.color = 'var(--text-4)'; 2041 restoreChannelsItem.appendChild(restoreDesc); 2042 2043 const restoreBtn = document.createElement('button'); 2044 restoreBtn.textContent = 'Restore Default Channels'; 2045 restoreBtn.style.padding = '8px 16px'; 2046 restoreBtn.style.background = 'var(--accent-2)'; 2047 restoreBtn.style.color = 'var(--text-0)'; 2048 restoreBtn.style.border = 'var(--border-thickness) solid var(--accent-3)'; 2049 restoreBtn.style.cursor = 'pointer'; 2050 restoreBtn.style.fontFamily = 'var(--font)'; 2051 restoreBtn.style.fontSize = '13px'; 2052 restoreBtn.style.transition = 'all 0.2s ease'; 2053 restoreBtn.addEventListener('click', () => { 2054 if (!firebase || !firebase.database) { 2055 alert('Firebase not available.'); 2056 return; 2057 } 2058 2059 restoreBtn.disabled = true; 2060 restoreBtn.textContent = 'Restoring...'; 2061 2062 const channelsRef = firebase.database().ref('chatroom/channels'); 2063 const orderRef = firebase.database().ref('chatroom/channelOrder'); 2064 2065 // Restore general channel 2066 const generalPromise = channelsRef.child('general').child('metadata').once('value').then(snapshot => { 2067 if (!snapshot.exists()) { 2068 return channelsRef.child('general').child('metadata').set({ 2069 name: 'general', 2070 topic: 'Welcome to puppy\'s chatroom!', 2071 created: Date.now(), 2072 createdBy: 'system' 2073 }); 2074 } 2075 }); 2076 2077 // Restore whimsical-chatting channel 2078 const whimsicalPromise = channelsRef.child('whimsical-chatting').child('metadata').once('value').then(snapshot => { 2079 if (!snapshot.exists()) { 2080 return channelsRef.child('whimsical-chatting').child('metadata').set({ 2081 name: 'whimsical chatting', 2082 topic: 'Text chat for Whimsy Zone voice channel', 2083 created: Date.now(), 2084 createdBy: 'system', 2085 type: 'voice-text' 2086 }); 2087 } 2088 }); 2089 2090 Promise.all([generalPromise, whimsicalPromise]).then(() => { 2091 // Update channel order to include these channels 2092 orderRef.once('value', (snapshot) => { 2093 let currentOrder = []; 2094 if (snapshot.exists()) { 2095 currentOrder = snapshot.val() || []; 2096 } 2097 2098 // Add channels if they're not in the order 2099 if (!currentOrder.includes('general')) { 2100 currentOrder.unshift('general'); // Add at the beginning 2101 } 2102 if (!currentOrder.includes('whimsical-chatting')) { 2103 currentOrder.push('whimsical-chatting'); // Add at the end 2104 } 2105 2106 return orderRef.set(currentOrder); 2107 }).then(() => { 2108 restoreBtn.disabled = false;
2109 restoreBtn.textContent = 'â Channels Restored!'; 2110 restoreBtn.style.background = 'var(--green-2)'; 2111 2112 setTimeout(() => { 2113 restoreBtn.textContent = 'Restore Default Channels'; 2114 restoreBtn.style.background = 'var(--accent-2)'; 2115 }, 3000); 2116 2117 // Refresh channels 2118 if (window.Chatroom && window.Chatroom.setupChannels) { 2119 setTimeout(() => { 2120 window.Chatroom.setupChannels(); 2121 }, 1000); 2122 } else { 2123 // Fallback: reload page 2124 setTimeout(() => { 2125 window.location.reload(); 2126 }, 2000); 2127 } 2128 }).catch(err => { 2129 console.error('Error updating channel order:', err); 2130 restoreBtn.disabled = false; 2131 restoreBtn.textContent = 'Restore Default Channels'; 2132 alert('Channels restored but error updating order. Please refresh the page.'); 2133 }); 2134 }).catch(err => { 2135 console.error('Error restoring channels:', err); 2136 restoreBtn.disabled = false; 2137 restoreBtn.textContent = 'Restore Default Channels'; 2138 alert('Error restoring channels: ' + err.message); 2139 }); 2140 }); 2141 restoreBtn.addEventListener('mouseenter', () => { 2142 if (!restoreBtn.disabled) { 2143 restoreBtn.style.background = 'var(--accent-3)'; 2144 } 2145 }); 2146 restoreBtn.addEventListener('mouseleave', () => { 2147 if (!restoreBtn.disabled && restoreBtn.textContent === 'Restore Default Channels') { 2148 restoreBtn.style.background = 'var(--accent-2)'; 2149 } 2150 }); 2151 restoreChannelsItem.appendChild(restoreBtn); 2152 panel.appendChild(restoreChannelsItem); 2153 2154 // Moderation log 2155 const modLogItem = document.createElement('div'); 2156 modLogItem.className = 'setting-item'; 2157 modLogItem.style.marginBottom = 'var(--gap)'; 2158 modLogItem.style.paddingTop = 'var(--gap)'; 2159 modLogItem.style.borderTop = 'var(--border-thickness) solid var(--border)'; 2160 modLogItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">ð Moderation log</label>'; 2161 2162 const modLogDesc = document.createElement('small'); 2163 modLogDesc.textContent = 'Recent auto-mutes, strikes, IP bans, and message deletes. Use this to debug false positives.'; 2164 modLogDesc.style.display = 'block'; 2165 modLogDesc.style.marginTop = '4px'; 2166 modLogDesc.style.marginBottom = '12px'; 2167 modLogDesc.style.color = 'var(--text-4)'; 2168 modLogItem.appendChild(modLogDesc); 2169 2170 const modLogToolbar = document.createElement('div'); 2171 modLogToolbar.style.display = 'flex'; 2172 modLogToolbar.style.gap = '8px'; 2173 modLogToolbar.style.marginBottom = '12px'; 2174 2175 const modLogRefreshBtn = document.createElement('button'); 2176 modLogRefreshBtn.type = 'button'; 2177 modLogRefreshBtn.className = 'settings-mod-btn'; 2178 modLogRefreshBtn.textContent = 'Refresh log'; 2179 modLogToolbar.appendChild(modLogRefreshBtn); 2180 modLogItem.appendChild(modLogToolbar); 2181 2182 const modLogList = document.createElement('div'); 2183 modLogList.className = 'settings-mod-log-list'; 2184 modLogList.style.maxHeight = '320px'; 2185 modLogList.style.overflowY = 'auto'; 2186 modLogList.style.border = 'var(--border-thickness) solid var(--border)'; 2187 modLogList.style.borderRadius = '8px'; 2188 modLogList.style.background = 'var(--bg-2)'; 2189 modLogList.style.padding = '8px'; 2190 modLogList.textContent = 'Loadingâ¦'; 2191 modLogItem.appendChild(modLogList); 2192 2193 function renderModerationLogRows(entries) { 2194 modLogList.innerHTML = ''; 2195 if (!entries || !entries.length) { 2196 modLogList.textContent = 'No moderation events logged yet.'; 2197 return; 2198 } 2199 var logApi = window.ChatroomModerationLog;
2200 entries.forEach(function (entry) { 2201 var row = document.createElement('div'); 2202 row.className = 'settings-mod-log-row'; 2203 row.style.padding = '10px 8px'; 2204 row.style.borderBottom = '1px solid var(--border)'; 2205 row.style.fontSize = '12px'; 2206 row.style.lineHeight = '1.45'; 2207 row.style.color = 'var(--text-3)'; 2208 2209 var typeColors = { 2210 strike: '#e65100', 2211 timeout_block: '#f57c00', 2212 ip_ban: '#c62828', 2213 message_delete: '#5c6bc0', 2214 channel_delete: '#7b1fa2', 2215 admin_clear: '#2e7d32' 2216 }; 2217 var typeLabel = logApi ? logApi.formatTypeLabel(entry.type) : entry.type; 2218 var when = logApi ? logApi.formatTimestamp(entry.timestamp) : ''; 2219 var detail = logApi ? logApi.describeEntry(entry) : ''; 2220 2221 row.innerHTML = 2222 '<div style="display:flex;justify-content:space-between;gap:8px;margin-bottom:4px;">' + 2223 '<strong style="color:' + (typeColors[entry.type] || 'var(--text-2)') + ';">' + typeLabel + '</strong>' + 2224 '<span style="color:var(--text-4);white-space:nowrap;">' + when + '</span>' + 2225 '</div>' + 2226 '<div><strong style="color:var(--text-2);">' + (entry.username || entry.actor || 'unknown') + '</strong></div>' + 2227 '<div style="margin-top:4px;">' + detail + '</div>'; 2228 modLogList.appendChild(row); 2229 }); 2230 } 2231 2232 function loadModerationLog() { 2233 modLogList.textContent = 'Loadingâ¦'; 2234 if (!window.ChatroomModerationLog) { 2235 modLogList.textContent = 'Moderation log module not loaded.'; 2236 return; 2237 } 2238 window.ChatroomModerationLog.fetchRecent(80).then(renderModerationLogRows); 2239 } 2240 2241 modLogRefreshBtn.addEventListener('click', loadModerationLog); 2242 loadModerationLog(); 2243 panel.appendChild(modLogItem); 2244 2245 // Channel Reordering section 2246 const reorderItem = document.createElement('div'); 2247 reorderItem.className = 'setting-item'; 2248 reorderItem.style.marginBottom = 'var(--gap)'; 2249 reorderItem.style.paddingTop = 'var(--gap)'; 2250 reorderItem.style.borderTop = 'var(--border-thickness) solid var(--border)'; 2251 reorderItem.innerHTML = '<label style="font-weight: 500; color: var(--text-2);">ð Reorder Channels</label>'; 2252 2253 const reorderDesc = document.createElement('small'); 2254 reorderDesc.textContent = 'Drag and drop text channels in the sidebar to reorder them. The order is synced globally for all users.'; 2255 reorderDesc.style.display = 'block'; 2256 reorderDesc.style.marginTop = '4px'; 2257 reorderDesc.style.color = 'var(--text-4)'; 2258 reorderItem.appendChild(reorderDesc); 2259 panel.appendChild(reorderItem); 2260 2261 return; 2262 break; 2263 } 2264 2265 // Fallback for old HTML-based approach 2266 if (html) { 2267 panel.innerHTML = html; 2268 populateSettingsForm(); 2269 attachSettingsHandlers(); 2270 } 2271 } 2272 2273 /** 2274 * Helper: Create select element (fallback) 2275 */ 2276 function createSelect(options, value, id, onChange) { 2277 const select = document.createElement('select'); 2278 select.id = id; 2279 options.forEach(opt => { 2280 const option = document.createElement('option'); 2281 option.value = opt; 2282 option.textContent = opt.charAt(0).toUpperCase() + opt.slice(1); 2283 if (opt === value) option.selected = true; 2284 select.appendChild(option); 2285 }); 2286 if (onChange) { 2287 select.addEventListener('change', (e) =>
2287 onChange(e.target.value)); 2288 } 2289 return select; 2290 } 2291 2292 /** 2293 * Helper: Create checkbox (fallback) 2294 */ 2295 function createCheckbox(label, checked, id, onChange) { 2296 const labelEl = document.createElement('label'); 2297 const input = document.createElement('input'); 2298 input.type = 'checkbox'; 2299 input.id = id; 2300 input.checked = checked; 2301 if (onChange) { 2302 input.addEventListener('change', (e) => onChange(e.target.checked)); 2303 } 2304 labelEl.appendChild(input); 2305 const span = document.createElement('span'); 2306 span.textContent = label; 2307 labelEl.appendChild(span); 2308 return labelEl; 2309 } 2310 2311 /** 2312 * Helper: Create textarea (fallback) 2313 */ 2314 function createTextarea(placeholder, value, id, rows, onChange) { 2315 const textarea = document.createElement('textarea'); 2316 textarea.id = id; 2317 textarea.placeholder = placeholder; 2318 textarea.value = value; 2319 textarea.rows = rows; 2320 if (onChange) { 2321 let timeout; 2322 textarea.addEventListener('input', (e) => { 2323 clearTimeout(timeout); 2324 timeout = setTimeout(() => onChange(e.target.value), 500); 2325 }); 2326 } 2327 return textarea; 2328 } 2329 2330 /** 2331 * Populate settings form with current values 2332 */ 2333 function populateSettingsForm() { 2334 hydrateThemeFromSite(); 2335 syncThemeControlsInForm(); 2336 const fontSizeSelect = document.getElementById('setting-font-size'); 2337 if (fontSizeSelect) fontSizeSelect.value = settings.fontSize; 2338 const emojiSizeSelect = document.getElementById('setting-emoji-size'); 2339 if (emojiSizeSelect) emojiSizeSelect.value = settings.emojiSize; 2340 document.getElementById('setting-animations') && (document.getElementById('setting-animations').checked = settings.animations); 2341 document.getElementById('setting-message-grouping') && (document.getElementById('setting-message-grouping').checked = settings.messageGrouping); 2342 document.getElementById('setting-show-timestamps') && (document.getElementById('setting-show-timestamps').checked = settings.showTimestamps); 2343 document.getElementById('setting-show-avatars') && (document.getElementById('setting-show-avatars').checked = settings.showAvatars); 2344 document.getElementById('setting-notifications') && (document.getElementById('setting-notifications').checked = settings.notifications); 2345 document.getElementById('setting-sound-effects') && (document.getElementById('setting-sound-effects').checked = settings.soundEffects); 2346 } 2347 2348 /** 2349 * Attach event handlers to settings inputs 2350 */ 2351 function attachSettingsHandlers() { 2352 // Theme 2353 const themeSelect = document.getElementById('setting-theme'); 2354 if (themeSelect) { 2355 themeSelect.value = settings.theme; 2356 themeSelect.addEventListener('change', (e) => { 2357 settings.theme = e.target.value; 2358 saveSettings(); 2359 }); 2360 } 2361 2362 // Font size 2363 const fontSizeSelect = document.getElementById('setting-font-size'); 2364 if (fontSizeSelect) { 2365 fontSizeSelect.value = settings.fontSize; 2366 fontSizeSelect.addEventListener('change', (e) => { 2367 settings.fontSize = e.target.value; 2368 saveSettings(); 2369 }); 2370 } 2371 2372 // Emoji size 2373 const emojiSizeSelect = document.getElementById('setting-emoji-size'); 2374 if (emojiSizeSelect) { 2375 emojiSizeSelect.value = settings.emojiSize; 2376 emojiSizeSelect.addEventListener('change', (e) => { 2377 settings.emojiSize = e.target.value; 2378 saveSettings(); 2379 }); 2380 } 2381 2382 // Checkboxes 2383 ['animations', 'message-grouping', 'show-timestamps', 'show-avatars', 'notifications', 'sound-effects'].forEach(id => { 2384 const checkbox = document.getElementById(`setting-${id}`); 2385 if (checkbox) { 2386 const key = id.replace(/-/g, '').replace('setting', ''); 2387 checkbox.checked = settings[key] || false; 2388 checkbox.addEventListener('change', (e) => { 2389 settings[key] = e.target.checked; 2390 saveSettings(); 2391 }); 2392 } 2393 }); 2394 2395 // Custom CSS - handled by Apply button in showCategory 2396 2397 // Reset button
2398 const resetBtn = document.getElementById('reset-settings-btn'); 2399 if (resetBtn) { 2400 resetBtn.addEventListener('click', () => { 2401 if (confirm('Are you sure you want to reset all settings to default?')) { 2402 settings = { 2403 theme: 'default', 2404 colorMode: 'dark', 2405 messageGrouping: true, 2406 showTimestamps: true, 2407 showAvatars: true, 2408 fontSize: 'medium', 2409 emojiSize: 'medium', 2410 animations: true, 2411 soundEffects: false, 2412 notifications: false, 2413 desktopNotifications: false, 2414 enterToSend: true, 2415 muteOnJoinVoice: false, 2416 playJoinLeaveSounds: true, 2417 autoScrollNewMessages: true, 2418 showLinkPreviews: true, 2419 showOnlineStatus: true, 2420 confirmBeforeLeavingVoice: true 2421 }; 2422 saveSettings(); 2423 populateSettingsForm(); 2424 } 2425 }); 2426 } 2427 } 2428 2429 /** 2430 * Get setting value 2431 */ 2432 function getSetting(key) { 2433 return settings[key]; 2434 } 2435 2436 /** 2437 * Set setting value 2438 */ 2439 function setSetting(key, value) { 2440 settings[key] = value; 2441 saveSettings(); 2442 } 2443 2444 function syncChatroomThemeFromSiteEvent(detail) { 2445 if (!detail) return; 2446 if (detail.palette) { 2447 settings.theme = PALETTE_THEMES.includes(detail.palette) ? detail.palette : 'default'; 2448 } 2449 if (detail.colorMode === 'light' || detail.colorMode === 'dark') { 2450 settings.colorMode = detail.colorMode; 2451 } else if (typeof detail.darkMode === 'boolean') { 2452 settings.colorMode = detail.darkMode ? 'dark' : 'light'; 2453 } 2454 } 2455 2456 function persistThemeToLocalStorage() { 2457 try { 2458 localStorage.setItem('chatroom_settings', JSON.stringify(settings)); 2459 } catch (e) { 2460 console.error('Error saving local settings:', e); 2461 } 2462 } 2463 2464 function syncThemeControlsInForm() { 2465 const theme = PALETTE_THEMES.includes(settings.theme) ? settings.theme : 'default'; 2466 settings.theme = theme; 2467 const themeSelect = document.getElementById('setting-theme'); 2468 if (themeSelect && themeSelect.querySelector('option[value="' + theme + '"]')) { 2469 themeSelect.value = theme; 2470 } 2471 const colorToggle = document.getElementById('setting-color-mode'); 2472 if (colorToggle) { 2473 colorToggle.checked = settings.colorMode === 'dark'; 2474 } 2475 const colorModeCardEl = document.getElementById('chatroom-color-mode-card'); 2476 if (colorModeCardEl) { 2477 colorModeCardEl.classList.toggle('is-dark', settings.colorMode === 'dark'); 2478 } 2479 } 2480 2481 function syncThemeFromSite(source, detail) { 2482 if (source === 'chatroom-settings') return; 2483 if (detail) { 2484 syncChatroomThemeFromSiteEvent(detail); 2485 } else { 2486 hydrateThemeFromSite(); 2487 } 2488 syncThemeControlsInForm(); 2489 persistThemeToLocalStorage(); 2490 if (isChatroomRouteActive()) { 2491 applySettings(); 2492 } 2493 } 2494 2495 document.addEventListener('pupwife-color-mode', function (e) { 2496 if (!e.detail) return; 2497 syncThemeFromSite(e.detail.source, e.detail); 2498 }); 2499 2500 document.addEventListener('pupwife-site-theme', function (e) { 2501 if (!e.detail || !e.detail.palette) return; 2502 syncThemeFromSite(e.detail.source, e.detail); 2503 }); 2504 2505 // Initialize on load 2506 if (document.readyState === 'loading') { 2507 document.addEventListener('DOMContentLoaded', init); 2508 } else { 2509 init(); 2510 } 2511 2512 return { 2513 getSetting, 2514 setSetting, 2515 getColorMode, 2516 setColorMode, 2517 showSettings, 2518 hideSettings, 2519 init, 2520 applySettings, 2521 isRouteActive: isChatroomRouteActive 2522 }; 2523})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.