// static/js/08-settings.js "use strict"; window.Glow = window.Glow || {}; Glow.state = Glow.state || {}; Glow.state.settings = Glow.state.settings || null; Glow.applyThemeMode = mode => { const normalized = mode === "light" ? "light" : "dark"; document.body.classList.toggle("theme-light", normalized === "light"); localStorage.setItem("glow_theme_mode", normalized); }; Glow.applyCustomCss = cssText => { let style = document.getElementById("glowCustomCss"); if (!style) { style = document.createElement("style"); style.id = "glowCustomCss"; document.head.append(style); } style.textContent = typeof cssText === "string" ? cssText : ""; }; Glow.loadMySettings = async () => { try { const data = await Glow.apiGetMySettings(); Glow.state.settings = data.settings || {}; const accent = Glow.state.settings.accent_color || Glow.state.accentColor; Glow.applyAccentColor(accent); const mode = Glow.state.settings.theme_mode || localStorage.getItem("glow_theme_mode") || "dark"; Glow.applyThemeMode(mode); Glow.applyCustomCss(Glow.state.settings.custom_css || ""); } catch (_) { const mode = localStorage.getItem("glow_theme_mode") || "dark"; Glow.applyThemeMode(mode); } }; Glow.renderHeaderMenu = () => { const list = Glow.els.headerMenuList; if (!list) return; list.replaceChildren(); const addItem = ({ label, hint = "", onClick, hidden = false }) => { if (hidden) return; const button = document.createElement("button"); button.className = "header-menu-item"; button.type = "button"; const left = document.createElement("span"); left.textContent = label; const right = document.createElement("small"); right.textContent = hint; button.append(left, right); button.addEventListener("click", () => { Glow.closeHeaderMenu?.(); onClick?.(); }); list.append(button); }; addItem({ label: "My profile", hint: Glow.state.currentUser ? `@${Glow.state.currentUser.username}` : "", onClick: () => Glow.openMyProfile?.(), hidden: !Glow.state.currentUser }); addItem({ label: "Settings", hint: "Theme, CSS, profile", onClick: () => Glow.openSettings?.() }); addItem({ label: "Direct messages", hint: Glow.state.currentUser ? "Private chat" : "", onClick: async () => { Glow.state.activeView = "dms"; Glow.updateViewUI(); await Glow.loadDms?.(); Glow.schedulePoll?.(); }, hidden: !Glow.state.currentUser }); addItem({ label: "Create team", hint: "Up to 3 max", onClick: () => Glow.openCreateTeamDialog?.(), hidden: !Glow.state.currentUser }); addItem({ label: "Edit current team", hint: Glow.state.activeTeam?.can_manage ? "Manage" : "", onClick: () => Glow.openEditTeamDialog?.(Glow.state.activeTeam), hidden: !Glow.state.activeTeam || !Glow.state.activeTeam.can_manage }); addItem({ label: "Add channel", hint: Glow.state.activeTeam?.name || "", onClick: () => Glow.openCreateChannelDialog?.(Glow.state.activeTeam), hidden: !Glow.state.activeTeam || !Glow.state.activeTeam.can_manage }); addItem({ label: "Edit current channel", hint: Glow.state.activeChannel?.name || Glow.state.activeRoom?.name || "", onClick: () => Glow.openEditChannelDialog?.( Glow.state.activeTeam, Glow.state.activeChannel || Glow.state.activeRoom ), hidden: !Glow.state.activeTeam || !Glow.state.activeTeam.can_manage || !(Glow.state.activeChannel || Glow.state.activeRoom) || Glow.state.activeView === "dm" }); addItem({ label: "Manage team members", hint: Glow.state.activeTeam?.can_manage ? "Members" : "", onClick: () => Glow.manageTeamMembers?.(Glow.state.activeTeam), hidden: !Glow.state.activeTeam || !Glow.state.activeTeam.can_manage }); addItem({ label: "Create team invite", hint: Glow.state.activeTeam?.can_manage ? "Share" : "", onClick: () => Glow.createTeamInviteLink?.(Glow.state.activeTeam), hidden: !Glow.state.activeTeam || !Glow.state.activeTeam.can_manage }); addItem({ label: "Delete current channel", hint: Glow.state.activeChannel?.name || "", onClick: () => Glow.deleteCurrentChannel?.(Glow.state.activeTeam), hidden: !Glow.state.activeTeam || !Glow.state.activeTeam.can_manage || !Glow.state.activeChannel || Boolean(Glow.state.activeChannel?.legacy_room_id) }); }; Glow.openHeaderMenu = () => { Glow.renderHeaderMenu(); Glow.els.headerMenuOverlay?.classList.remove("hidden"); }; Glow.closeHeaderMenu = () => { Glow.els.headerMenuOverlay?.classList.add("hidden"); }; Glow.toggleHeaderMenu = () => { if (Glow.els.headerMenuOverlay?.classList.contains("hidden")) { Glow.openHeaderMenu(); } else { Glow.closeHeaderMenu(); } }; Glow.openSettings = async () => { Glow.state.activeView = "settings"; Glow.updateViewUI(); if (!Glow.state.settings && Glow.state.currentUser) { await Glow.loadMySettings(); } Glow.renderSettingsView(); }; Glow.renderNameColorPreview = value => { const wrap = document.createElement("div"); wrap.className = "settings-row"; const label = document.createElement("div"); label.className = "header-subtitle"; label.textContent = "Preview"; const preview = document.createElement("div"); preview.className = "name-color-preview"; preview.textContent = Glow.state.currentUser?.username || "username"; preview.style.color = Glow.normalizeNameColor(value) || Glow.roleDefaultNameColor(Glow.state.currentUser?.role); wrap.append(label, preview); return wrap; }; Glow.renderSettingsView = () => { const box = Glow.els.profileBox; box.replaceChildren(); const card = document.createElement("div"); card.className = "settings-card"; const appearance = document.createElement("section"); appearance.className = "settings-section"; const aTitle = document.createElement("h2"); aTitle.textContent = "Appearance"; const aText = document.createElement("p"); aText.textContent = "Set your theme mode, accent color, username color, and custom CSS."; const themeRow = document.createElement("div"); themeRow.className = "settings-theme-row"; const currentTheme = Glow.state.settings?.theme_mode || localStorage.getItem("glow_theme_mode") || "dark"; const darkButton = document.createElement("button"); darkButton.className = `theme-pill${currentTheme !== "light" ? " active" : ""}`; darkButton.type = "button"; darkButton.textContent = "Dark"; const lightButton = document.createElement("button"); lightButton.className = `theme-pill${currentTheme === "light" ? " active" : ""}`; lightButton.type = "button"; lightButton.textContent = "Light"; darkButton.addEventListener("click", () => { Glow.applyThemeMode("dark"); Glow.state.settings ||= {}; Glow.state.settings.theme_mode = "dark"; Glow.renderSettingsView(); }); lightButton.addEventListener("click", () => { Glow.applyThemeMode("light"); Glow.state.settings ||= {}; Glow.state.settings.theme_mode = "light"; Glow.renderSettingsView(); }); themeRow.append(darkButton, lightButton); const accentWrap = document.createElement("div"); accentWrap.className = "settings-row"; const accentLabel = document.createElement("label"); accentLabel.className = "dialog-label"; accentLabel.textContent = "Accent color"; const accentInput = document.createElement("input"); accentInput.type = "color"; accentInput.className = "accent-color-input"; accentInput.value = Glow.normalizeAccentColor( Glow.state.settings?.accent_color || Glow.state.accentColor ) || Glow.DEFAULT_ACCENT_COLOR; accentInput.addEventListener("input", () => { Glow.applyAccentColor(accentInput.value); Glow.state.settings ||= {}; Glow.state.settings.accent_color = accentInput.value; }); accentLabel.append(accentInput); accentWrap.append(accentLabel); const nameColorWrap = document.createElement("div"); nameColorWrap.className = "settings-row"; const nameColorLabel = document.createElement("label"); nameColorLabel.className = "dialog-label"; nameColorLabel.textContent = "Username color"; const nameColorInput = document.createElement("input"); nameColorInput.type = "color"; nameColorInput.className = "accent-color-input"; nameColorInput.value = Glow.normalizeNameColor(Glow.state.settings?.name_color) || Glow.roleDefaultNameColor(Glow.state.currentUser?.role); const nameColorText = document.createElement("input"); nameColorText.className = "field accent-hex-input"; nameColorText.type = "text"; nameColorText.maxLength = 7; nameColorText.value = Glow.normalizeNameColor(Glow.state.settings?.name_color) || ""; nameColorText.placeholder = "#7dd3fc"; const nameRow = document.createElement("div"); nameRow.className = "accent-picker-row"; const previewSwatch = document.createElement("span"); previewSwatch.className = "accent-preview"; previewSwatch.style.background = Glow.normalizeNameColor(Glow.state.settings?.name_color) || Glow.roleDefaultNameColor(Glow.state.currentUser?.role); const help = document.createElement("div"); help.className = "header-subtitle"; help.textContent = `Leave empty to use your role default (${Glow.roleDefaultNameColor(Glow.state.currentUser?.role)}).`; const updatePreview = value => { const normalized = Glow.normalizeNameColor(value); const finalColor = normalized || Glow.roleDefaultNameColor(Glow.state.currentUser?.role); previewSwatch.style.background = finalColor; return finalColor; }; nameColorInput.addEventListener("input", () => { nameColorText.value = nameColorInput.value; Glow.state.settings ||= {}; Glow.state.settings.name_color = nameColorInput.value; updatePreview(nameColorInput.value); Glow.renderSettingsView(); }); nameColorText.addEventListener("input", () => { const normalized = Glow.normalizeNameColor(nameColorText.value.trim()); Glow.state.settings ||= {}; Glow.state.settings.name_color = normalized || null; updatePreview(nameColorText.value.trim()); }); const resetNameColor = document.createElement("button"); resetNameColor.className = "button secondary"; resetNameColor.type = "button"; resetNameColor.textContent = "Use role default"; resetNameColor.addEventListener("click", () => { Glow.state.settings ||= {}; Glow.state.settings.name_color = null; Glow.renderSettingsView(); }); nameRow.append(nameColorInput, nameColorText, previewSwatch); nameColorLabel.append(nameRow); nameColorWrap.append(nameColorLabel, help, Glow.renderNameColorPreview( Glow.state.settings?.name_color ), resetNameColor); appearance.append(aTitle, aText, themeRow, accentWrap, nameColorWrap); const cssSection = document.createElement("section"); cssSection.className = "settings-section"; const cTitle = document.createElement("h2"); cTitle.textContent = "Custom CSS"; const cText = document.createElement("p"); cText.textContent = "Optional CSS applied only for your account in this browser session and saved to your settings. \n ex: *,html,body{font-size:95%;text-transform:lowercase;}"; const cssInput = document.createElement("textarea"); cssInput.className = "field settings-custom-css"; cssInput.placeholder = "/* Custom CSS */"; cssInput.value = Glow.state.settings?.custom_css || ""; cssInput.addEventListener("input", () => { Glow.applyCustomCss(cssInput.value); Glow.state.settings ||= {}; Glow.state.settings.custom_css = cssInput.value; }); cssSection.append(cTitle, cText, cssInput); const accountSection = document.createElement("section"); accountSection.className = "settings-section"; const accountTitle = document.createElement("h2"); accountTitle.textContent = "Account"; const accountText = document.createElement("p"); accountText.textContent = "Update your public profile and application preferences."; const actions = document.createElement("div"); actions.className = "settings-actions"; const editProfile = document.createElement("button"); editProfile.className = "button"; editProfile.type = "button"; editProfile.textContent = "Edit profile"; editProfile.addEventListener("click", () => { Glow.openMyProfile?.(true); }); const saveSettings = document.createElement("button"); saveSettings.className = "button"; saveSettings.type = "button"; saveSettings.textContent = "Save settings"; saveSettings.addEventListener("click", async () => { try { const payload = { accent_color: Glow.normalizeAccentColor( Glow.state.settings?.accent_color || Glow.state.accentColor ) || Glow.DEFAULT_ACCENT_COLOR, name_color: Glow.normalizeNameColor(Glow.state.settings?.name_color) || null, custom_css: Glow.state.settings?.custom_css || "", theme_mode: Glow.state.settings?.theme_mode || localStorage.getItem("glow_theme_mode") || "dark" }; const data = await Glow.apiUpdateMySettings(payload); Glow.state.settings = data.settings || payload; Glow.showToast("Settings saved."); Glow.renderSettingsView(); } catch (error) { Glow.showToast(error.message || "Could not save settings.", "error"); } }); const resetAccent = document.createElement("button"); resetAccent.className = "button secondary"; resetAccent.type = "button"; resetAccent.textContent = "Reset accent"; resetAccent.addEventListener("click", () => { Glow.resetAccentColor(); Glow.state.settings ||= {}; Glow.state.settings.accent_color = Glow.DEFAULT_ACCENT_COLOR; Glow.renderSettingsView(); }); actions.append(editProfile, saveSettings, resetAccent); accountSection.append(accountTitle, accountText, actions); card.append(appearance, cssSection, accountSection); box.append(card); };