mirror of
https://github.com/ancsemi/Haven
synced 2026-04-21 13:37:41 +00:00
- Replace Google STUN servers with open-source defaults (stunprotocol.org, nextcloud.com) - Add STUN_URLS env var for custom STUN server configuration - Update .env.example with STUN_URLS documentation
2576 lines
180 KiB
HTML
2576 lines
180 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content">
|
||
<meta name="theme-color" content="#1a1a2e">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<link rel="manifest" href="/manifest.webmanifest">
|
||
<title>Haven</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cG9seWdvbiBwb2ludHM9IjUwLDMgOTMsMjggOTMsNzIgNTAsOTcgNyw3MiA3LDI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM2YjRmZGIiIHN0cm9rZS13aWR0aD0iOCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjx0ZXh0IHg9IjUwIiB5PSI2MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsLEhlbHZldGljYSxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iYm9sZCIgZm9udC1zaXplPSIzOCIgZmlsbD0iIzZiNGZkYiIgb3BhY2l0eT0iMC45NSI+SDwvdGV4dD48L3N2Zz4=">
|
||
<link rel="stylesheet" href="/css/style.css?v=2.7.11">
|
||
<script src="/js/theme-init.js"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="app">
|
||
<div id="app-body">
|
||
|
||
<!-- ─── Server Bar (far left — like Discord) ─────── -->
|
||
<nav class="server-bar" id="server-bar">
|
||
<div class="server-icon home active" id="home-server" data-i18n-title="app.sidebar.this_server" title="This Server">
|
||
<span class="server-icon-text">⬡</span>
|
||
<span class="server-status-dot online"></span>
|
||
</div>
|
||
<div class="server-separator"></div>
|
||
<div id="server-list"></div>
|
||
<button class="server-icon add-server" id="add-server-btn" data-i18n-title="app.sidebar.add_server" title="Add Server">
|
||
<span class="server-icon-text">+</span>
|
||
</button>
|
||
<button class="server-icon manage-servers" id="manage-servers-btn" data-i18n-title="app.sidebar.manage_servers" title="Manage Servers">
|
||
<span class="server-icon-text">⚙</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<!-- ─── Left Sidebar ────────────────────────────── -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
|
||
<div class="sidebar-header">
|
||
<div class="brand">
|
||
<span class="logo-sm">⬡</span>
|
||
<span class="brand-text">HAVEN</span>
|
||
</div>
|
||
<button id="mobile-sidebar-close" class="mobile-panel-close" data-i18n-title="modals.common.close" data-i18n-aria-label="app.actions.close_sidebar" title="Close" aria-label="Close sidebar">×</button>
|
||
<div class="user-bar">
|
||
<span class="led on" id="connection-led" data-i18n-title="app.status.server_connection" title="Server connection"></span>
|
||
<div class="user-names">
|
||
<span class="current-user" id="current-user"></span>
|
||
<span class="login-name" id="login-name" data-i18n-title="app.profile.login_username" title="Your login username"></span>
|
||
</div>
|
||
<button id="rename-btn" class="icon-btn small" data-i18n-title="app.actions.change_display_name" title="Change display name">✏️</button>
|
||
<button id="logout-btn" class="icon-btn" data-i18n-title="app.actions.logout" title="Logout">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||
<polyline points="16 17 21 12 16 7"/>
|
||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-mod-container" id="sidebar-mod-container">
|
||
<!-- Mobile server bubbles (visible only on mobile) -->
|
||
<div class="sidebar-section mobile-servers-section" id="mobile-servers-section">
|
||
<h5 class="section-label mobile-servers-toggle" id="mobile-servers-toggle" style="cursor:pointer;user-select:none">
|
||
<span class="collapse-toggle-arrow" id="mobile-servers-arrow">▾</span>
|
||
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.servers">Servers</span>
|
||
</h5>
|
||
<div class="mobile-servers-row" id="mobile-servers-row">
|
||
<div class="mobile-servers-scroll" id="mobile-servers-scroll"></div>
|
||
<button class="mobile-srv-add-btn" id="mobile-srv-add-btn" data-i18n-title="app.sidebar.add_server" title="Add Server">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section" data-mod-id="join">
|
||
<h5 class="section-label collapsible-section-toggle" id="join-section-toggle" style="cursor:pointer;user-select:none">
|
||
<span class="collapse-toggle-arrow" id="join-section-arrow">▾</span>
|
||
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.join_channel">Join a Channel</span>
|
||
</h5>
|
||
<div class="collapsible-section-body" id="join-section-body">
|
||
<div class="input-row">
|
||
<input type="text" id="channel-code-input"
|
||
data-i18n-placeholder="app.sidebar.join_placeholder" placeholder="Enter code..." maxlength="8" spellcheck="false">
|
||
<button id="join-channel-btn" class="btn-sm" data-i18n="app.sidebar.join_btn">Join</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-section" id="admin-controls" data-mod-id="create" style="display:none">
|
||
<h5 class="section-label collapsible-section-toggle" id="create-section-toggle" style="cursor:pointer;user-select:none">
|
||
<span class="collapse-toggle-arrow" id="create-section-arrow">▾</span>
|
||
<span class="section-label-text" style="flex:1" data-i18n="app.sidebar.create_channel">Create Channel</span>
|
||
</h5>
|
||
<div class="collapsible-section-body" id="create-section-body">
|
||
<div class="input-row">
|
||
<input type="text" id="new-channel-name"
|
||
data-i18n-placeholder="app.sidebar.channel_name_placeholder" placeholder="Channel name..." maxlength="50">
|
||
<button id="create-channel-btn" class="btn-sm btn-accent">+</button>
|
||
</div>
|
||
<label class="checkbox-row" style="margin:6px 0 2px;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
|
||
<input type="checkbox" id="new-channel-private" style="width:14px;height:14px">
|
||
<span>🔒 <span data-i18n="app.sidebar.private_channel">Private</span></span> <span style="opacity:0.5;font-size:0.75rem" data-i18n="app.sidebar.private_hint">(invite-only)</span>
|
||
</label>
|
||
<label class="checkbox-row" style="margin:2px 0;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.8rem;opacity:0.8">
|
||
<input type="checkbox" id="new-channel-temporary" style="width:14px;height:14px">
|
||
<span>⏱️ <span data-i18n="app.sidebar.temporary_channel">Temporary</span></span> <span style="opacity:0.5;font-size:0.75rem" data-i18n="app.sidebar.temporary_hint">(auto-delete)</span>
|
||
</label>
|
||
<div id="temp-channel-duration-row" style="display:none;margin:4px 0 2px;padding-left:22px">
|
||
<div class="input-row" style="gap:6px;align-items:center">
|
||
<input type="number" id="new-channel-duration" min="1" max="720" value="24" style="width:60px;font-size:0.8rem" placeholder="Hours">
|
||
<span style="font-size:0.75rem;opacity:0.6" data-i18n="app.sidebar.hours">hours</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-split" id="sidebar-split" data-mod-id="channels">
|
||
<div class="sidebar-section channel-section" id="channels-pane">
|
||
<h5 class="section-label channels-toggle" id="channels-toggle" style="cursor:pointer;user-select:none"><span class="channels-toggle-arrow" id="channels-toggle-arrow">▾</span><span class="section-label-text" style="flex:1" data-i18n="app.sidebar.channels">Channels</span><button class="icon-btn small" id="sub-channel-panel-btn" data-i18n-title="app.actions.sub_channel_subscriptions" title="Sub-channel subscriptions" style="font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2;display:none">📡</button><button class="icon-btn small" id="organize-channels-btn" data-i18n-title="app.actions.organize_channels" title="Organize channels" style="display:none;font-size:0.85rem;padding:2px 6px;opacity:0.6;position:relative;z-index:2">📋</button></h5>
|
||
<div class="channel-list" id="channel-list"></div>
|
||
</div>
|
||
<div class="sidebar-split-handle" id="sidebar-split-handle" data-i18n-title="app.actions.drag_to_resize" title="Drag to resize"></div>
|
||
<div class="sidebar-section dm-section-pane" id="dm-pane">
|
||
<h5 class="section-label dm-section-label dm-toggle" id="dm-toggle-header" style="cursor:pointer;user-select:none"><span class="dm-toggle-arrow" id="dm-toggle-arrow">▾</span><span class="section-label-text" style="flex:1" data-i18n="app.sidebar.direct_messages">Direct Messages</span><span style="display:flex;align-items:center;gap:4px;position:relative;z-index:2"><span class="dm-unread-count" id="dm-unread-badge" style="display:none"></span><button class="icon-btn small" id="organize-dms-btn" data-i18n-title="app.actions.organize_dms" title="Organize DMs" style="font-size:0.85rem;padding:2px 6px;opacity:0.6">📋</button></span></h5>
|
||
<div class="dm-list-scroll" id="dm-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pinned bottom area (never pushed off screen) -->
|
||
<div class="sidebar-bottom">
|
||
<!-- Theme popup (floats above bottom bar) -->
|
||
<div id="theme-popup" class="theme-popup" style="display:none">
|
||
<div class="theme-popup-header">
|
||
<span class="theme-popup-title" data-i18n="app.theme.title">Theme</span>
|
||
<button id="theme-popup-close" class="icon-btn small" data-i18n-title="app.theme.close" title="Close">×</button>
|
||
</div>
|
||
<div class="theme-selector" id="theme-selector">
|
||
<button class="theme-btn active" data-theme="haven" title="Haven"><span class="theme-icon">⬡</span></button>
|
||
<button class="theme-btn" data-theme="discord" title="Discord"><span class="theme-icon">🎮</span></button>
|
||
<button class="theme-btn" data-theme="matrix" title="Matrix"><span class="theme-icon">Ⅿ</span></button>
|
||
<button class="theme-btn" data-theme="tron" title="Tron"><span class="theme-icon">◈</span></button>
|
||
<button class="theme-btn" data-theme="halo" title="HALO"><span class="theme-icon">⌁</span></button>
|
||
<button class="theme-btn" data-theme="lotr" title="LoTR"><span class="theme-icon">⚜</span></button>
|
||
<button class="theme-btn" data-theme="cyberpunk" title="Cyberpunk"><span class="theme-icon">⚡</span></button>
|
||
<button class="theme-btn" data-theme="nord" title="Nord"><span class="theme-icon">❄</span></button>
|
||
<button class="theme-btn" data-theme="dracula" title="Dracula"><span class="theme-icon">🧛</span></button>
|
||
<button class="theme-btn" data-theme="bloodborne" title="Bloodborne"><span class="theme-icon">🩸</span></button>
|
||
<button class="theme-btn" data-theme="darksouls" title="Dark Souls"><span class="theme-icon">🔥</span></button>
|
||
<button class="theme-btn" data-theme="eldenring" title="Elden Ring"><span class="theme-icon">💍</span></button>
|
||
<button class="theme-btn" data-theme="ice" title="Ice"><span class="theme-icon">🧊</span></button>
|
||
<button class="theme-btn" data-theme="abyss" title="Abyss"><span class="theme-icon">🌊</span></button>
|
||
<button class="theme-btn" data-theme="minecraft" title="Minecraft"><span class="theme-icon">⛏️</span></button>
|
||
<button class="theme-btn" data-theme="ffx" title="Final Fantasy X"><span class="theme-icon">⚔️</span></button>
|
||
<button class="theme-btn" data-theme="zelda" title="Zelda"><span class="theme-icon">🗡️</span></button>
|
||
<button class="theme-btn" data-theme="fallout" title="Fallout (PIP Boy)"><span class="theme-icon">☢️</span></button>
|
||
<button class="theme-btn" data-theme="scripture" title="Scripture"><span class="theme-icon">✝️</span></button>
|
||
<button class="theme-btn" data-theme="chapel" title="Chapel"><span class="theme-icon">⛪</span></button>
|
||
<button class="theme-btn" data-theme="gospel" title="Gospel"><span class="theme-icon">🕊️</span></button>
|
||
<button class="theme-btn" data-theme="midnightpurple" title="Midnight Purple"><span class="theme-icon">🔮</span></button>
|
||
<button class="theme-btn" data-theme="crt" title="CRT"><span class="theme-icon">📺</span></button>
|
||
<button class="theme-btn" data-theme="win95" title="Windows 95"><span class="theme-icon">🪟</span></button>
|
||
<button class="theme-btn" data-theme="custom" title="Custom"><span class="theme-icon" id="custom-theme-swatch">🎨</span></button>
|
||
<button class="theme-btn" data-theme="rgb" title="RGB"><span class="theme-icon">🌈</span></button>
|
||
<button class="theme-btn" data-theme="daylight" title="Daylight (Light)"><span class="theme-icon">☀️</span></button>
|
||
<button class="theme-btn" data-theme="cloudy" title="Cloudy (Light)"><span class="theme-icon">☁️</span></button>
|
||
</div>
|
||
<!-- Effect overlay picker (mashup) -->
|
||
<div class="effect-overlay-section">
|
||
<div class="theme-popup-title" style="margin-top: 2px;" data-i18n="app.theme.effect_overlay">Effect Overlay</div>
|
||
<div class="effect-selector" id="effect-selector">
|
||
<button class="effect-btn active" data-effect="auto" title="Match theme">⟳</button>
|
||
<button class="effect-btn" data-effect="none" title="No effects">🚫</button>
|
||
<button class="effect-btn" data-effect="crt" title="CRT Scanlines">📺</button>
|
||
<button class="effect-btn" data-effect="matrix" title="Matrix Rain">Ⅿ</button>
|
||
<button class="effect-btn" data-effect="matrixbars" title="Matrix Scan Lines">▤</button>
|
||
<button class="effect-btn" data-effect="nord" title="Snowfall">❄</button>
|
||
<button class="effect-btn" data-effect="darksouls" title="Campfire">🔥</button>
|
||
<button class="effect-btn" data-effect="eldenring" title="Golden Grace">💍</button>
|
||
<button class="effect-btn" data-effect="bloodborne" title="Blood Vignette">🩸</button>
|
||
<button class="effect-btn" data-effect="fallout" title="Phosphor Glow">☢️</button>
|
||
<button class="effect-btn" data-effect="ffx" title="Water Flow">⚔️</button>
|
||
<button class="effect-btn" data-effect="ice" title="Frost Shimmer">🧊</button>
|
||
<button class="effect-btn" data-effect="cyberpunk" title="Glitch">⚡</button>
|
||
<button class="effect-btn" data-effect="lotr" title="Candlelight">⚜</button>
|
||
<button class="effect-btn" data-effect="abyss" title="Ocean Depth">🌊</button>
|
||
<button class="effect-btn" data-effect="scripture" title="Cross Light">✝️</button>
|
||
<button class="effect-btn" data-effect="chapel" title="Stained Glass">⛪</button>
|
||
<button class="effect-btn" data-effect="gospel" title="Divine Radiance">🕊️</button>
|
||
</div>
|
||
</div>
|
||
<!-- Custom theme triangle picker -->
|
||
<div id="custom-theme-editor" style="display:none">
|
||
<canvas id="custom-hue-bar" width="206" height="18"></canvas>
|
||
<canvas id="custom-triangle" width="206" height="180"></canvas>
|
||
</div>
|
||
<!-- RGB theme controls -->
|
||
<div id="rgb-theme-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.speed">Speed</span>
|
||
<input type="range" id="rgb-speed-slider" min="1" max="100" value="30" class="slider-sm rgb-slider">
|
||
</label>
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.vibrancy">Vibrancy</span>
|
||
<input type="range" id="rgb-vibrancy-slider" min="10" max="100" value="75" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- Effect speed slider (dynamic-effect themes) -->
|
||
<div id="effect-speed-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label" data-i18n="app.theme.effect_speed">Effect Speed</span>
|
||
<input type="range" id="effect-speed-slider" min="15" max="200" value="100" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- Sacred intensity slider (religious themes) -->
|
||
<div id="sacred-intensity-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label">Sacred Intensity</span>
|
||
<input type="range" id="sacred-intensity-slider" min="20" max="250" value="100" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- Glitch frequency slider (cyberpunk) -->
|
||
<div id="glitch-freq-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label">Glitch Frequency</span>
|
||
<input type="range" id="glitch-freq-slider" min="5" max="100" value="50" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
<!-- CRT effect sliders (vignette darkness + scanline intensity) -->
|
||
<div id="crt-editor" class="rgb-theme-editor" style="display:none">
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label">CRT Vignette</span>
|
||
<input type="range" id="crt-vignette-slider" min="0" max="100" value="50" class="slider-sm rgb-slider">
|
||
</label>
|
||
<label class="rgb-slider-row">
|
||
<span class="rgb-slider-label">CRT Scanlines</span>
|
||
<input type="range" id="crt-scanline-slider" min="0" max="80" value="45" class="slider-sm rgb-slider">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom bar -->
|
||
<div id="voice-bar" class="voice-bar" style="display:none"></div>
|
||
<div class="sidebar-bottom-bar">
|
||
<button id="theme-popup-toggle" class="sidebar-bottom-btn" data-i18n-title="app.theme.title" title="Themes">🎨</button>
|
||
<button class="sidebar-bottom-btn" id="activities-btn" data-i18n-title="modals.activities.title" title="Activities">🎮</button>
|
||
<button class="sidebar-bottom-btn" id="sidebar-members-btn" data-i18n-title="modals.all_members.title" title="All Members" style="display:none">👥</button>
|
||
<button id="mobile-settings-btn" class="sidebar-bottom-btn mobile-settings-btn" data-i18n-title="settings.title" title="Settings">⚙️</button>
|
||
<span style="flex:1"></span>
|
||
<button id="donors-btn" class="sidebar-bottom-btn donate-btn">❤️</button>
|
||
<button id="voice-mute-btn" class="sidebar-bottom-btn voice-header-btn" title="Mute" style="display:none">🎙️</button>
|
||
<button id="voice-deafen-btn" class="sidebar-bottom-btn voice-header-btn" title="Deafen" style="display:none">🔊</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ─── Main Content ────────────────────────────── -->
|
||
<main class="main">
|
||
<header class="channel-header">
|
||
<button id="mobile-menu-btn" class="mobile-menu-btn" data-i18n-title="header.mobile_menu" data-i18n-aria-label="header.mobile_menu" title="Menu" aria-label="Menu">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
<div class="mobile-server-dropdown" id="mobile-server-dropdown">
|
||
<button class="mobile-server-btn" id="mobile-server-btn" data-i18n-title="header.mobile_servers" data-i18n-aria-label="header.mobile_servers" title="Servers" aria-label="Servers">⬡</button>
|
||
<div class="mobile-server-menu" id="mobile-server-menu">
|
||
<div class="mobile-server-menu-header" data-i18n="header.mobile_servers">Servers</div>
|
||
<div id="mobile-server-list"></div>
|
||
<button class="mobile-server-add" id="mobile-server-add-btn" data-i18n="header.mobile_add_server">+ Add Server</button>
|
||
</div>
|
||
</div>
|
||
<div class="channel-info">
|
||
<h3 id="channel-header-name" data-i18n="header.select_channel">Select a channel</h3>
|
||
</div>
|
||
<div class="header-actions-box" id="header-actions-box" style="display:none">
|
||
<span class="channel-code-tag" id="channel-code-display" data-i18n-title="header.channel_code" title="Channel code"></span>
|
||
<button id="copy-code-btn" class="icon-btn small" data-i18n-title="header.copy_code" title="Copy code">📋</button>
|
||
<button id="channel-code-settings-btn" class="icon-btn small" data-i18n-title="header.channel_code_settings" title="Channel code settings">⚙️</button>
|
||
<span class="header-actions-divider"></span>
|
||
<button id="search-toggle-btn" class="icon-btn small" data-i18n-title="header.search_messages" title="Search messages (Ctrl+F)">🔍</button>
|
||
<button id="pinned-toggle-btn" class="icon-btn small" data-i18n-title="header.pinned_messages" title="Pinned messages">📌</button>
|
||
<button id="move-select-btn" class="icon-btn small" data-i18n-title="header.select_messages_to_move" title="Select messages to move" style="display:none">☰</button>
|
||
<div class="e2e-menu-wrapper" style="display:none" id="e2e-menu-wrapper">
|
||
<button id="e2e-menu-btn" class="icon-btn small" data-i18n-title="header.encryption_options" title="Encryption options">🔐</button>
|
||
<div class="e2e-dropdown" id="e2e-dropdown" style="display:none">
|
||
<button class="e2e-dropdown-item" id="e2e-verify-btn">🔑 <span data-i18n="header.verify_encryption">Verify Encryption</span></button>
|
||
<button class="e2e-dropdown-item e2e-danger" id="e2e-reset-btn">🔄 <span data-i18n="header.reset_encryption">Reset Encryption Keys</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="search-container" id="search-container" style="display:none">
|
||
<input type="text" id="search-input" class="search-input" data-i18n-placeholder="header.search_placeholder" placeholder="Search messages..." maxlength="100">
|
||
<button id="search-close-btn" class="icon-btn small" title="Close search">×</button>
|
||
</div>
|
||
<!-- Update available banner -->
|
||
<a id="update-banner" class="update-banner" href="#" style="display:none" target="_blank" rel="noopener">
|
||
<span class="update-pulse"></span>
|
||
<span class="update-text">Update Available</span>
|
||
</a>
|
||
<!-- Desktop app banner -->
|
||
<a id="desktop-app-banner" class="desktop-app-banner" href="https://ancsemi.github.io/Haven/#download" target="_blank" rel="noopener" style="display:none">
|
||
<span class="desktop-app-icon">🖥️</span>
|
||
<span class="desktop-app-text" data-i18n="header.get_desktop_app">Get the Desktop App</span>
|
||
<button class="desktop-app-dismiss" id="desktop-app-dismiss" title="Dismiss">×</button>
|
||
</a>
|
||
<!-- Android beta banner (must be <div>, not <button>; nesting
|
||
<button> inside <button> is invalid HTML and causes the
|
||
dismiss × to be hoisted out of its parent by the parser,
|
||
creating a phantom X in the channel header on mobile) -->
|
||
<div id="android-beta-banner" class="android-beta-banner" role="button" tabindex="0" style="display:none">
|
||
<span class="android-beta-icon">📱</span>
|
||
<span class="android-beta-text" data-i18n="header.android_beta">Android App</span>
|
||
<button class="android-beta-dismiss" id="android-beta-dismiss" title="Dismiss">×</button>
|
||
</div>
|
||
<!-- Spacer pushes voice controls to the right -->
|
||
<div style="flex:1"></div>
|
||
<!-- Right-side: dynamic indicators, then voice (always furthest right) -->
|
||
<div class="voice-controls">
|
||
<button id="mobile-users-btn" class="mobile-users-btn" title="Members" aria-label="Members">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||
<circle cx="9" cy="7" r="4"/>
|
||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||
</svg>
|
||
</button>
|
||
<!-- Dynamic indicators (music, streams) inject here via JS -->
|
||
<button id="voice-join-btn" class="btn-voice" style="display:none"><span data-i18n="voice.join">🎤 Join Voice</span></button>
|
||
<div id="voice-active-indicator" class="voice-active-indicator" style="display:none">
|
||
<span class="voice-indicator-icon">🎤</span>
|
||
<span class="voice-indicator-text" data-i18n="header.voice_active">Voice Active</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Welcome Screen -->
|
||
<div id="no-channel-msg" class="welcome-screen">
|
||
<div class="welcome-content">
|
||
<div class="welcome-icon">⬡</div>
|
||
<h2 data-i18n="welcome.title">Welcome to Haven</h2>
|
||
<p data-i18n="welcome.subtitle">Join a channel with a code from your friends,<br>
|
||
or create one if you're the admin.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Area -->
|
||
<div id="message-area" class="message-area" style="display:none">
|
||
<!-- Webcam Grid -->
|
||
<div id="webcam-container" class="webcam-container" style="display:none">
|
||
<div class="webcam-header">
|
||
<span id="webcam-label">📷 <span data-i18n="media.cameras">Cameras</span></span>
|
||
<div class="webcam-header-controls">
|
||
<div class="webcam-layout-picker" id="webcam-layout-picker" data-i18n-title="media.camera_layout" title="Camera layout">
|
||
<button class="stream-layout-btn" id="webcam-layout-btn" data-i18n-title="media.change_layout" title="Change layout">⊞</button>
|
||
<div class="stream-layout-menu" id="webcam-layout-menu">
|
||
<button class="stream-layout-opt active" data-layout="auto" data-i18n-title="media.layout_auto_title" title="Auto grid">⊞ Auto</button>
|
||
<button class="stream-layout-opt" data-layout="vertical" data-i18n-title="media.layout_stack_title" title="Vertical stack">▬ Stack</button>
|
||
<button class="stream-layout-opt" data-layout="side-by-side" data-i18n-title="media.layout_side_title" title="Side by side">◫ Side</button>
|
||
<button class="stream-layout-opt" data-layout="grid-2x2" data-i18n-title="media.layout_2x2_title" title="2×2 Grid">⊟ 2×2</button>
|
||
</div>
|
||
</div>
|
||
<label class="stream-size-label" data-i18n-title="media.camera_size" title="Camera display size">
|
||
📷
|
||
<input type="range" id="webcam-size-slider" class="stream-size-slider" min="15" max="70" value="25">
|
||
</label>
|
||
<button id="webcam-collapse-btn" class="icon-btn small" data-i18n-title="media.minimize_cameras" title="Minimize cameras">─</button>
|
||
<button id="webcam-close-btn" class="icon-btn small" data-i18n-title="media.close_cameras" title="Close cameras">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="webcam-grid" class="webcam-grid"></div>
|
||
</div>
|
||
<!-- Screen Share Viewer (multi-stream) -->
|
||
<div id="screen-share-container" class="screen-share-container" style="display:none">
|
||
<div class="screen-share-header">
|
||
<span id="screen-share-label">🖥️ <span data-i18n="media.streams">Streams</span></span>
|
||
<div class="screen-share-header-controls">
|
||
<div class="stream-layout-picker" id="stream-layout-picker" data-i18n-title="media.stream_layout" title="Stream layout">
|
||
<button class="stream-layout-btn" id="stream-layout-btn" data-i18n-title="media.change_layout" title="Change layout">⊞</button>
|
||
<div class="stream-layout-menu" id="stream-layout-menu">
|
||
<button class="stream-layout-opt active" data-layout="auto" data-i18n-title="media.layout_auto_title" title="Auto grid">⊞ Auto</button>
|
||
<button class="stream-layout-opt" data-layout="vertical" data-i18n-title="media.layout_stack_title" title="Vertical stack">▬ Stack</button>
|
||
<button class="stream-layout-opt" data-layout="side-by-side" data-i18n-title="media.layout_side_title" title="Side by side">◫ Side</button>
|
||
<button class="stream-layout-opt" data-layout="grid-2x2" data-i18n-title="media.layout_2x2_title" title="2×2 Grid">⊟ 2×2</button>
|
||
</div>
|
||
</div>
|
||
<label class="stream-size-label" data-i18n-title="media.stream_size" title="Stream display size">
|
||
🖥️
|
||
<input type="range" id="stream-size-slider" class="stream-size-slider" min="20" max="90" value="50">
|
||
</label>
|
||
<button id="screen-share-minimize" class="icon-btn small" data-i18n-title="media.minimize_streams" title="Minimize streams">─</button>
|
||
<button id="screen-share-close" class="icon-btn small" data-i18n-title="media.close_streams" title="Close streams">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="screen-share-grid" class="screen-share-grid"></div>
|
||
</div>
|
||
<!-- Music Player Panel -->
|
||
<div id="music-panel" class="music-panel" style="display:none">
|
||
<div class="music-panel-header">
|
||
<div class="music-panel-header-left">
|
||
<button id="music-popout-btn" class="icon-btn small" data-i18n-title="media.music_popout" title="Pop out player">⧉</button>
|
||
<div class="music-panel-copy">
|
||
<span id="music-panel-label">🎵 <span data-i18n="media.listening_together">Listening Together</span></span>
|
||
<span id="music-up-next" class="music-up-next" data-i18n="media.music_up_next_empty">Up next: Nothing queued</span>
|
||
</div>
|
||
</div>
|
||
<span id="music-activity-hint" class="music-activity-hint"></span>
|
||
<div class="music-panel-controls">
|
||
<button id="music-queue-btn" class="icon-btn small" data-i18n-title="media.music_queue" title="Queue">☰</button>
|
||
<button id="music-play-pause-btn" class="icon-btn small" data-i18n-title="media.music_play_pause" title="Play/Pause">⏸</button>
|
||
<button id="music-next-btn" class="icon-btn small" data-i18n-title="media.music_next" title="Next track" style="display:none">⏭</button>
|
||
<span id="music-time-current" class="music-time">0:00</span>
|
||
<input type="range" id="music-seek-slider" class="music-seek-slider" min="0" max="100" value="0" step="0.1" data-i18n-title="media.music_seek" title="Seek">
|
||
<span id="music-time-duration" class="music-time">0:00</span>
|
||
<button id="music-mute-btn" class="icon-btn small" data-i18n-title="media.music_mute" title="Mute music">🔊</button>
|
||
<input type="range" id="music-volume-slider" class="stream-vol-slider" min="0" max="100" value="80" data-i18n-title="media.music_volume" title="Music volume">
|
||
<button id="music-close-btn" class="icon-btn small" data-i18n-title="media.minimize" title="Minimize">─</button>
|
||
<button id="music-stop-btn" class="icon-btn small" data-i18n-title="media.music_stop" title="Close / stop music">✕</button>
|
||
</div>
|
||
</div>
|
||
<div id="music-embed-container" class="music-embed-container"></div>
|
||
</div>
|
||
<div id="search-results-panel" class="search-results-panel" style="display:none">
|
||
<div class="search-results-header">
|
||
<span id="search-results-count" data-i18n="header.results">Results</span>
|
||
<button id="search-results-close" class="icon-btn small">×</button>
|
||
</div>
|
||
<div id="search-results-list" class="search-results-list"></div>
|
||
</div>
|
||
<div id="pinned-panel" class="pinned-panel" style="display:none">
|
||
<div class="pinned-panel-header">
|
||
<span id="pinned-count">📌 <span data-i18n="header.pinned_count">Pinned Messages</span></span>
|
||
<button id="pinned-close" class="icon-btn small">×</button>
|
||
</div>
|
||
<div id="pinned-list" class="pinned-list"></div>
|
||
</div>
|
||
<div class="messages" id="messages">
|
||
<!-- Floating "⋯" button for touch-device message actions fallback -->
|
||
<button id="msg-more-btn" aria-label="Message actions">⋯</button>
|
||
</div>
|
||
<div class="typing-indicator" id="typing-indicator"></div>
|
||
<div id="reply-bar" class="reply-bar" style="display:none">
|
||
<span class="reply-bar-text" id="reply-preview-text"></span>
|
||
<button class="reply-bar-close" id="reply-close-btn" title="Cancel reply">×</button>
|
||
</div>
|
||
<div id="image-queue-bar" class="image-queue-bar" style="display:none"></div>
|
||
<div id="upload-progress-bar" class="upload-progress-bar" style="display:none">
|
||
<div class="upload-progress-track"><div class="upload-progress-fill" id="upload-progress-fill"></div></div>
|
||
<span class="upload-progress-text" id="upload-progress-text" data-i18n="header.uploading">Uploading...</span>
|
||
</div>
|
||
<div id="message-input-area" class="message-input-area">
|
||
<div class="input-actions-box">
|
||
<button id="upload-btn" class="btn-upload" data-i18n-title="media.upload_file_title" title="Upload File">
|
||
<svg class="upload-icon-default" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<path d="M21 15l-5-5L5 21"/>
|
||
</svg>
|
||
<span class="upload-icon-clippy">📎</span>
|
||
</button>
|
||
<span class="input-actions-divider"></span>
|
||
<button id="emoji-btn" class="btn-emoji" data-i18n-title="app.input_bar.emoji_btn" title="Emoji">😀</button>
|
||
<span class="input-actions-divider"></span>
|
||
<button id="gif-btn" class="btn-gif" data-i18n-title="app.input_bar.gif_btn" title="Search GIFs"><span data-i18n="app.input_bar.gif_label">GIF</span></button>
|
||
<span class="input-actions-divider"></span>
|
||
<button id="poll-btn" class="btn-poll" data-i18n-title="app.input_bar.poll_btn" title="Create Poll">📊</button>
|
||
</div>
|
||
<div id="emoji-picker" class="emoji-picker" style="display:none"></div>
|
||
<div id="gif-picker" class="gif-picker" style="display:none">
|
||
<div class="gif-picker-header">
|
||
<input type="text" id="gif-search-input" data-i18n-placeholder="header.gif_search_placeholder" placeholder="Search GIPHY..." maxlength="100" autocomplete="off">
|
||
</div>
|
||
<div class="gif-picker-grid" id="gif-grid"></div>
|
||
<div class="gif-picker-footer" data-i18n="header.powered_by_giphy">Powered by GIPHY</div>
|
||
</div>
|
||
<div id="mention-dropdown" class="mention-dropdown" style="display:none"></div>
|
||
<div id="emoji-dropdown" class="emoji-dropdown" style="display:none"></div>
|
||
<div id="slash-dropdown" class="slash-dropdown" style="display:none"></div>
|
||
<textarea id="message-input" data-i18n-placeholder="header.message_placeholder_commands" placeholder="Type a message... (/ for commands)"
|
||
rows="1" maxlength="2000"></textarea>
|
||
<button id="send-btn" class="btn-send" data-i18n-title="media.send_title" title="Send">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<input type="file" id="file-input" style="display:none">
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Sidebar collapse arrow — fixed to the right edge of the viewport -->
|
||
<button id="sidebar-toggle-btn" class="sidebar-collapse-btn" data-i18n-title="app.actions.toggle_members_panel" data-i18n-aria-label="app.actions.toggle_members_panel" title="Toggle members panel" aria-label="Toggle members panel">❯</button>
|
||
|
||
<!-- ─── Right Sidebar ───────────────────────────── -->
|
||
<aside class="right-sidebar" id="right-sidebar">
|
||
<div class="right-sidebar-resize-handle" id="right-sidebar-resize-handle"></div>
|
||
<!-- Voice users section -->
|
||
<div class="right-sidebar-voice" id="right-sidebar-voice">
|
||
<div class="panel">
|
||
<div class="voice-panel-header">
|
||
<h5 class="panel-title" style="margin-bottom:0">🔊 <span data-i18n="right_sidebar.voice_title">Voice</span></h5>
|
||
<button id="mobile-right-close" class="mobile-panel-close mobile-panel-close-right" data-i18n-title="modals.common.close" data-i18n-aria-label="right_sidebar.close_panel" title="Close" aria-label="Close panel">×</button>
|
||
<div class="voice-header-actions">
|
||
<button id="voice-mute-btn-header" class="voice-header-btn" data-i18n-title="voice.mute" title="Mute" style="display:none">🎙️</button>
|
||
<button id="voice-deafen-btn-header" class="voice-header-btn" data-i18n-title="voice.deafen" title="Deafen" style="display:none">🔊</button>
|
||
</div>
|
||
</div>
|
||
<button id="voice-join-mobile" class="btn-voice mobile-voice-join" style="display:none">🎤 <span data-i18n="voice.join">Join Voice</span></button>
|
||
<div id="voice-users" class="user-list">
|
||
<p class="muted-text" data-i18n="right_sidebar.no_one_in_voice">No one in voice</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Scrollable users section -->
|
||
<div class="right-sidebar-users" id="right-sidebar-users">
|
||
<div class="panel">
|
||
<h5 class="panel-title">👥 <span data-i18n="right_sidebar.online_title">Online</span></h5>
|
||
<div id="online-users" class="user-list">
|
||
<p class="muted-text" data-i18n="right_sidebar.select_channel">Select a channel</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Voice settings sub-panel (slides up above controls) -->
|
||
<div id="voice-settings-panel" class="voice-settings-panel" style="display:none">
|
||
<div class="voice-settings-section-label">🎧 <span data-i18n="voice_settings.audio_devices">Audio Devices</span></div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.input">Input</label>
|
||
<select id="voice-input-device" class="voice-settings-select">
|
||
<option value="" data-i18n="voice_settings.default_mic">Default Microphone</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.output">Output</label>
|
||
<select id="voice-output-device" class="voice-settings-select">
|
||
<option value="" data-i18n="voice_settings.default_speaker">Default Speaker</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.camera">Camera</label>
|
||
<select id="voice-cam-device" class="voice-settings-select">
|
||
<option value="" data-i18n="voice_settings.default_camera">Default Camera</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-divider"></div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label">🤫 <span data-i18n="voice_settings.noise_reduction">Noise Reduction</span></label>
|
||
<select id="voice-noise-mode" class="voice-settings-select" data-i18n-title="voice_settings.noise_mode_hint" title="Off = no processing, Gate = silence below threshold, Suppress = AI noise removal (RNNoise)">
|
||
<option value="off" data-i18n="voice_settings.off">Off</option>
|
||
<option value="gate" data-i18n="voice_settings.noise_gate">Noise Gate</option>
|
||
<option value="suppress" data-i18n="voice_settings.suppression_ai">Suppression (AI)</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row" id="noise-gate-row">
|
||
<label class="voice-settings-label" style="font-size:0.72rem;opacity:0.8" data-i18n="voice_settings.gate_sensitivity">Gate Sensitivity</label>
|
||
<input type="range" id="voice-ns-slider" class="ns-slider" min="0" max="100" value="10" data-i18n-title="voice_settings.gate_slider_hint" title="Noise gate: 0 = off, 100 = aggressive">
|
||
</div>
|
||
<div class="voice-settings-row mic-meter-row">
|
||
<label class="voice-settings-label">🎙️ <span data-i18n="voice_settings.mic_level">Mic Level</span></label>
|
||
<div class="mic-meter" id="mic-level-meter">
|
||
<div class="mic-meter-fill" id="mic-meter-fill"></div>
|
||
<div class="mic-meter-threshold" id="mic-meter-threshold" data-i18n-title="voice_settings.gate_threshold" title="Gate threshold"></div>
|
||
</div>
|
||
</div>
|
||
<div class="voice-settings-divider"></div>
|
||
<div class="voice-settings-section-label">🖥️ <span data-i18n="voice_settings.screen_share_quality">Screen Share Quality</span></div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.resolution">Resolution</label>
|
||
<select id="screen-res-select" class="voice-settings-select">
|
||
<option value="source" data-i18n="voice_settings.resolution_source">Source</option>
|
||
<option value="720">720p</option>
|
||
<option value="1080" selected>1080p</option>
|
||
<option value="1440">1440p</option>
|
||
</select>
|
||
</div>
|
||
<div class="voice-settings-row">
|
||
<label class="voice-settings-label" data-i18n="voice_settings.frame_rate">Frame Rate</label>
|
||
<select id="screen-fps-select" class="voice-settings-select">
|
||
<option value="15">15 fps</option>
|
||
<option value="30" selected>30 fps</option>
|
||
<option value="60">60 fps</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- Voice controls bar (pinned above settings) -->
|
||
<div id="voice-panel" class="voice-panel" style="display:none">
|
||
<button id="voice-cam-btn" class="voice-panel-btn" data-i18n-title="voice.panel.camera" title="Camera">📷</button>
|
||
<button id="screen-share-btn" class="voice-panel-btn" data-i18n-title="voice.screen_share" title="Share Screen">🖥️</button>
|
||
<button id="voice-soundboard-btn" class="voice-panel-btn" data-i18n-title="voice.panel.soundboard" title="Soundboard">🎵</button>
|
||
<button id="voice-listen-together-btn" class="voice-panel-btn" data-i18n-title="voice.panel.listen_together" title="Listen Together">🎶</button>
|
||
<span class="voice-panel-divider"></span>
|
||
<button id="voice-settings-toggle" class="voice-panel-btn" data-i18n-title="voice.panel.settings" title="Voice Settings">⚙️</button>
|
||
<button id="voice-leave-sidebar-btn" class="voice-panel-btn voice-panel-leave" data-i18n-title="voice.leave" title="Leave Voice">✕</button>
|
||
</div>
|
||
<!-- Settings button pinned at bottom -->
|
||
<div class="panel sidebar-settings-panel">
|
||
<button class="btn-settings-popout" id="open-settings-btn" data-i18n-title="app.actions.settings" title="Settings">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||
</svg>
|
||
<span data-i18n="app.actions.settings">Settings</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Mobile overlay (click to close sidebars) -->
|
||
<div id="mobile-overlay" class="mobile-overlay"></div>
|
||
</div>
|
||
|
||
<!-- ─── Status Bar (bottom) ───────────────────────── -->
|
||
<div class="status-bar" id="status-bar">
|
||
<div class="status-item">
|
||
<span class="led on" id="status-server-led"></span>
|
||
<span class="label" data-i18n="status_bar.server">Server</span>
|
||
<span class="value" id="status-server-text" data-i18n="app.status.connected">Connected</span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="label" data-i18n="status_bar.ping">Ping</span>
|
||
<span class="value" id="status-ping">--</span>
|
||
<span class="label" data-i18n="status_bar.ping_unit">ms</span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="label" data-i18n="status_bar.channel">Channel</span>
|
||
<span class="value" id="status-channel" data-i18n="status_bar.channel_none">None</span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item status-online-trigger" id="status-online-trigger" title="Click to see who's online">
|
||
<span class="label" data-i18n="status_bar.online">Online</span>
|
||
<span class="value" id="status-online-count">0</span>
|
||
</div>
|
||
<span class="spacer"></span>
|
||
<div class="status-item">
|
||
<span class="value" id="status-clock"></span>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="status-item">
|
||
<span class="value" id="status-version"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Online users overlay (floats above status bar) -->
|
||
<div id="online-overlay" class="online-overlay" style="display:none">
|
||
<div class="online-overlay-header">
|
||
<span class="online-overlay-title" data-i18n="status_bar.users_overlay_title">Users</span>
|
||
<button id="online-overlay-close" class="icon-btn small" title="Close">×</button>
|
||
</div>
|
||
<div id="online-overlay-list" class="online-overlay-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hidden audio container for voice streams -->
|
||
<div id="audio-container" style="display:none"></div>
|
||
|
||
<!-- Toast notifications -->
|
||
<div id="toast-container"></div>
|
||
|
||
<!-- Move Messages Toolbar (floating above messages) -->
|
||
<div id="move-msg-toolbar" class="move-msg-toolbar" style="display:none">
|
||
<span id="move-msg-count">0 selected</span>
|
||
<button class="btn-sm btn-accent" id="move-msg-move-btn" data-i18n="modals.move_messages.move_to">Move to...</button>
|
||
<button class="btn-sm" id="move-msg-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
|
||
<!-- Move Messages Channel Picker Modal -->
|
||
<div class="modal-overlay" id="move-msg-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.move_messages.title">Move Messages</h3>
|
||
<p class="modal-desc" id="move-msg-desc" data-i18n="modals.move_messages.desc">Select a destination channel</p>
|
||
<div class="move-msg-channel-list" id="move-msg-channel-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="move-msg-modal-cancel" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- First-Time Setup Wizard (Admin only) -->
|
||
<div class="modal-overlay" id="setup-wizard-modal" style="display:none">
|
||
<div class="modal modal-wizard">
|
||
<div class="wizard-header">
|
||
<span class="wizard-hex">⬡</span>
|
||
<h2 data-i18n="modals.wizard.title">Welcome to Haven</h2>
|
||
<p class="wizard-subtitle" data-i18n="modals.wizard.subtitle">Let's get your server ready. This only takes a minute.</p>
|
||
</div>
|
||
|
||
<div class="wizard-steps">
|
||
<!-- Step indicators -->
|
||
<div class="wizard-step-indicators">
|
||
<div class="wizard-indicator active" data-step="1">1</div>
|
||
<div class="wizard-indicator-line"></div>
|
||
<div class="wizard-indicator" data-step="2">2</div>
|
||
<div class="wizard-indicator-line"></div>
|
||
<div class="wizard-indicator" data-step="3">3</div>
|
||
<div class="wizard-indicator-line"></div>
|
||
<div class="wizard-indicator" data-step="4">4</div>
|
||
</div>
|
||
|
||
<!-- Step 1: Server basics -->
|
||
<div class="wizard-step" id="wizard-step-1" style="display:block">
|
||
<h3>🏠 <span data-i18n="modals.wizard.step1_title">Your Server</span></h3>
|
||
<p data-i18n="modals.wizard.step1_desc">Everything is already running. Here's what was set up automatically:</p>
|
||
<div class="wizard-checklist">
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_ssl">SSL certificates generated (encrypted connections)</span></div>
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_db">Database created</span></div>
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_keys">Security keys generated</span></div>
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_admin">Admin account created (that's you!)</span></div>
|
||
</div>
|
||
<div class="wizard-info-box">
|
||
<strong data-i18n="modals.wizard.server_name_label">Your server name:</strong>
|
||
<input type="text" id="wizard-server-name" class="wizard-input" value="Haven" placeholder="e.g. My Server, The Hideout" data-i18n-placeholder="modals.wizard.server_name_placeholder">
|
||
<span class="wizard-hint" data-i18n="modals.wizard.server_name_hint">This is what your friends see in their server list.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Create first channel -->
|
||
<div class="wizard-step" id="wizard-step-2" style="display:none">
|
||
<h3>📢 <span data-i18n="modals.wizard.step2_title">Create Your First Channel</span></h3>
|
||
<p data-i18n="modals.wizard.step2_desc">Channels are where conversations happen. Create one to get started.</p>
|
||
<div class="wizard-info-box">
|
||
<strong data-i18n="modals.wizard.channel_name_label">Channel name:</strong>
|
||
<input type="text" id="wizard-channel-name" class="wizard-input" value="General" placeholder="e.g. General, Gaming, Hangout" data-i18n-placeholder="modals.wizard.channel_name_placeholder">
|
||
<span class="wizard-hint" data-i18n="modals.wizard.channel_name_hint">You can create more channels later from the sidebar.</span>
|
||
</div>
|
||
<div id="wizard-channel-result" style="display:none">
|
||
<div class="wizard-check" id="wizard-channel-created">✅ Channel created!</div>
|
||
<div class="wizard-code-display">
|
||
<span data-i18n="modals.wizard.invite_code">Invite code:</span>
|
||
<code id="wizard-channel-code"></code>
|
||
<button id="wizard-copy-code" class="wizard-btn-small" data-i18n="modals.wizard.copy_btn">Copy</button>
|
||
</div>
|
||
<span class="wizard-hint" data-i18n="modals.wizard.share_hint">Share this code with friends so they can join this channel.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Connectivity check -->
|
||
<div class="wizard-step" id="wizard-step-3" style="display:none">
|
||
<h3>🌐 <span data-i18n="modals.wizard.step3_title">Internet Access</span></h3>
|
||
<p data-i18n="modals.wizard.step3_desc">Want friends outside your WiFi to connect? Let's check if your server is reachable from the internet.</p>
|
||
<div id="wizard-port-checking" style="display:none">
|
||
<div class="wizard-spinner"></div>
|
||
<span data-i18n="modals.wizard.checking_port">Checking port reachability...</span>
|
||
</div>
|
||
<div id="wizard-port-result" style="display:none"></div>
|
||
<button id="wizard-check-port-btn" class="wizard-btn-secondary">🔍 <span data-i18n="modals.wizard.check_connection_btn">Check My Connection</span></button>
|
||
<div class="wizard-info-box" style="margin-top:16px">
|
||
<span class="wizard-hint" data-i18n-html="modals.wizard.lan_only_hint"><strong>LAN only?</strong> If your friends are on the same WiFi, you can skip this — it will work automatically.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 4: You're ready -->
|
||
<div class="wizard-step" id="wizard-step-4" style="display:none">
|
||
<h3>🎉 <span data-i18n="modals.wizard.step4_title">You're All Set!</span></h3>
|
||
<p data-i18n="modals.wizard.step4_desc">Haven is running and ready for your friends.</p>
|
||
<div class="wizard-checklist">
|
||
<div class="wizard-check">✅ <span data-i18n="modals.wizard.check_configured">Server configured</span></div>
|
||
<div class="wizard-check" id="wizard-summary-channel">✅ First channel created</div>
|
||
<div class="wizard-check" id="wizard-summary-port">⏭️ Port check skipped</div>
|
||
</div>
|
||
<div class="wizard-info-box">
|
||
<strong data-i18n="modals.wizard.how_friends_join">How friends join:</strong>
|
||
<ol class="wizard-steps-list">
|
||
<li>They open <code id="wizard-final-url">https://YOUR_IP:3000</code> in their browser</li>
|
||
<li data-i18n="modals.wizard.step_cert">Click "Advanced" → "Proceed" on the certificate warning</li>
|
||
<li data-i18n="modals.wizard.step_register">Register a username and password</li>
|
||
<li data-i18n="modals.wizard.step_code">Enter the channel invite code you share with them</li>
|
||
</ol>
|
||
</div>
|
||
<div class="wizard-info-box">
|
||
<span class="wizard-hint">💡 <span data-i18n-html="modals.wizard.tip"><strong>Tip:</strong> You can always access settings with the ⚙️ gear icon in the sidebar. This wizard won't show again.</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wizard-footer">
|
||
<button id="wizard-back-btn" class="wizard-btn-secondary" style="display:none" data-i18n="modals.wizard.back_btn">← Back</button>
|
||
<div style="flex:1"></div>
|
||
<button id="wizard-skip-btn" class="wizard-btn-text" data-i18n="modals.wizard.skip_btn">Skip Setup</button>
|
||
<button id="wizard-next-btn" class="wizard-btn-primary" data-i18n="modals.wizard.next_btn">Next →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel Code Settings Modal (Admin) -->
|
||
<div class="modal-overlay" id="code-settings-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.code_settings.title">Channel Code Settings</h3>
|
||
<p class="modal-desc" id="code-settings-channel-name"></p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.code_settings.visibility_label">Code Visibility</label>
|
||
<select id="code-visibility-select" class="form-select">
|
||
<option value="public" data-i18n="modals.code_settings.visibility_public">Public — everyone can see the code</option>
|
||
<option value="private" data-i18n="modals.code_settings.visibility_private">Private — only admins see the code</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.code_settings.mode_label">Code Mode</label>
|
||
<select id="code-mode-select" class="form-select">
|
||
<option value="static" data-i18n="modals.code_settings.mode_static">Static — code never changes</option>
|
||
<option value="dynamic" data-i18n="modals.code_settings.mode_dynamic">Dynamic — code rotates automatically</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="rotation-type-group" style="display:none">
|
||
<label data-i18n="modals.code_settings.rotation_label">Rotation Trigger</label>
|
||
<select id="code-rotation-type-select" class="form-select">
|
||
<option value="time" data-i18n="modals.code_settings.rotation_time">Time-based — rotate every X minutes</option>
|
||
<option value="joins" data-i18n="modals.code_settings.rotation_joins">Join-based — rotate after X new joins</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="rotation-interval-group" style="display:none">
|
||
<label id="rotation-interval-label" data-i18n="modals.code_settings.interval_label">Rotation Interval (minutes)</label>
|
||
<input type="number" id="code-rotation-interval" min="1" max="10000" value="60">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="code-settings-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="code-rotate-now-btn">🔄 <span data-i18n="modals.code_settings.rotate_now_btn">Rotate Now</span></button>
|
||
<button class="btn-sm btn-accent" id="code-settings-save-btn" data-i18n="modals.common.save">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Server Modal -->
|
||
<div class="modal-overlay" id="add-server-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 id="add-server-modal-title" data-i18n="modals.add_server.title">Add a Server</h3>
|
||
<p class="modal-desc" data-i18n="modals.add_server.desc">Connect to a friend's Haven server</p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.add_server.name_label">Server Name</label>
|
||
<input type="text" id="add-server-name-input" placeholder='e.g. Jake's Haven' maxlength="30">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.add_server.address_label">Server Address</label>
|
||
<input type="text" id="server-url-input" placeholder="e.g. https://192.168.1.5:3000">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.add_server.icon_label">Icon URL</label> <span class="muted-text" data-i18n="modals.add_server.icon_optional">(optional)</span>
|
||
<input type="text" id="add-server-icon-input" placeholder="https://... or leave blank">
|
||
</div>
|
||
<label class="eula-check-row" style="margin-bottom:12px;gap:8px">
|
||
<input type="checkbox" id="server-auto-icon" checked>
|
||
<span data-i18n="modals.add_server.auto_icon">Auto-pull icon from server</span>
|
||
</label>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-server-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="save-server-btn" data-i18n="modals.add_server.add_btn">Add Server</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rename / Profile Modal -->
|
||
<div class="modal-overlay" id="rename-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 data-i18n="modals.edit_profile.title">Edit Profile</h3>
|
||
|
||
<!-- Avatar Customizer -->
|
||
<div class="avatar-settings" style="margin-bottom: 16px;">
|
||
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.avatar_title">Avatar</h5>
|
||
<div class="avatar-upload-row" style="margin-bottom: 10px;">
|
||
<div class="avatar-upload-preview" id="avatar-upload-preview"></div>
|
||
<button type="button" class="btn-sm" id="avatar-upload-btn" data-i18n="modals.edit_profile.upload_btn">Upload</button>
|
||
<button type="button" class="btn-sm" id="avatar-remove-btn" data-i18n="modals.edit_profile.clear_btn">Clear</button>
|
||
<input type="file" id="avatar-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
|
||
</div>
|
||
<small class="muted-text" style="display:block;margin-top:-4px;margin-bottom:8px;opacity:0.6" data-i18n="modals.edit_profile.avatar_hint">JPG, PNG, GIF (animated!), WebP · Max 2 MB</small>
|
||
<h5 class="settings-section-subtitle" data-i18n="modals.edit_profile.shape_title">Shape</h5>
|
||
<div class="avatar-shape-picker" id="avatar-shape-picker">
|
||
<button type="button" class="avatar-shape-btn active" data-shape="circle" title="Circle"><div class="shape-preview shape-circle"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="rounded" title="Rounded"><div class="shape-preview shape-rounded"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="squircle" title="Squircle"><div class="shape-preview shape-squircle"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="hex" title="Hexagon"><div class="shape-preview shape-hex"></div></button>
|
||
<button type="button" class="avatar-shape-btn" data-shape="diamond" title="Diamond"><div class="shape-preview shape-diamond"></div></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Display Name -->
|
||
<p class="modal-desc" data-i18n="modals.edit_profile.display_name_desc">Letters, numbers, underscores, and spaces (2–20 chars)</p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.edit_profile.display_name_label">Display Name</label>
|
||
<input type="text" id="rename-input" data-i18n-placeholder="modals.edit_profile.display_name_placeholder" placeholder="New display name..." maxlength="20">
|
||
</div>
|
||
|
||
<!-- Bio -->
|
||
<div class="form-group" style="margin-top:8px">
|
||
<label data-i18n="modals.edit_profile.bio_label">Bio</label> <span class="muted-text" data-i18n="modals.edit_profile.bio_max">(max 190 chars)</span>
|
||
<textarea id="edit-profile-bio" class="edit-profile-textarea" maxlength="190" data-i18n-placeholder="modals.edit_profile.bio_placeholder" placeholder="Tell people about yourself…" rows="2"></textarea>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<span id="avatar-save-status" class="settings-hint" style="flex:1"></span>
|
||
<button class="btn-sm" id="cancel-rename-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="save-rename-btn" data-i18n="modals.common.save">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Action Modal (Kick/Ban/Mute/Delete) -->
|
||
<div class="modal-overlay" id="admin-action-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3 id="admin-action-title" data-i18n="modals.admin_action.title">Moderate User</h3>
|
||
<p class="modal-desc" id="admin-action-desc"></p>
|
||
<div class="form-group" id="admin-reason-group">
|
||
<label data-i18n="modals.admin_action.reason_label">Reason (optional)</label>
|
||
<input type="text" id="admin-action-reason" data-i18n-placeholder="modals.admin_action.reason_placeholder" placeholder="Reason..." maxlength="200">
|
||
</div>
|
||
<div class="form-group" id="admin-duration-group" style="display:none">
|
||
<label data-i18n="modals.admin_action.duration_label">Duration (minutes)</label>
|
||
<input type="number" id="admin-action-duration" value="10" min="1" max="43200" step="1">
|
||
</div>
|
||
<div class="form-group" id="admin-scrub-group" style="display:none">
|
||
<label class="toggle-row">
|
||
<span data-i18n="modals.admin_action.scrub_label">Scrub messages</span>
|
||
<input type="checkbox" id="admin-scrub-checkbox">
|
||
</label>
|
||
<div id="admin-scrub-scope-row" style="display:none;margin-top:6px;">
|
||
<label data-i18n="modals.admin_action.scope_label">Scope</label>
|
||
<select id="admin-scrub-scope" class="settings-select" style="width:100%;margin-top:4px;">
|
||
<option value="channel" data-i18n="modals.admin_action.scope_channel">This channel only</option>
|
||
<option value="server" data-i18n="modals.admin_action.scope_server">Server-wide</option>
|
||
</select>
|
||
</div>
|
||
<small class="settings-hint" data-i18n="modals.admin_action.scrub_hint">Removes the user's messages (archived/protected messages are preserved)</small>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-admin-action-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent btn-danger-fill" id="confirm-admin-action-btn" data-i18n="modals.common.confirm">Confirm</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Popout Modal -->
|
||
<div class="modal-overlay" id="settings-modal" style="display:none">
|
||
<div class="modal modal-settings">
|
||
<div class="settings-header">
|
||
<h3>⚙️ <span data-i18n="settings.title">Settings</span></h3>
|
||
<button class="settings-close-btn" id="close-settings-btn">×</button>
|
||
</div>
|
||
|
||
<div class="settings-layout">
|
||
<nav class="settings-nav" id="settings-nav">
|
||
<div class="settings-nav-group" data-i18n="settings.nav.user_group">User</div>
|
||
<div class="settings-nav-item" data-target="section-language">🗣️ <span data-i18n="settings.nav.language">Language</span></div>
|
||
<div class="settings-nav-item active" data-target="section-density">📐 <span data-i18n="settings.nav.layout">Layout</span></div>
|
||
<div class="settings-nav-item" data-target="section-sounds">🔔 <span data-i18n="settings.nav.sounds">Sounds</span></div>
|
||
<div class="settings-nav-item" data-target="section-push">📲 <span data-i18n="settings.nav.push">Push</span></div>
|
||
<div class="settings-nav-item" data-target="section-password">🔒 <span data-i18n="settings.nav.password">Password</span></div>
|
||
<div class="settings-nav-item" data-target="section-2fa">🔐 <span data-i18n="settings.nav.two_factor">Two-Factor</span></div>
|
||
<div class="settings-nav-item" data-target="section-recovery">🔑 <span data-i18n="settings.nav.recovery">Recovery</span></div>
|
||
<div class="settings-nav-item" data-target="section-delete-account">⚠️ <span data-i18n="settings.nav.account">Account</span></div>
|
||
<div class="settings-nav-item" data-target="section-plugins">🧩 <span data-i18n="settings.nav.plugins">Plugins & Themes</span></div>
|
||
<div class="settings-nav-item" data-target="section-desktop-shortcuts" id="desktop-shortcuts-nav" style="display:none">⌨️ <span data-i18n="settings.nav.shortcuts">Shortcuts</span></div>
|
||
<div class="settings-nav-item" data-target="section-desktop-app" id="desktop-app-nav" style="display:none">🖥️ <span data-i18n="settings.nav.desktop_app">Desktop App</span></div>
|
||
<div class="settings-nav-group settings-nav-admin" style="display:none" data-i18n="settings.nav.admin_group">Admin</div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-branding" style="display:none">🏠 <span data-i18n="settings.nav.branding">Branding</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-members" style="display:none">👥 <span data-i18n="settings.nav.members">Members</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-whitelist" style="display:none">🛡️ <span data-i18n="settings.nav.whitelist">Whitelist</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-invite" style="display:none">🌐 <span data-i18n="settings.nav.invite">Invite Code</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-cleanup" style="display:none">🗑️ <span data-i18n="settings.nav.cleanup">Cleanup</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-uploads" style="display:none">📁 <span data-i18n="settings.nav.limits">Limits</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-sounds-admin" style="display:none">🔊 <span data-i18n="settings.nav.admin_sounds">Sounds</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-emojis" style="display:none">😎 <span data-i18n="settings.nav.emojis">Emojis</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-roles" style="display:none">👑 <span data-i18n="settings.nav.roles">Roles</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-tunnel" style="display:none">🧭 <span data-i18n="settings.nav.tunnel">Tunnel</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-bots" style="display:none">🤖 <span data-i18n="settings.nav.bots">Bots</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-import" style="display:none">📦 <span data-i18n="settings.nav.import">Import</span></div>
|
||
<div class="settings-nav-item settings-nav-admin" data-target="section-modmode" style="display:none">🔧 <span data-i18n="settings.nav.mod_mode">Mod Mode</span></div>
|
||
</nav>
|
||
<div class="settings-body">
|
||
|
||
<!-- Desktop Shortcuts Section (only shown in Haven Desktop) -->
|
||
<div class="settings-section" id="section-desktop-shortcuts" style="display:none">
|
||
<h5 class="settings-section-title">⌨️ <span data-i18n="settings.desktop_shortcuts_section.title">Desktop Shortcuts</span></h5>
|
||
<p class="settings-desc" data-i18n="settings.desktop_shortcuts_section.desc">Global keyboard shortcuts — active even when Haven is in the background. Click a row to rebind, or "Clear" to disable.</p>
|
||
<div class="shortcut-table" id="shortcut-table">
|
||
<div class="shortcut-row" id="shortcut-row-mute">
|
||
<span class="shortcut-label" data-i18n="settings.desktop_shortcuts_section.mute_unmute">Mute / Unmute</span>
|
||
<span class="shortcut-key" id="shortcut-key-mute">—</span>
|
||
<div class="shortcut-btns">
|
||
<button class="btn-xs shortcut-record-btn" data-action="mute" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
|
||
<button class="btn-xs shortcut-clear-btn" data-action="mute" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div class="shortcut-row" id="shortcut-row-deafen">
|
||
<span class="shortcut-label" data-i18n="settings.desktop_shortcuts_section.deafen_undeafen">Deafen / Undeafen</span>
|
||
<span class="shortcut-key" id="shortcut-key-deafen">—</span>
|
||
<div class="shortcut-btns">
|
||
<button class="btn-xs shortcut-record-btn" data-action="deafen" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
|
||
<button class="btn-xs shortcut-clear-btn" data-action="deafen" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
<div class="shortcut-row" id="shortcut-row-ptt">
|
||
<span class="shortcut-label"><span data-i18n="settings.desktop_shortcuts_section.ptt">Push-to-Talk</span> <span class="muted-text" style="font-weight:400;font-size:0.85em" data-i18n="settings.desktop_shortcuts_section.ptt_toggle">(toggle)</span></span>
|
||
<span class="shortcut-key" id="shortcut-key-ptt" data-i18n="settings.desktop_shortcuts_section.not_set">Not set</span>
|
||
<div class="shortcut-btns">
|
||
<button class="btn-xs shortcut-record-btn" data-action="ptt" data-i18n="settings.desktop_shortcuts_section.change_btn">Change</button>
|
||
<button class="btn-xs shortcut-clear-btn" data-action="ptt" data-i18n="settings.desktop_shortcuts_section.clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="settings-hint" id="shortcut-status" style="margin-top:8px"></p>
|
||
</div>
|
||
|
||
<!-- Layout Density Section -->
|
||
|
||
<!-- Desktop App Settings Section (only shown in Haven Desktop) -->
|
||
<div class="settings-section" id="section-desktop-app" style="display:none">
|
||
<h5 class="settings-section-title">🖥️ <span data-i18n="settings.desktop_app_section.title">Desktop App</span></h5>
|
||
<p class="settings-desc" data-i18n="settings.desktop_app_section.desc">Settings specific to Haven Desktop.</p>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-start-on-login">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.start_on_login">Start on Login</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.start_on_login_hint">Launch Haven Desktop automatically when you log in to your computer.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row" id="pref-start-hidden-row" style="display:none">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-start-hidden">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.start_hidden">Start Hidden to Tray</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.start_hidden_hint">When launching on login, start minimized to the system tray instead of showing the window.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-minimize-to-tray">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.minimize_to_tray">Minimize to Tray on Close</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.minimize_to_tray_hint">Closing the window hides Haven to the system tray instead of quitting.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-hide-menu-bar">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.hide_menu_bar">Hide Menu Bar</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.hide_menu_bar_hint">Hide the File / Edit / View / Window / Help toolbar. Press Alt to temporarily show it.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row">
|
||
<label class="toggle-row">
|
||
<input type="checkbox" id="pref-force-sdr">
|
||
<span class="toggle-label" data-i18n="settings.desktop_app_section.force_sdr">Force SDR (sRGB) Color</span>
|
||
</label>
|
||
<p class="settings-hint" data-i18n="settings.desktop_app_section.force_sdr_hint">Fix washed-out or over-saturated colors on HDR monitors. Requires restart.</p>
|
||
</div>
|
||
|
||
<div class="desktop-pref-row" style="margin-top:16px;padding-top:12px;border-top:1px solid var(--border)">
|
||
<span class="settings-hint" id="desktop-version-info" style="opacity:0.6"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Language Section -->
|
||
<div class="settings-section" id="section-language">
|
||
<h5 class="settings-section-title">🗣️ <span data-i18n="settings.language_section.title">Language</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.language_section.label">Language</span>
|
||
<select id="language-select" class="settings-select">
|
||
<option value="en">🇬🇧 English</option>
|
||
<option value="fr">🇫🇷 Français</option>
|
||
<option value="de">🇩🇪 Deutsch</option>
|
||
<option value="es">🇪🇸 Español</option>
|
||
<option value="ru">🇷🇺 Русский</option>
|
||
<option value="zh">🇨🇳 中文</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.language_section.hint">Changes apply immediately</small>
|
||
</div>
|
||
|
||
<div class="settings-section" id="section-density">
|
||
<h5 class="settings-section-title">📐 <span data-i18n="settings.layout_density.title">Layout Density</span></h5>
|
||
<div class="density-picker" id="density-picker">
|
||
<button type="button" class="density-btn" data-density="compact" data-i18n-title="settings.layout_density.compact_hint" title="Compact — tighter spacing, smaller avatars">
|
||
<span class="density-icon">▪️</span>
|
||
<span class="density-label" data-i18n="settings.layout_density.compact">Compact</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-density="cozy" data-i18n-title="settings.layout_density.cozy_hint" title="Cozy — default balanced layout">
|
||
<span class="density-icon">◾</span>
|
||
<span class="density-label" data-i18n="settings.layout_density.cozy">Cozy</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-density="spacious" data-i18n-title="settings.layout_density.spacious_hint" title="Spacious — more breathing room">
|
||
<span class="density-icon">⬛</span>
|
||
<span class="density-label" data-i18n="settings.layout_density.spacious">Spacious</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Font Size -->
|
||
<div class="settings-section" id="section-font-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🔤 <span data-i18n="settings.font_size.title">Font Size</span></h5>
|
||
<div class="font-size-picker" id="font-size-picker">
|
||
<button type="button" class="density-btn" data-fontsize="small" data-i18n-title="settings.font_size.small_hint" title="Small — 13px base">
|
||
<span class="density-icon" style="font-size:11px">A</span>
|
||
<span class="density-label" data-i18n="settings.font_size.small">Small</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-fontsize="normal" data-i18n-title="settings.font_size.normal_hint" title="Normal — 15px base (default)">
|
||
<span class="density-icon" style="font-size:14px">A</span>
|
||
<span class="density-label" data-i18n="settings.font_size.normal">Normal</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-fontsize="large" data-i18n-title="settings.font_size.large_hint" title="Large — 17px base">
|
||
<span class="density-icon" style="font-size:17px">A</span>
|
||
<span class="density-label" data-i18n="settings.font_size.large">Large</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-fontsize="x-large" data-i18n-title="settings.font_size.xl_hint" title="Extra Large — 20px base">
|
||
<span class="density-icon" style="font-size:20px">A</span>
|
||
<span class="density-label" data-i18n="settings.font_size.xl">XL</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Emoji Reaction Size -->
|
||
<div class="settings-section" id="section-emoji-size" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">😀 Reaction Size</h5>
|
||
<div class="font-size-picker" id="emoji-size-picker">
|
||
<button type="button" class="density-btn" data-emojisize="small" title="Small reactions">
|
||
<span class="density-icon" style="font-size:11px">😀</span>
|
||
<span class="density-label">Small</span>
|
||
</button>
|
||
<button type="button" class="density-btn active" data-emojisize="normal" title="Normal reactions (default)">
|
||
<span class="density-icon" style="font-size:14px">😀</span>
|
||
<span class="density-label">Normal</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-emojisize="large" title="Large reactions">
|
||
<span class="density-icon" style="font-size:18px">😀</span>
|
||
<span class="density-label">Large</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-emojisize="x-large" title="Extra large reactions">
|
||
<span class="density-icon" style="font-size:22px">😀</span>
|
||
<span class="density-label">XL</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Image Display Mode -->
|
||
<div class="settings-section" id="section-density" style="border-top: 1px solid var(--border-light); padding-top: 16px; margin-top: 8px;">
|
||
<h5 class="settings-section-title">🖼️ <span data-i18n="settings.image_display.title">Image Display</span></h5>
|
||
<div class="density-picker" id="image-mode-picker">
|
||
<button type="button" class="density-btn active" data-image-mode="thumbnail" data-i18n-title="settings.image_display.thumbnail_hint" title="Thumbnail — small preview, click to enlarge">
|
||
<span class="density-icon">🔍</span>
|
||
<span class="density-label" data-i18n="settings.image_display.thumbnail">Thumbnail</span>
|
||
</button>
|
||
<button type="button" class="density-btn" data-image-mode="full" data-i18n-title="settings.image_display.full_hint" title="Full — large inline images like Discord">
|
||
<span class="density-icon">🖼️</span>
|
||
<span class="density-label" data-i18n="settings.image_display.full">Full</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sounds Section -->
|
||
<div class="settings-section" id="section-sounds">
|
||
<h5 class="settings-section-title">🔔 <span data-i18n="settings.sounds_section.title">Sounds</span></h5>
|
||
<div class="notif-settings">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.notifications">Notifications</span>
|
||
<input type="checkbox" id="notif-enabled" checked>
|
||
</label>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.auto_accept_streams">Auto-accept Streams</span>
|
||
<input type="checkbox" id="auto-accept-streams" checked>
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.auto_accept_streams_hint">Automatically open screen shares when someone starts streaming. Turn off to join manually.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.hide_voice_panel">Hide Voice Panel</span>
|
||
<input type="checkbox" id="hide-voice-panel">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.hide_voice_panel_hint">Hide the voice users panel on desktop. Voice users are still shown in channel indicators.</small>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.sounds_section.sidebar_voice_controls">Sidebar Voice Controls</span>
|
||
<input type="checkbox" id="sidebar-voice-controls">
|
||
</label>
|
||
<small class="settings-hint" style="display:block;margin-bottom:6px" data-i18n="settings.sounds_section.sidebar_voice_controls_hint">Show mute/deafen buttons in the sidebar instead of the voice panel header.</small>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.volume">Volume</span>
|
||
<input type="range" id="notif-volume" min="0" max="100" value="50" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.messages">Messages</span>
|
||
<select id="notif-msg-sound">
|
||
<option value="ping">Ping</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="bell">Bell</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="alert">Alert</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.msg_sent">Msg Sent</span>
|
||
<select id="notif-sent-sound">
|
||
<option value="swoosh">Swoosh</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div class="notif-divider"></div>
|
||
<label class="volume-row">
|
||
<span data-i18n="settings.sounds_section.mention_vol">@Mention Vol</span>
|
||
<input type="range" id="notif-mention-volume" min="0" max="100" value="80" class="slider-sm">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.mentions">@Mentions</span>
|
||
<select id="notif-mention-sound">
|
||
<option value="bell">Bell</option>
|
||
<option value="alert">Alert</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="drop">Drop</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div class="notif-divider"></div>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.user_joined">User Joined</span>
|
||
<select id="notif-join-sound">
|
||
<option value="chime">Chime</option>
|
||
<option value="bell">Bell</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="chord">Chord</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.sounds_section.user_left">User Left</span>
|
||
<select id="notif-leave-sound">
|
||
<option value="drop">Drop</option>
|
||
<option value="chime">Chime</option>
|
||
<option value="ping">Ping</option>
|
||
<option value="blip">Blip</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</label>
|
||
<div style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<button class="btn-sm btn-full btn-accent" id="open-sound-manager-user-btn">🎵 <span data-i18n="settings.sounds_section.sound_manager_btn">Sound Manager</span></button>
|
||
<small class="settings-hint" style="display:block;margin-top:4px" data-i18n="settings.sounds_section.sound_manager_hint">Browse sounds, assign to events, set hotkeys</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Push Notifications Section -->
|
||
<div class="settings-section" id="section-push">
|
||
<h5 class="settings-section-title">📲 <span data-i18n="settings.push_section.title">Push Notifications</span></h5>
|
||
<div class="notif-settings">
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.push_section.enable">Enable Push</span>
|
||
<input type="checkbox" id="push-notif-enabled">
|
||
</label>
|
||
<small class="settings-hint" id="push-notif-status" data-i18n="settings.push_section.checking">Checking...</small>
|
||
<small class="settings-hint" style="opacity:0.5" data-i18n="settings.push_section.hint">Receive notifications even when Haven is closed</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Password Change Section -->
|
||
<div class="settings-section" id="section-password">
|
||
<h5 class="settings-section-title">🔒 <span data-i18n="settings.password_section.title">Password</span></h5>
|
||
<div class="password-change-form">
|
||
<div class="form-group compact">
|
||
<input type="password" id="current-password" data-i18n-placeholder="settings.password_section.current_placeholder" placeholder="Current password" maxlength="128" autocomplete="current-password">
|
||
</div>
|
||
<div class="form-group compact">
|
||
<input type="password" id="new-password" data-i18n-placeholder="settings.password_section.new_placeholder" placeholder="New password (8+ chars)" maxlength="128" autocomplete="new-password">
|
||
</div>
|
||
<div class="form-group compact">
|
||
<input type="password" id="confirm-password" data-i18n-placeholder="settings.password_section.confirm_placeholder" placeholder="Confirm new password" maxlength="128" autocomplete="new-password">
|
||
</div>
|
||
<button class="btn-sm btn-full" id="change-password-btn" data-i18n="settings.password_section.change_btn">Change Password</button>
|
||
<small class="settings-hint" id="password-status"></small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Two-Factor Authentication Section -->
|
||
<div class="settings-section" id="section-2fa">
|
||
<h5 class="settings-section-title">🔐 <span data-i18n="settings.two_factor_section.title">Two-Factor Authentication</span></h5>
|
||
|
||
<!-- Status display -->
|
||
<div id="totp-status-area">
|
||
<p class="settings-hint" id="totp-status-text"></p>
|
||
</div>
|
||
|
||
<!-- Enable flow (shown when TOTP is OFF) -->
|
||
<div id="totp-enable-area" style="display:none">
|
||
<p class="settings-hint" style="margin-bottom:10px" data-i18n="settings.two_factor_section.enable_hint">Protect your account with a TOTP authenticator app (Google Authenticator, Authy, etc.).</p>
|
||
<button class="btn-sm btn-full" id="totp-enable-btn" data-i18n="settings.two_factor_section.enable_btn">Enable Two-Factor Auth</button>
|
||
</div>
|
||
|
||
<!-- Setup flow (shown after clicking Enable) -->
|
||
<div id="totp-setup-area" style="display:none">
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.two_factor_section.setup_hint">Scan this QR code with your authenticator app:</p>
|
||
<div style="text-align:center;margin:12px 0">
|
||
<img id="totp-qr-img" alt="QR Code" style="max-width:200px;border-radius:8px;background:#fff;padding:8px">
|
||
</div>
|
||
<p class="settings-hint" style="margin-bottom:4px" data-i18n="settings.two_factor_section.manual_hint">Or enter this secret manually:</p>
|
||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px">
|
||
<code id="totp-secret-text" style="font-size:14px;letter-spacing:2px;user-select:all;flex:1;word-break:break-all"></code>
|
||
<button class="btn-sm" id="totp-copy-secret" data-i18n-title="settings.two_factor_section.copy_secret_title" title="Copy secret" style="white-space:nowrap">📋 <span data-i18n="settings.two_factor_section.copy_secret_btn">Copy</span></button>
|
||
</div>
|
||
<div class="form-group compact">
|
||
<input type="text" id="totp-verify-code" data-i18n-placeholder="settings.two_factor_section.verify_placeholder" placeholder="Enter 6-digit code to confirm" maxlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" style="text-align:center;font-size:18px;letter-spacing:6px">
|
||
</div>
|
||
<button class="btn-sm btn-full" id="totp-verify-setup-btn" data-i18n="settings.two_factor_section.verify_btn">Verify & Activate</button>
|
||
<button class="btn-sm btn-full" id="totp-cancel-setup-btn" data-i18n="modals.common.cancel" style="margin-top:6px;opacity:0.7">Cancel</button>
|
||
<small class="settings-hint" id="totp-setup-status"></small>
|
||
</div>
|
||
|
||
<!-- Backup codes display (shown after successful setup) -->
|
||
<div id="totp-backup-area" style="display:none">
|
||
<h5 style="margin-bottom:6px">🗝️ <span data-i18n="settings.two_factor_section.backup_title">Backup Codes</span></h5>
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.two_factor_section.backup_hint">Save these codes somewhere safe. Each can be used once if you lose access to your authenticator.</p>
|
||
<div id="totp-backup-codes" style="font-family:monospace;font-size:15px;line-height:2;background:var(--bg-primary);padding:12px;border-radius:8px;text-align:center;user-select:all"></div>
|
||
<button class="btn-sm btn-full" id="totp-copy-backup-btn" style="margin-top:8px">📋 <span data-i18n="settings.two_factor_section.copy_backup_btn">Copy Backup Codes</span></button>
|
||
<button class="btn-sm btn-full" id="totp-backup-done-btn" data-i18n="settings.two_factor_section.done_btn" style="margin-top:6px">Done</button>
|
||
</div>
|
||
|
||
<!-- Manage area (shown when TOTP is ON) -->
|
||
<div id="totp-manage-area" style="display:none">
|
||
<p class="settings-hint" style="color:var(--accent);margin-bottom:6px">✅ <span data-i18n="settings.two_factor_section.enabled_text">Two-factor authentication is enabled</span></p>
|
||
<p class="settings-hint" id="totp-backup-remaining" style="margin-bottom:12px"></p>
|
||
<div class="form-group compact">
|
||
<input type="password" id="totp-disable-password" data-i18n-placeholder="settings.two_factor_section.disable_password_placeholder" placeholder="Enter password to modify" maxlength="128" autocomplete="current-password">
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn-sm btn-danger-fill" id="totp-disable-btn" data-i18n="settings.two_factor_section.disable_btn" style="flex:1">Disable 2FA</button>
|
||
<button class="btn-sm" id="totp-regen-backup-btn" data-i18n="settings.two_factor_section.regen_backup_btn" style="flex:1">Regenerate Backup Codes</button>
|
||
</div>
|
||
<small class="settings-hint" id="totp-manage-status"></small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Account Recovery Codes Section -->
|
||
<div class="settings-section" id="section-recovery">
|
||
<h5 class="settings-section-title">🔑 <span data-i18n="settings.recovery_section.title">Account Recovery</span></h5>
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.recovery_section.desc">Generate one-time recovery codes to regain access if you forget your password. Store them somewhere safe — they work like a last-resort key.</p>
|
||
<div class="settings-warning-box" id="recovery-e2e-warning" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.4);border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:0.82rem;color:var(--text-secondary)">
|
||
<span data-i18n-html="settings.recovery_section.e2e_warning">⚠️ <strong>Important:</strong> Using a recovery code to reset your password will permanently and irreversibly destroy your end-to-end encryption keys. All encrypted direct message history will become unreadable. There is no way to undo this.</span>
|
||
</div>
|
||
<!-- Status -->
|
||
<p class="settings-hint" id="recovery-code-status" style="margin-bottom:10px"></p>
|
||
<!-- Generate flow -->
|
||
<div id="recovery-generate-area">
|
||
<div class="form-group compact">
|
||
<input type="password" id="recovery-gen-password" data-i18n-placeholder="settings.recovery_section.confirm_password_placeholder" placeholder="Confirm your password" maxlength="128" autocomplete="current-password">
|
||
</div>
|
||
<button class="btn-sm btn-full" id="recovery-generate-btn" data-i18n="settings.recovery_section.generate_btn">Generate Recovery Codes</button>
|
||
<small class="settings-hint" id="recovery-gen-status"></small>
|
||
</div>
|
||
<!-- Codes display (shown after generation) -->
|
||
<div id="recovery-codes-area" style="display:none">
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="settings.recovery_section.save_hint">Save these codes now — they will not be shown again. Each code can only be used once.</p>
|
||
<div id="recovery-codes-list" style="font-family:monospace;font-size:15px;line-height:2;background:var(--bg-primary);padding:12px;border-radius:8px;text-align:center;user-select:all"></div>
|
||
<button class="btn-sm btn-full" id="recovery-copy-btn" style="margin-top:8px">📋 <span data-i18n="settings.recovery_section.copy_codes_btn">Copy Codes</span></button>
|
||
<button class="btn-sm btn-full" id="recovery-codes-done-btn" data-i18n="settings.recovery_section.done_btn" style="margin-top:6px">Done</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Account Section -->
|
||
<div class="settings-section" id="section-delete-account">
|
||
<h5 class="settings-section-title">⚠️ <span data-i18n="settings.delete_account_section.title">Delete Account</span></h5>
|
||
<p class="settings-hint" style="margin-bottom:8px;" data-i18n="settings.delete_account_section.desc">Permanently delete your account. This cannot be undone.</p>
|
||
<button class="btn-sm btn-danger-fill" id="delete-account-btn" data-i18n="settings.delete_account_section.btn">Delete My Account</button>
|
||
</div>
|
||
|
||
<!-- Plugins & Themes Section -->
|
||
<div class="settings-section" id="section-plugins">
|
||
<h5 class="settings-section-title">🧩 <span data-i18n="settings.plugins_section.title">Plugins & Themes</span></h5>
|
||
<p class="settings-hint" data-i18n-html="settings.plugins_section.hint">Drop <code>.plugin.js</code> files into the <code>plugins/</code> folder and <code>.theme.css</code> files into the <code>themes/</code> folder, then refresh.</p>
|
||
<div style="display:flex;gap:8px;margin-bottom:12px;">
|
||
<button class="btn-sm btn-accent" id="plugin-refresh-btn">🔄 <span data-i18n="settings.plugins_section.refresh_btn">Refresh</span></button>
|
||
</div>
|
||
<h6 style="margin:10px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted)" data-i18n="settings.plugins_section.plugins_label">Plugins</h6>
|
||
<div id="plugin-list" class="plugin-list"></div>
|
||
<h6 style="margin:14px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted)" data-i18n="settings.plugins_section.themes_label">Themes</h6>
|
||
<div id="theme-list" class="plugin-list"></div>
|
||
</div>
|
||
|
||
<!-- Admin Section (hidden for non-admins) -->
|
||
<div class="settings-section admin-settings-section" id="admin-mod-panel" style="display:none">
|
||
<h5 class="settings-section-title">🛡️ <span data-i18n="settings.admin.title">Admin</span></h5>
|
||
|
||
<!-- Server Branding -->
|
||
<div class="admin-settings" id="section-branding">
|
||
<h5 class="settings-section-subtitle">🏠 <span data-i18n="settings.admin.branding_title">Server Branding</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.server_name">Server Name</span>
|
||
<input type="text" id="server-name-input" maxlength="32" class="settings-text-input" style="max-width:160px" placeholder="HAVEN">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.login_title">Login Title</span>
|
||
<input type="text" id="server-title-input" maxlength="40" class="settings-text-input" style="max-width:200px" placeholder="(shown on login page)">
|
||
</label>
|
||
<small class="settings-hint" style="margin-top:2px;display:block" data-i18n="settings.admin.login_title_hint">Custom title displayed on the login screen below the HAVEN logo.</small>
|
||
<div class="server-icon-upload-area">
|
||
<div class="server-icon-preview" id="server-icon-preview">
|
||
<span class="server-icon-text">⬡</span>
|
||
</div>
|
||
<div class="server-icon-upload-controls">
|
||
<small class="settings-hint" data-i18n="settings.admin.server_icon_hint">Server icon (square, max 2 MB)</small>
|
||
<input type="file" id="server-icon-file" accept="image/*" style="font-size:11px;max-width:160px">
|
||
<div style="display:flex;gap:4px;margin-top:4px">
|
||
<button class="btn-sm btn-accent" id="server-icon-upload-btn" data-i18n="settings.admin.upload_btn">Upload</button>
|
||
<button class="btn-sm" id="server-icon-remove-btn" data-i18n="settings.admin.remove_btn">Remove</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.default_theme">Default Theme</span>
|
||
<select id="default-theme-select" style="max-width:160px">
|
||
<option value="" data-i18n="settings.admin.no_theme">None (user's choice)</option>
|
||
<option value="haven">Haven</option>
|
||
<option value="discord">Discord</option>
|
||
<option value="matrix">Matrix</option>
|
||
<option value="fallout">Fallout</option>
|
||
<option value="ffx">Final Fantasy</option>
|
||
<option value="ice">Ice</option>
|
||
<option value="nord">Nord</option>
|
||
<option value="darksouls">Dark Souls</option>
|
||
<option value="eldenring">Elden Ring</option>
|
||
<option value="bloodborne">Bloodborne</option>
|
||
<option value="cyberpunk">Cyberpunk</option>
|
||
<option value="lotr">Lord of the Rings</option>
|
||
<option value="abyss">Abyss</option>
|
||
<option value="scripture">Scripture</option>
|
||
<option value="chapel">Chapel</option>
|
||
<option value="gospel">Gospel</option>
|
||
<option value="tron">Tron</option>
|
||
<option value="halo">Halo</option>
|
||
<option value="dracula">Dracula</option>
|
||
<option value="win95">Windows 95</option>
|
||
</select>
|
||
</label>
|
||
<small class="settings-hint" style="margin-top:2px;display:block" data-i18n="settings.admin.default_theme_hint">New users see this theme on first visit. They can still pick their own.</small>
|
||
</div>
|
||
|
||
<div class="admin-settings" id="section-members" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.show_members">Show Members</span>
|
||
<select id="member-visibility-select">
|
||
<option value="online" data-i18n="settings.admin.members_online_only">Online Only</option>
|
||
<option value="all" data-i18n="settings.admin.members_all">All Members</option>
|
||
<option value="none" data-i18n="settings.admin.members_hidden">Hidden</option>
|
||
</select>
|
||
</label>
|
||
<button class="btn-sm btn-full" id="view-bans-btn">📋 <span data-i18n="settings.admin.view_bans_btn">View Bans</span></button>
|
||
<button class="btn-sm btn-full" id="view-deleted-users-btn" style="margin-top:6px">🗑️ <span data-i18n="settings.admin.view_deleted_btn">View Deleted Users</span></button>
|
||
<button class="btn-sm btn-full" id="view-all-members-btn" style="margin-top:6px">👥 <span data-i18n="settings.admin.view_all_members_btn">View All Members</span></button>
|
||
<label class="toggle-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.update_banner_label">Update banner (admins only)</span>
|
||
<input type="checkbox" id="update-banner-admin-only">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.update_banner_hint">When enabled, the "update available" banner is only shown to admins</small>
|
||
</div>
|
||
<div class="admin-settings" id="section-whitelist" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🛡️ <span data-i18n="settings.admin.whitelist_title">Whitelist</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.whitelist_enabled">Enabled</span>
|
||
<input type="checkbox" id="whitelist-enabled">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.whitelist_hint">When enabled, only pre-approved usernames can register</small>
|
||
<div id="whitelist-panel" style="margin-top:8px;">
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="whitelist-username-input" data-i18n-placeholder="settings.admin.whitelist_username_placeholder" placeholder="Username..." maxlength="20" class="settings-text-input">
|
||
<button class="btn-sm btn-accent" id="whitelist-add-btn" data-i18n="settings.admin.whitelist_add_btn">Add</button>
|
||
</div>
|
||
<div id="whitelist-list" class="whitelist-list">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-invite" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🌐 <span data-i18n="settings.admin.invite_title">Server Invite Code</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.invite_hint">A single code that adds people to every channel & sub-channel on the server</small>
|
||
<div style="margin-top:8px;">
|
||
<div id="server-code-display" style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
||
<code id="server-code-value" style="font-family:monospace;font-size:1rem;letter-spacing:0.05em;opacity:0.7">—</code>
|
||
<button class="btn-sm" id="copy-server-code-btn" data-i18n-title="settings.admin.copy_code_title" title="Copy code">📋</button>
|
||
</div>
|
||
<div style="display:flex;gap:4px;">
|
||
<button class="btn-sm btn-accent" id="generate-server-code-btn" data-i18n="settings.admin.invite_generate_btn">Generate</button>
|
||
<button class="btn-sm" id="clear-server-code-btn" data-i18n="settings.admin.invite_clear_btn">Clear</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-cleanup" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🗑️ <span data-i18n="settings.admin.cleanup_title">Auto-Cleanup</span></h5>
|
||
<label class="toggle-row">
|
||
<span data-i18n="settings.admin.cleanup_enabled">Enabled</span>
|
||
<input type="checkbox" id="cleanup-enabled">
|
||
</label>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.cleanup_max_age">Max Age (days)</span>
|
||
<input type="number" id="cleanup-max-age" min="0" max="3650" value="0" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.cleanup_age_hint">Delete messages older than N days (0 = off)</small>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.cleanup_max_size">Max DB Size (MB)</span>
|
||
<input type="number" id="cleanup-max-size" min="0" max="100000" value="0" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.cleanup_size_hint">Trim oldest messages when DB exceeds N MB (0 = off)</small>
|
||
<button class="btn-sm btn-full" id="run-cleanup-now-btn" style="margin-top:4px;">🧹 <span data-i18n="settings.admin.cleanup_run_btn">Run Cleanup Now</span></button>
|
||
</div>
|
||
<div class="admin-settings" id="section-uploads" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">📁 <span data-i18n="settings.admin.uploads_title">Uploads & Limits</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.max_upload">Max Upload Size (MB)</span>
|
||
<input type="number" id="max-upload-mb" min="1" max="2048" value="25" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_upload_hint">Maximum file size users can upload (1–2048 MB, default 25)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_sound">Max Sound File Size (KB)</span>
|
||
<input type="number" id="max-sound-kb" min="256" max="10240" value="1024" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_sound_hint">Max soundboard upload size (256–10240 KB, default 1024 / 1 MB)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_emoji">Max Emoji File Size (KB)</span>
|
||
<input type="number" id="max-emoji-kb" min="64" max="1024" value="256" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_emoji_hint">Max custom emoji upload size (64–1024 KB, default 256)</small>
|
||
<label class="select-row" style="margin-top:8px">
|
||
<span data-i18n="settings.admin.max_poll_options">Max Poll Options</span>
|
||
<input type="number" id="max-poll-options" min="2" max="25" value="10" class="settings-number-input">
|
||
</label>
|
||
<small class="settings-hint" data-i18n="settings.admin.max_poll_hint">Maximum answer choices per poll (2–25, default 10)</small>
|
||
</div>
|
||
<div class="admin-settings" id="section-sounds-admin" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🔊 <span data-i18n="settings.admin.sounds_title">Custom Sounds</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.sounds_hint">Upload audio files for custom notification sounds</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-sound-manager-btn">⚙️ <span data-i18n="settings.admin.manage_sounds_btn">Manage Sounds</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="admin-settings" id="section-emojis" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">😎 <span data-i18n="settings.admin.emojis_title">Custom Emojis</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.emojis_hint">Upload images for custom server emojis</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-emoji-manager-btn">⚙️ <span data-i18n="settings.admin.manage_emojis_btn">Manage Emojis</span></button>
|
||
</div>
|
||
</div>
|
||
<!-- Role Management (Admin only) -->
|
||
<div class="admin-settings" id="section-roles" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">👑 <span data-i18n="settings.admin.roles_title">Role Management</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.roles_hint">Create and manage roles. Assign roles to users from the user list context menu.</small>
|
||
<div style="margin-top:8px;display:flex;gap:6px">
|
||
<button class="btn-sm btn-full btn-accent" id="open-role-editor-btn">⚙️ <span data-i18n="settings.admin.manage_roles_btn">Manage Roles</span></button>
|
||
<button class="btn-sm" id="reset-roles-btn" title="Reset all roles to deployment defaults">🔄 <span data-i18n="settings.admin.reset_roles_btn">Reset to Default</span></button>
|
||
</div>
|
||
<div id="roles-list-preview" style="margin-top:8px;">
|
||
</div>
|
||
</div>
|
||
<!-- Tunnel settings (Admin only) -->
|
||
<div class="admin-settings" id="section-tunnel" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🧭 <span data-i18n="settings.admin.tunnel_title">Tunnel</span></h5>
|
||
<label class="select-row">
|
||
<span data-i18n="settings.admin.tunnel_provider">Provider</span>
|
||
<select id="tunnel-provider-select">
|
||
<option value="localtunnel">LocalTunnel</option>
|
||
<option value="cloudflared">Cloudflared</option>
|
||
</select>
|
||
</label>
|
||
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
|
||
<button class="btn-sm btn-accent" id="tunnel-toggle-btn" data-i18n="settings.admin.tunnel_start_btn">Start Tunnel</button>
|
||
<span id="tunnel-status-display" class="muted-text" data-i18n="settings.admin.tunnel_inactive" style="font-size:11px;">Inactive</span>
|
||
</div>
|
||
</div>
|
||
<!-- Webhooks / Bots (Admin only) -->
|
||
<div class="admin-settings" id="section-bots" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🤖 <span data-i18n="settings.admin.bots_title">Webhooks / Bots</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.bots_hint">Create and manage webhook bots that can post messages to channels.</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-bot-editor-btn">⚙️ <span data-i18n="settings.admin.manage_bots_btn">Manage Bots</span></button>
|
||
</div>
|
||
<div id="webhooks-list" style="margin-top:8px;">
|
||
<p class="muted-text" data-i18n="settings.admin.no_bots">No bots configured</p>
|
||
</div>
|
||
</div>
|
||
<!-- Custom Terms of Service (Admin only) -->
|
||
<div class="admin-settings" id="section-custom-tos" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">⚖️ <span data-i18n="settings.admin.custom_tos_title">Custom Terms of Service</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.custom_tos_hint">Add custom terms that appear before the default ToS on the login page. Supports plain text. Leave empty to show only the default Haven ToS.</small>
|
||
<textarea id="custom-tos-input" class="settings-textarea" rows="6" maxlength="50000" placeholder="Enter your custom terms of service..." style="margin-top:8px;width:100%;resize:vertical;min-height:80px;font-size:12px;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-family:inherit;"></textarea>
|
||
</div>
|
||
<!-- Discord Import (Admin only) -->
|
||
<div class="admin-settings" id="section-import" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">📦 <span data-i18n="settings.admin.import_title">Discord Import</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.import_hint">Import channels and messages from a Discord export file</small>
|
||
<div style="margin-top:8px;">
|
||
<button class="btn-sm btn-full btn-accent" id="open-import-btn">📦 <span data-i18n="settings.admin.import_btn">Import from Discord</span></button>
|
||
</div>
|
||
</div>
|
||
<!-- Mod Mode (Admin only) -->
|
||
<div class="admin-settings" id="section-modmode" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
|
||
<h5 class="settings-section-subtitle">🔧 <span data-i18n="settings.admin.modmode_title">Mod Mode</span></h5>
|
||
<small class="settings-hint" data-i18n="settings.admin.modmode_hint">Rearrange sidebar sections and snap server/sidebar/status panels</small>
|
||
<button class="btn-sm btn-full" id="mod-mode-settings-toggle" style="margin-top:6px;">🔧 <span data-i18n="settings.admin.modmode_toggle_btn">Toggle Mod Mode</span></button>
|
||
<button class="btn-sm btn-full" id="mod-mode-reset" data-i18n="settings.admin.modmode_reset_btn" style="margin-top:6px;">↺ Reset Layout</button>
|
||
</div>
|
||
<!-- Admin save bar — changes only apply when Save is clicked -->
|
||
<div class="admin-save-bar">
|
||
<button class="btn-accent btn-admin-save" id="admin-save-btn">💾 <span data-i18n="settings.admin.save_btn">Save Settings</span></button>
|
||
<small class="settings-hint" style="text-align:center;margin-top:4px" data-i18n="settings.admin.save_hint">Changes only apply when you click Save. Close (✕) to cancel.</small>
|
||
</div>
|
||
</div>
|
||
</div><!-- /settings-body -->
|
||
</div><!-- /settings-layout -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Discord Import Modal -->
|
||
<div class="modal-overlay" id="import-modal" style="display:none">
|
||
<div class="modal" style="max-width:560px">
|
||
<h3>📦 <span data-i18n="modals.discord_import.title">Import from Discord</span></h3>
|
||
|
||
<!-- Step 1: Upload or Connect -->
|
||
<div id="import-step-upload">
|
||
<div class="import-tabs">
|
||
<button class="import-tab active" data-tab="file">📁 <span data-i18n="modals.discord_import.file_tab">Upload File</span></button>
|
||
<button class="import-tab" data-tab="connect">🔗 <span data-i18n="modals.discord_import.connect_tab">Connect to Discord</span></button>
|
||
</div>
|
||
|
||
<!-- Tab: Upload File -->
|
||
<div id="import-tab-file">
|
||
<p class="settings-hint" style="margin-bottom:10px" data-i18n="modals.discord_import.upload_hint">
|
||
Upload a Discord export file to import channels and messages into Haven.
|
||
</p>
|
||
<div class="import-format-info">
|
||
<div class="import-format-row">
|
||
<strong>DiscordChatExporter</strong>
|
||
<span class="muted-text" data-i18n="modals.discord_import.format_dce">— JSON or ZIP (recommended — includes all users)</span>
|
||
</div>
|
||
<div class="import-format-row">
|
||
<strong>Discord Data Package</strong>
|
||
<span class="muted-text" data-i18n="modals.discord_import.format_ddp">— ZIP from Settings → Privacy (your messages only)</span>
|
||
</div>
|
||
</div>
|
||
<div class="import-dropzone" id="import-dropzone">
|
||
<input type="file" id="import-file-input" accept=".json,.zip" style="display:none">
|
||
<div class="import-dropzone-content">
|
||
<span class="import-dropzone-icon">📁</span>
|
||
<span><span data-i18n="modals.discord_import.drop_text">Drop file here or </span><a href="#" id="import-browse-link" data-i18n="modals.discord_import.browse_link">browse</a></span>
|
||
<span class="muted-text" style="font-size:11px" data-i18n="modals.discord_import.dropzone_hint">.json or .zip — up to 500 MB</span>
|
||
</div>
|
||
</div>
|
||
<div id="import-upload-progress" style="display:none">
|
||
<div class="import-progress-bar"><div class="import-progress-fill" id="import-progress-fill"></div></div>
|
||
<p class="muted-text" id="import-upload-status" style="margin-top:6px;text-align:center" data-i18n="modals.discord_import.uploading">Uploading...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Connect to Discord -->
|
||
<div id="import-tab-connect" style="display:none">
|
||
<div id="import-connect-step-token">
|
||
<p class="settings-hint" style="margin-bottom:8px" data-i18n="modals.discord_import.connect_hint">
|
||
Connect directly to Discord and pull your messages — no external tools needed.
|
||
</p>
|
||
<details class="import-token-help">
|
||
<summary data-i18n="modals.discord_import.token_help_summary">How do I get my Discord token?</summary>
|
||
<div class="import-token-steps">
|
||
<ol>
|
||
<li data-i18n-html="modals.discord_import.token_step_1">Open <a href="https://discord.com/app" target="_blank" rel="noopener">discord.com/app</a> in Chrome/Edge (or use the desktop app)</li>
|
||
<li data-i18n-html="modals.discord_import.token_step_2">Press <kbd>F12</kbd> to open DevTools</li>
|
||
<li data-i18n-html="modals.discord_import.token_step_3">Click the <strong>Application</strong> tab (click <code>>></code> in the toolbar if you don't see it)</li>
|
||
<li data-i18n-html="modals.discord_import.token_step_4">In the left sidebar, expand <strong>Local Storage</strong> → click <strong>https://discord.com</strong></li>
|
||
<li data-i18n-html="modals.discord_import.token_step_5">Find the <code>token</code> key — copy the value (without the quotes)</li>
|
||
</ol>
|
||
<p style="margin-top:8px" data-i18n-html="modals.discord_import.desktop_app_hint"><strong>Desktop app:</strong> If F12 doesn't open DevTools, close Discord fully, then edit:</p>
|
||
<code class="import-token-snippet">%appdata%\discord\settings.json</code>
|
||
<p style="margin-top:4px" data-i18n="modals.discord_import.add_line_hint">Add this line anywhere inside the { }:</p>
|
||
<code class="import-token-snippet">"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true</code>
|
||
<p style="margin-top:4px" data-i18n="modals.discord_import.restart_hint">Restart Discord, then follow the steps above.</p>
|
||
<p class="muted-text" style="margin-top:8px">⚠️ <span data-i18n="modals.discord_import.token_warning">Your token is like a password. Haven uses it only for this import and never stores it.</span></p>
|
||
</div>
|
||
</details>
|
||
<div style="display:flex;gap:8px;margin-top:10px">
|
||
<input type="password" id="import-discord-token" class="import-token-input" data-i18n-placeholder="modals.discord_import.token_placeholder" placeholder="Paste your Discord token here">
|
||
<button class="btn-sm btn-accent" id="import-connect-btn" data-i18n="modals.discord_import.connect_btn">Connect</button>
|
||
</div>
|
||
<p class="muted-text" id="import-connect-status" style="margin-top:6px;display:none"></p>
|
||
</div>
|
||
|
||
<div id="import-connect-step-servers" style="display:none">
|
||
<div class="import-info-bar" style="margin-bottom:8px">
|
||
<span><span data-i18n="modals.discord_import.connected_as">Connected as</span> <strong id="import-discord-username"></strong></span>
|
||
<a href="#" id="import-connect-disconnect" data-i18n="modals.discord_import.disconnect" style="font-size:11px;margin-left:auto">Disconnect</a>
|
||
</div>
|
||
<p class="settings-hint" style="margin-bottom:6px" data-i18n="modals.discord_import.pick_server">Pick a server to import from:</p>
|
||
<div id="import-server-list" class="import-server-grid"></div>
|
||
</div>
|
||
|
||
<div id="import-connect-step-channels" style="display:none">
|
||
<div class="import-info-bar" style="margin-bottom:8px">
|
||
<span id="import-connect-guild-name" style="font-weight:600"></span>
|
||
<a href="#" id="import-connect-back-servers" data-i18n="modals.discord_import.back_to_servers" style="font-size:11px;margin-left:auto">← Back to servers</a>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||
<p class="settings-hint" style="margin:0;flex:1" data-i18n="modals.discord_import.select_channels">Select channels to fetch:</p>
|
||
<a href="#" id="import-connect-toggle-all" data-i18n="modals.discord_import.deselect_all" style="font-size:11px;white-space:nowrap">Deselect All</a>
|
||
</div>
|
||
<div class="import-channel-list" id="import-connect-channel-list"></div>
|
||
<div style="display:flex;gap:8px;margin-top:10px;justify-content:flex-end">
|
||
<button class="btn-sm btn-accent" id="import-fetch-btn">📥 <span data-i18n="modals.discord_import.fetch_btn">Fetch Messages</span></button>
|
||
</div>
|
||
<p class="muted-text" id="import-fetch-status" style="margin-top:6px;display:none"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Preview & Select -->
|
||
<div id="import-step-preview" style="display:none">
|
||
<div class="import-info-bar">
|
||
<span class="import-format-badge" id="import-format-badge"></span>
|
||
<span id="import-server-name" style="font-weight:600"></span>
|
||
<span class="muted-text" id="import-total-msgs"></span>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin:8px 0 6px">
|
||
<p class="settings-hint" style="margin:0;flex:1" data-i18n="modals.discord_import.preview_select_hint">Select channels to import. You can rename them before importing.</p>
|
||
<a href="#" id="import-toggle-all" data-i18n="modals.discord_import.deselect_all" style="font-size:11px;white-space:nowrap">Deselect All</a>
|
||
</div>
|
||
<div class="import-channel-list" id="import-channel-list"></div>
|
||
<div style="display:flex;gap:8px;margin-top:12px;justify-content:flex-end">
|
||
<button class="btn-sm" id="import-back-btn" data-i18n="modals.common.back">← Back</button>
|
||
<button class="btn-sm btn-accent" id="import-execute-btn">📦 <span data-i18n="modals.discord_import.import_selected_btn">Import Selected</span></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Done -->
|
||
<div id="import-step-done" style="display:none;text-align:center;padding:20px 0">
|
||
<div style="font-size:48px;margin-bottom:12px">✅</div>
|
||
<p id="import-done-msg" style="font-size:14px" data-i18n="modals.discord_import.import_complete">Import complete!</p>
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top:12px">
|
||
<button class="btn-sm" id="close-import-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Role Management Modal -->
|
||
<div class="modal-overlay" id="role-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>👑 <span data-i18n="modals.role_management.title">Role Management</span></h3>
|
||
<div class="role-editor-layout">
|
||
<div class="role-sidebar">
|
||
<div id="role-list-sidebar" class="role-list-sidebar"></div>
|
||
<button class="btn-sm btn-accent btn-full" id="create-role-btn" data-i18n="modals.role_management.new_role_btn" style="margin-top:8px;">+ New Role</button>
|
||
</div>
|
||
<div class="role-detail" id="role-detail-panel">
|
||
<p class="muted-text" style="padding:20px;text-align:center" data-i18n="modals.role_management.select_to_edit">Select a role to edit</p>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="save-role-btn" data-i18n="modals.common.save" style="display:none">Save</button>
|
||
<button class="btn-sm" id="close-role-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Assign Role Modal -->
|
||
<div class="modal-overlay" id="assign-role-modal" style="display:none">
|
||
<div class="modal" style="max-width:440px">
|
||
<h3>👑 <span data-i18n="modals.assign_role.title">Assign Role</span></h3>
|
||
<p id="assign-role-user-label" class="muted-text"></p>
|
||
|
||
<label class="assign-role-field-label" data-i18n="modals.assign_role.role_label">Role</label>
|
||
<select id="assign-role-select" class="settings-select assign-role-select">
|
||
<option value="" data-i18n="modals.assign_role.select_role">-- Select Role --</option>
|
||
</select>
|
||
|
||
<label class="assign-role-field-label" style="margin-top:10px" data-i18n="modals.assign_role.scope_label">Scope</label>
|
||
<p class="assign-role-hint" data-i18n="modals.assign_role.scope_hint">Server-wide applies to all channels. Channel-specific only applies within that channel.</p>
|
||
<select id="assign-role-scope" class="settings-select assign-role-select">
|
||
<option value="server" data-i18n="modals.assign_role.server_wide">Server-wide</option>
|
||
</select>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-assign-role-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="confirm-assign-role-btn" data-i18n="modals.assign_role.assign_btn">Assign</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Centralized Role Assignment (3-pane) Modal -->
|
||
<div class="modal-overlay" id="role-assign-center-modal" style="display:none">
|
||
<div class="modal role-assign-center-box">
|
||
<h3>👑 <span data-i18n="modals.role_assign_center.title">Role Assignment</span></h3>
|
||
<p class="muted-text role-assign-center-subtitle" data-i18n="modals.role_assign_center.subtitle">Select a user, then a channel, then configure their role.</p>
|
||
|
||
<div class="rac-layout">
|
||
<!-- Left pane: Users -->
|
||
<div class="rac-pane rac-users-pane">
|
||
<div class="rac-pane-header">
|
||
<span class="rac-pane-title" data-i18n="modals.role_assign_center.users_pane">Users</span>
|
||
<input type="text" id="rac-user-search" class="rac-search" data-i18n-placeholder="modals.role_assign_center.search_users_placeholder" placeholder="Search users…">
|
||
</div>
|
||
<div class="rac-pane-list" id="rac-user-list"></div>
|
||
</div>
|
||
|
||
<!-- Center pane: Channels -->
|
||
<div class="rac-pane rac-channels-pane">
|
||
<div class="rac-pane-header">
|
||
<span class="rac-pane-title" data-i18n="modals.role_assign_center.channels_pane">Channels</span>
|
||
</div>
|
||
<div class="rac-pane-list" id="rac-channel-list">
|
||
<p class="rac-placeholder" data-i18n="modals.role_assign_center.select_user_hint">← Select a user</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right pane: Role config -->
|
||
<div class="rac-pane rac-config-pane">
|
||
<div class="rac-pane-header">
|
||
<span class="rac-pane-title" data-i18n="modals.role_assign_center.config_pane">Role Configuration</span>
|
||
</div>
|
||
<div class="rac-config-body" id="rac-config-body">
|
||
<p class="rac-placeholder" data-i18n="modals.role_assign_center.select_channel_hint">← Select a channel</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions rac-footer">
|
||
<button class="btn-sm" id="rac-manage-roles-btn" style="display:none" data-i18n-title="modals.role_assign_center.manage_roles_title" title="Open role management">⚙️ <span data-i18n="modals.role_assign_center.manage_roles_btn">Manage Roles</span></button>
|
||
<div class="rac-footer-right">
|
||
<button class="btn-sm" id="rac-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="rac-save-btn" data-i18n="modals.role_assign_center.save_btn" disabled>Save Changes</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bot Management Modal -->
|
||
<div class="modal-overlay" id="bot-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>🤖 <span data-i18n="modals.bot_mgmt.title">Bot Management</span></h3>
|
||
<div class="role-editor-layout">
|
||
<div class="role-sidebar">
|
||
<div id="bot-list-sidebar" class="role-list-sidebar"></div>
|
||
<button class="btn-sm btn-accent btn-full" id="create-bot-btn" data-i18n="modals.bot_mgmt.new_bot_btn" style="margin-top:8px;">+ New Bot</button>
|
||
</div>
|
||
<div class="role-detail" id="bot-detail-panel">
|
||
<p class="muted-text" style="padding:20px;text-align:center" data-i18n="modals.bot_mgmt.select_or_create">Select a bot to edit, or create a new one</p>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-bot-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sound Management Modal (Full Popout) -->
|
||
<div class="modal-overlay" id="sound-modal" style="display:none">
|
||
<div class="modal" style="max-width:640px;min-height:420px">
|
||
<h3>🔊 <span data-i18n="modals.sound_manager.title">Sound Manager</span></h3>
|
||
<!-- Tabs -->
|
||
<div class="sound-modal-tabs">
|
||
<button class="sound-tab active" data-tab="soundboard">🎵 <span data-i18n="modals.sound_manager.soundboard_tab">Soundboard</span></button>
|
||
<button class="sound-tab" data-tab="assign">🔔 <span data-i18n="modals.sound_manager.assign_tab">Assign to Events</span></button>
|
||
<button class="sound-tab sound-tab-admin" data-tab="manage" style="display:none">⚙️ <span data-i18n="modals.sound_manager.manage_tab">Manage</span></button>
|
||
</div>
|
||
|
||
<!-- TAB: Soundboard (all users) -->
|
||
<div class="sound-tab-content active" id="sound-tab-soundboard">
|
||
<small class="settings-hint" style="display:block;margin-bottom:8px" data-i18n="modals.sound_manager.modal_hint">Browse & play custom sounds. Set hotkeys to play them anytime.</small>
|
||
<div class="sound-search-row">
|
||
<input type="text" id="soundboard-search" data-i18n-placeholder="modals.sound_manager.search_placeholder" placeholder="Search sounds..." class="settings-text-input" style="flex:1">
|
||
<button id="soundboard-popout-btn" class="icon-btn small" title="Pop out soundboard">⧉</button>
|
||
</div>
|
||
<div id="soundboard-grid" class="soundboard-grid">
|
||
<p class="muted-text" data-i18n="modals.sound_manager.no_sounds">No sounds available</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Assign to Events (all users) -->
|
||
<div class="sound-tab-content" id="sound-tab-assign">
|
||
<small class="settings-hint" style="display:block;margin-bottom:10px" data-i18n="modals.sound_manager.assign_hint">Choose which sound plays for each event. Custom sounds uploaded by the admin appear here too.</small>
|
||
<div class="sound-assign-list">
|
||
<label class="select-row">
|
||
<span>📨 <span data-i18n="modals.sound_manager.new_message">New Message</span></span>
|
||
<select id="assign-msg-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>📤 <span data-i18n="modals.sound_manager.msg_sent">Message Sent</span></span>
|
||
<select id="assign-sent-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>📢 <span data-i18n="modals.sound_manager.mention">@Mention</span></span>
|
||
<select id="assign-mention-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>👋 <span data-i18n="modals.sound_manager.user_joined">User Joined</span></span>
|
||
<select id="assign-join-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
<label class="select-row">
|
||
<span>🚪 <span data-i18n="modals.sound_manager.user_left">User Left</span></span>
|
||
<select id="assign-leave-sound" class="sound-assign-select"></select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Manage (admin only) -->
|
||
<div class="sound-tab-content" id="sound-tab-manage">
|
||
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="modals.sound_manager.upload_hint">Upload audio files for custom sounds (max 1 MB each, mp3/ogg/wav/webm)</small>
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="sound-name-input" data-i18n-placeholder="modals.sound_manager.name_placeholder" placeholder="Sound name..." maxlength="30" class="settings-text-input">
|
||
<input type="file" id="sound-file-input" accept="audio/*" style="max-width:120px;font-size:11px">
|
||
<button class="btn-sm btn-accent" id="sound-upload-btn" data-i18n="modals.sound_manager.upload_btn">Upload</button>
|
||
</div>
|
||
<div id="custom-sounds-list" style="margin-top:12px;">
|
||
<p class="muted-text" data-i18n="modals.sound_manager.no_custom_sounds">No custom sounds uploaded</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-sound-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Emoji Management Modal -->
|
||
<div class="modal-overlay" id="emoji-modal" style="display:none">
|
||
<div class="modal" style="max-width:520px">
|
||
<h3>😎 <span data-i18n="modals.emoji_mgmt.title">Emoji Management</span></h3>
|
||
<small class="settings-hint" style="display:block;margin-bottom:12px" data-i18n="settings.admin.emojis_upload_hint">Upload images for custom server emojis (max 256 KB, png/gif/webp). Name must be lowercase, no spaces.</small>
|
||
<div class="whitelist-add-row">
|
||
<input type="text" id="emoji-name-input" data-i18n-placeholder="modals.emoji_mgmt.name_placeholder" placeholder="emoji_name" maxlength="30" class="settings-text-input">
|
||
<input type="file" id="emoji-file-input" accept="image/png,image/gif,image/webp,image/jpeg" style="max-width:120px;font-size:11px">
|
||
<button class="btn-sm btn-accent" id="emoji-upload-btn" data-i18n="modals.emoji_mgmt.upload_btn">Upload</button>
|
||
</div>
|
||
<div style="margin-top:6px;text-align:center">
|
||
<label class="btn-sm" style="cursor:pointer;display:inline-block" title="Select multiple images — names auto-generated from filenames">
|
||
📦 Bulk Upload
|
||
<input type="file" id="emoji-bulk-input" accept="image/png,image/gif,image/webp,image/jpeg" multiple style="display:none">
|
||
</label>
|
||
</div>
|
||
<div id="emoji-crop-preview-row" style="display:none;align-items:center;gap:10px;margin-top:8px;padding:8px;background:var(--bg-tertiary);border-radius:var(--radius-sm)">
|
||
<img id="emoji-crop-thumb" src="" alt="" style="width:32px;height:32px;border-radius:4px;object-fit:cover;border:1px solid var(--border)">
|
||
<span class="settings-hint" style="flex:1" data-i18n="modals.emoji_mgmt.crop_applied">Crop applied — ready to upload</span>
|
||
<button class="btn-xs" id="emoji-recrop-btn" title="Re-open cropper">✏️ <span data-i18n="modals.emoji_mgmt.adjust_btn">Adjust</span></button>
|
||
</div>
|
||
<div id="custom-emojis-list" style="margin-top:12px;">
|
||
<p class="muted-text" data-i18n="modals.emoji_mgmt.no_emojis">No custom emojis uploaded</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-emoji-modal-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Emoji Crop Modal -->
|
||
<div class="modal-overlay" id="emoji-crop-modal" style="display:none">
|
||
<div class="modal" style="max-width:320px;text-align:center">
|
||
<h3 style="margin-bottom:6px">✂️ <span data-i18n="modals.emoji_crop.title">Crop Emoji</span></h3>
|
||
<small class="settings-hint" style="display:block;margin-bottom:10px" data-i18n="modals.emoji_crop.hint">Drag to reposition · Slider to zoom</small>
|
||
<canvas id="emoji-crop-canvas" width="256" height="256" style="border-radius:var(--radius);cursor:grab;display:block;margin:0 auto 10px;border:2px solid var(--accent);max-width:100%;touch-action:none"></canvas>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;padding:0 4px">
|
||
<span style="font-size:13px" title="Zoom out">🔍</span>
|
||
<input type="range" id="emoji-crop-zoom" min="100" max="500" value="100" step="1" style="flex:1;accent-color:var(--accent)">
|
||
<span style="font-size:13px" title="Zoom in">🔎</span>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="emoji-crop-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="emoji-crop-confirm-btn" data-i18n="modals.emoji_crop.confirm_btn">Use This Crop</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="bans-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3 data-i18n="modals.bans.title">Banned Users</h3>
|
||
<div id="bans-list" class="bans-list">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-bans-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="deleted-users-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>🗑️ <span data-i18n="modals.deleted_users.title">Deleted Users</span></h3>
|
||
<div id="deleted-users-list" class="bans-list">
|
||
<p class="muted-text" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-deleted-users-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- All Members Modal -->
|
||
<div class="modal-overlay" id="all-members-modal" style="display:none">
|
||
<div class="modal modal-wide" style="max-width:720px;max-height:85vh;display:flex;flex-direction:column">
|
||
<h3>👥 <span data-i18n="modals.all_members.title">All Members</span> <span id="all-members-count" class="muted-text" style="font-size:13px;font-weight:normal"></span></h3>
|
||
<div style="margin-bottom:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<input type="text" id="all-members-search" data-i18n-placeholder="modals.all_members.search_placeholder" placeholder="Search users..." maxlength="40" class="settings-text-input" style="flex:1;min-width:140px">
|
||
<select id="all-members-filter" style="font-size:12px;padding:4px 8px;border-radius:var(--radius-sm);background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border)">
|
||
<option value="all" data-i18n="modals.all_members.filter_all">All</option>
|
||
<option value="online" data-i18n="modals.all_members.filter_online">Online</option>
|
||
<option value="offline" data-i18n="modals.all_members.filter_offline">Offline</option>
|
||
<option value="new" data-i18n="modals.all_members.filter_new">New (7 days)</option>
|
||
<option value="banned" data-i18n="modals.all_members.filter_banned">Banned</option>
|
||
</select>
|
||
</div>
|
||
<div id="all-members-list" style="overflow-y:auto;flex:1;min-height:0;display:flex;flex-direction:column;gap:4px">
|
||
<p class="muted-text" style="text-align:center;padding:20px" data-i18n="modals.common.loading">Loading...</p>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="close-all-members-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Music Share Modal -->
|
||
<div class="modal-overlay" id="music-modal" style="display:none">
|
||
<div class="modal">
|
||
<h3>🎵 <span data-i18n="modals.listen_together.title">Listen Together</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.listen_together.desc">Paste a link from Spotify, YouTube, or SoundCloud to share with everyone in voice</p>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.listen_together.link_label">Music Link</label>
|
||
<input type="text" id="music-link-input" data-i18n-placeholder="modals.listen_together.link_placeholder" placeholder="https://open.spotify.com/track/..." maxlength="500">
|
||
</div>
|
||
<div class="music-link-preview" id="music-link-preview"></div>
|
||
<div class="music-platforms-hint">
|
||
<span class="platform-badge spotify">Spotify</span>
|
||
<span class="platform-badge youtube">YouTube</span>
|
||
<span class="platform-badge soundcloud">SoundCloud</span>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="cancel-music-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<div style="display:flex;flex-direction:column;gap:6px;align-items:stretch">
|
||
<button class="btn-sm btn-accent" id="share-music-btn" data-i18n="modals.listen_together.share_btn">Share with Channel</button>
|
||
<button class="btn-sm btn-accent" id="share-music-playlist-btn" style="display:none" data-i18n="media.share_playlist">Share Playlist with Channel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="music-queue-modal" style="display:none">
|
||
<div class="modal modal-wide music-queue-modal">
|
||
<h3>📋 <span data-i18n="media.music_queue_title">Playback Queue</span></h3>
|
||
<p class="modal-desc" id="music-queue-summary" data-i18n="media.music_queue_empty_summary">No queued tracks</p>
|
||
<div class="music-queue-table-wrap">
|
||
<table class="music-queue-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="music-queue-col-handle"></th>
|
||
<th class="music-queue-col-pos" data-i18n="media.music_queue_col_pos">Pos.</th>
|
||
<th class="music-queue-col-requested-by" data-i18n="media.music_queue_col_requested_by">Requested By</th>
|
||
<th class="music-queue-col-title" data-i18n="media.music_queue_col_title">Title</th>
|
||
<th class="music-queue-col-actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="music-queue-body">
|
||
<tr><td colspan="5" class="music-queue-empty" data-i18n="media.music_queue_is_empty">Queue is empty</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="shuffle-music-queue-btn" style="display:none" data-i18n="media.music_shuffle_queue">Shuffle Queue</button>
|
||
<button class="btn-sm" id="close-music-queue-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Manage Servers Modal -->
|
||
<div class="modal-overlay" id="manage-servers-modal" style="display:none">
|
||
<div class="modal" style="max-width:480px">
|
||
<h3>⚙ <span data-i18n="modals.manage_servers.title">Manage Servers</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.manage_servers.desc">Edit or remove your linked servers</p>
|
||
<div id="manage-servers-list" class="manage-servers-list"></div>
|
||
<div class="modal-actions" style="justify-content:space-between">
|
||
<button class="btn-sm btn-accent" id="manage-servers-add-btn" data-i18n="modals.manage_servers.add_btn">+ Add Server</button>
|
||
<button class="btn-sm" id="manage-servers-close-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel context menu (appears on "..." hover button) -->
|
||
<div id="channel-ctx-menu" class="channel-ctx-menu" style="display:none">
|
||
<button class="channel-ctx-item" data-action="mute">🔔 <span data-i18n="context_menu.channel.mute">Mute Channel</span></button>
|
||
<button class="channel-ctx-item" data-action="join-voice">🎙️ <span data-i18n="voice.join_ctx">Join Voice</span></button>
|
||
<button class="channel-ctx-item" data-action="leave-voice" style="display:none">📴 <span data-i18n="context_menu.channel.disconnect_voice">Disconnect Voice</span></button>
|
||
<hr class="channel-ctx-sep">
|
||
<button class="channel-ctx-item" data-action="leave-channel">🚪 <span data-i18n="context_menu.channel.leave">Leave Channel</span></button>
|
||
<hr class="channel-ctx-sep">
|
||
<button class="channel-ctx-item mod-only" data-action="rename-channel">✏️ <span data-i18n="context_menu.channel.rename">Rename Channel</span></button>
|
||
<button class="channel-ctx-item mod-only" data-action="create-sub-channel">📁 <span data-i18n="context_menu.channel.create_sub">Create Sub-channel</span></button>
|
||
<hr class="channel-ctx-sep admin-only">
|
||
<button class="channel-ctx-item admin-only" data-action="organize" style="display:none">📋 <span data-i18n="context_menu.channel.organize">Organize</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="move-to-parent" style="display:none">📦 <span data-i18n="context_menu.channel.move_to">Move to…</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="promote-channel" style="display:none">⬆️ <span data-i18n="context_menu.channel.promote">Promote to Channel</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="channel-functions">⚙️ <span data-i18n="context_menu.channel.functions">Channel Functions</span> <span class="cfn-arrow">▶</span></button>
|
||
<div id="channel-functions-panel" class="channel-functions-panel" style="display:none">
|
||
<div class="cfn-row" data-fn="voice" data-i18n-title="channel_functions.voice_hint" title="Enable or disable voice chat in this channel">
|
||
<span class="cfn-label">🎙️ <span data-i18n="channel_functions.voice">Voice</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="text" data-i18n-title="channel_functions.text_hint" title="Enable or disable text chat in this channel">
|
||
<span class="cfn-label">💬 <span data-i18n="channel_functions.text">Text</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="streams" data-i18n-title="channel_functions.streams_hint" title="Allow screen sharing and camera (requires voice)">
|
||
<span class="cfn-label">🖥️ <span data-i18n="channel_functions.streams">Streams</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="music" data-i18n-title="channel_functions.music_hint" title="Allow Listen Together / music playback (requires voice)">
|
||
<span class="cfn-label">🎵 <span data-i18n="channel_functions.music">Music</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="media" data-i18n-title="channel_functions.media_hint" title="Allow image, video, and file uploads in this channel">
|
||
<span class="cfn-label">🖼️ <span data-i18n="channel_functions.media">Media</span></span>
|
||
<span class="cfn-badge cfn-on" data-i18n="channel_functions.on">ON</span>
|
||
</div>
|
||
<div class="cfn-divider"></div>
|
||
<div class="cfn-row" data-fn="slow-mode" data-i18n-title="channel_functions.slow_mode_hint" title="Require a cooldown between messages (0 = off)">
|
||
<span class="cfn-label">🐢 <span data-i18n="channel_functions.slow_mode">Slow Mode</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="cleanup-exempt" data-i18n-title="channel_functions.cleanup_guard_hint" title="Protect this channel from automatic message cleanup">
|
||
<span class="cfn-label">🛡️ <span data-i18n="channel_functions.cleanup_guard">Cleanup Guard</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="user-limit" data-i18n-title="channel_functions.voice_limit_hint" title="Limit simultaneous voice users (click to set; blank = unlimited)">
|
||
<span class="cfn-label">👥 <span data-i18n="channel_functions.voice_limit">Voice Limit</span></span>
|
||
<span class="cfn-badge cfn-off">∞</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="voice-bitrate" data-i18n-title="channel_functions.voice_bitrate_hint" title="Cap voice audio bitrate in kbps (0 = auto / no cap)">
|
||
<span class="cfn-label">🎚️ <span data-i18n="channel_functions.voice_bitrate">Voice Bitrate</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.voice_bitrate_auto">Auto</span>
|
||
</div>
|
||
<div class="cfn-divider"></div>
|
||
<div class="cfn-row" data-fn="self-destruct" data-i18n-title="channel_functions.self_destruct_hint" title="Set a self-destruct timer — channel auto-deletes after the specified hours (0 = off)">
|
||
<span class="cfn-label">⏱️ <span data-i18n="channel_functions.self_destruct">Self Destruct</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row" data-fn="announcement" data-i18n-title="channel_functions.announcement_hint" title="Announcement channel — messages use a distinct notification sound and gold highlight">
|
||
<span class="cfn-label">📢 <span data-i18n="channel_functions.announcement">Announcement</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-divider cfn-afk-divider" style="display:none"></div>
|
||
<div class="cfn-row cfn-afk-row" data-fn="afk-sub" style="display:none" data-i18n-title="channel_functions.afk_sub_hint" title="Designate a sub-channel as the AFK voice room — idle users are moved here">
|
||
<span class="cfn-label">💤 <span data-i18n="channel_functions.afk_sub">AFK Sub</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
<div class="cfn-row cfn-afk-row" data-fn="afk-timeout" style="display:none" data-i18n-title="channel_functions.afk_timeout_hint" title="Minutes of voice inactivity before moving to AFK sub (0 = disabled)">
|
||
<span class="cfn-label">⏱️ <span data-i18n="channel_functions.afk_timeout">AFK Timeout</span></span>
|
||
<span class="cfn-badge cfn-off" data-i18n="channel_functions.off">OFF</span>
|
||
</div>
|
||
</div>
|
||
<button class="channel-ctx-item admin-only" data-action="channel-roles">👑 <span data-i18n="context_menu.channel.roles">Roles</span></button>
|
||
<button class="channel-ctx-item admin-only" data-action="webhooks">🤖 <span data-i18n="context_menu.channel.webhooks">Webhooks</span></button>
|
||
<hr class="channel-ctx-sep admin-only">
|
||
<button class="channel-ctx-item admin-only danger" data-action="delete">🗑️ <span data-i18n="context_menu.channel.delete">Delete Channel</span></button>
|
||
</div>
|
||
|
||
<!-- DM context menu -->
|
||
<div id="dm-ctx-menu" class="channel-ctx-menu" style="display:none">
|
||
<button class="channel-ctx-item" data-action="dm-mute">🔔 <span data-i18n="context_menu.dm.mute">Mute DM</span></button>
|
||
<hr class="channel-ctx-sep">
|
||
<button class="channel-ctx-item danger" data-action="dm-delete">🗑️ <span data-i18n="context_menu.dm.delete">Delete DM</span></button>
|
||
</div>
|
||
|
||
<!-- Organize Sub-channels Modal -->
|
||
<div class="modal-overlay" id="organize-modal" style="display:none">
|
||
<div class="modal" style="max-width:500px">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||
<button id="organize-back-btn" class="btn-sm" style="display:none;padding:3px 8px;font-size:0.8rem" data-i18n-title="modals.organize.back_title" title="Back to channel list"><span data-i18n="modals.organize.back_btn">← Back</span></button>
|
||
<h3 id="organize-modal-title" style="margin:0">📋 <span data-i18n="modals.organize.title">Organize</span></h3>
|
||
</div>
|
||
<p class="modal-desc" id="organize-modal-parent-name"></p>
|
||
|
||
<div class="organize-toolbar">
|
||
<label class="organize-toolbar-label" data-i18n="modals.organize.sort_label">Sort:</label>
|
||
<select id="organize-global-sort" class="organize-select">
|
||
<option value="server_default">Server Default</option>
|
||
<option value="manual" data-i18n="modals.organize.sort_manual">Manual</option>
|
||
<option value="alpha" data-i18n="modals.organize.sort_alpha">Alphabetical</option>
|
||
<option value="created" data-i18n="modals.organize.sort_newest">Newest First</option>
|
||
<option value="oldest" data-i18n="modals.organize.sort_oldest">Oldest First</option>
|
||
<option value="dynamic" data-i18n="modals.organize.sort_dynamic">Dynamic</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="organize-toolbar" id="organize-cat-toolbar" style="display:none">
|
||
<label class="organize-toolbar-label" data-i18n="modals.organize.categories_label">Categories:</label>
|
||
<select id="organize-cat-sort" class="organize-select">
|
||
<option value="manual" data-i18n="modals.organize.cat_manual">Manual Order</option>
|
||
<option value="az" data-i18n="modals.organize.cat_az">A → Z</option>
|
||
<option value="za" data-i18n="modals.organize.cat_za">Z → A</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="organize-channel-list" class="organize-list"></div>
|
||
|
||
<div class="organize-controls">
|
||
<button class="btn-sm" id="organize-move-up" data-i18n="modals.organize.move_up" data-i18n-title="modals.organize.move_up_title" title="Move selected channel up">⬆ Up</button>
|
||
<button class="btn-sm" id="organize-move-down" data-i18n="modals.organize.move_down" data-i18n-title="modals.organize.move_down_title" title="Move selected channel down">⬇ Down</button>
|
||
<div class="organize-spacer"></div>
|
||
<div class="organize-tag-group">
|
||
<input type="text" id="organize-tag-input" class="organize-tag-input" maxlength="30" data-i18n-placeholder="modals.organize.tag_placeholder" placeholder="Tag name…">
|
||
<button class="btn-sm organize-tag-btn" id="organize-set-tag" data-i18n-title="modals.organize.tag_set_title" title="Apply tag">🏷️ <span data-i18n="modals.organize.tag_set_btn">Set</span></button>
|
||
<button class="btn-sm organize-tag-btn danger" id="organize-remove-tag" data-i18n="modals.organize.tag_remove_btn" data-i18n-title="modals.organize.tag_remove_title" title="Remove tag">✕</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="organize-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Move Channel Picker Modal -->
|
||
<div class="modal-overlay" id="reparent-modal" style="display:none">
|
||
<div class="modal" style="max-width:380px">
|
||
<h3 id="reparent-modal-title">📦 <span data-i18n="modals.move_channel.title">Move Channel</span></h3>
|
||
<p class="modal-desc" id="reparent-modal-desc"></p>
|
||
<div id="reparent-channel-list" class="organize-list" style="max-height:300px;overflow-y:auto"></div>
|
||
<div class="modal-actions" style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">
|
||
<button class="btn-sm" id="reparent-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DM Organize Modal -->
|
||
<div class="modal-overlay" id="dm-organize-modal" style="display:none">
|
||
<div class="modal" style="max-width:460px">
|
||
<h3>📋 <span data-i18n="modals.dm_organize.title">Organize DMs</span></h3>
|
||
<p class="modal-desc" data-i18n="modals.dm_organize.desc">Tag your conversations into collapsible categories</p>
|
||
|
||
<div class="organize-toolbar">
|
||
<label class="organize-toolbar-label" data-i18n="modals.dm_organize.sort_label">Sort:</label>
|
||
<select id="dm-organize-sort" class="organize-select">
|
||
<option value="manual" data-i18n="modals.dm_organize.sort_manual">Manual</option>
|
||
<option value="alpha" data-i18n="modals.dm_organize.sort_alpha">Alphabetical</option>
|
||
<option value="recent" data-i18n="modals.dm_organize.sort_recent">Recent Activity</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="dm-organize-list" class="organize-list"></div>
|
||
|
||
<div class="organize-controls">
|
||
<button class="btn-sm" id="dm-organize-move-up" data-i18n="modals.organize.move_up" data-i18n-title="modals.organize.move_up_title" title="Move up">⬆ Up</button>
|
||
<button class="btn-sm" id="dm-organize-move-down" data-i18n="modals.organize.move_down" data-i18n-title="modals.organize.move_down_title" title="Move down">⬇ Down</button>
|
||
<div class="organize-spacer"></div>
|
||
<div class="organize-tag-group">
|
||
<input type="text" id="dm-organize-tag-input" class="organize-tag-input" maxlength="30" data-i18n-placeholder="modals.dm_organize.category_placeholder" placeholder="Category…">
|
||
<button class="btn-sm organize-tag-btn" id="dm-organize-set-tag" data-i18n-title="modals.dm_organize.category_set_title" title="Apply category">🏷️ <span data-i18n="modals.organize.tag_set_btn">Set</span></button>
|
||
<button class="btn-sm organize-tag-btn danger" id="dm-organize-remove-tag" data-i18n="modals.organize.tag_remove_btn" data-i18n-title="modals.dm_organize.category_remove_title" title="Remove category">✕</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="dm-organize-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Webhook Management Modal -->
|
||
|
||
<!-- Poll Creation Modal -->
|
||
<div class="modal-overlay" id="poll-modal" style="display:none">
|
||
<div class="modal" style="max-width:440px">
|
||
<h3>📊 <span data-i18n="modals.poll.title">Create Poll</span></h3>
|
||
<div class="form-group">
|
||
<label data-i18n="modals.poll.question_label">Question</label>
|
||
<input type="text" id="poll-question-input" data-i18n-placeholder="modals.poll.question_placeholder" placeholder="What do you want to ask?" maxlength="300" autocomplete="off">
|
||
</div>
|
||
<div class="form-group" style="margin-top:8px">
|
||
<label data-i18n="modals.poll.options_label">Options</label>
|
||
<div id="poll-options-list" class="poll-options-list">
|
||
<div class="poll-option-row"><input type="text" class="poll-option-input" placeholder="Option 1" maxlength="100"><button class="poll-option-remove" title="Remove" style="display:none">×</button></div>
|
||
<div class="poll-option-row"><input type="text" class="poll-option-input" placeholder="Option 2" maxlength="100"><button class="poll-option-remove" title="Remove" style="display:none">×</button></div>
|
||
</div>
|
||
<button class="btn-sm" id="poll-add-option-btn" style="margin-top:6px" data-i18n="modals.poll.add_option_btn">+ Add Option</button>
|
||
</div>
|
||
<div class="form-group poll-settings-row" style="margin-top:10px">
|
||
<label class="poll-checkbox-label"><input type="checkbox" id="poll-multi-vote"> <span data-i18n="modals.poll.multi_vote">Allow multiple votes</span></label>
|
||
<label class="poll-checkbox-label"><input type="checkbox" id="poll-anonymous"> <span data-i18n="modals.poll.anonymous">Anonymous votes</span></label>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-sm" id="poll-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="poll-create-btn" data-i18n="modals.poll.create_btn">Create Poll</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel Roles Modal -->
|
||
<div class="modal-overlay" id="channel-roles-modal" style="display:none">
|
||
<div class="modal modal-wide">
|
||
<h3>👑 <span data-i18n="modals.channel_roles.title">Channel Roles</span></h3>
|
||
<p class="modal-desc" id="channel-roles-channel-name"></p>
|
||
|
||
<div class="channel-roles-layout">
|
||
<!-- Left: Members + Assignment -->
|
||
<div class="channel-roles-left">
|
||
<h4 class="channel-roles-section-title" data-i18n="modals.channel_roles.members_title">Members</h4>
|
||
<div class="channel-roles-member-list" id="channel-roles-member-list"></div>
|
||
|
||
<div class="channel-roles-actions" id="channel-roles-actions" style="display:none">
|
||
<h4 class="channel-roles-selected-name" id="channel-roles-selected-name"></h4>
|
||
<div class="channel-roles-current" id="channel-roles-current-roles"></div>
|
||
<div class="channel-roles-assign-area">
|
||
<div class="channel-roles-assign-row">
|
||
<select id="channel-roles-role-select" class="organize-select">
|
||
<option value="" data-i18n="modals.channel_roles.select_role">-- Select Role --</option>
|
||
</select>
|
||
<select id="channel-roles-scope-select" class="organize-select" style="max-width:140px">
|
||
<option value="server" data-i18n="modals.channel_roles.server_wide">🌐 Server-wide</option>
|
||
<option value="channel" data-i18n="modals.channel_roles.this_channel">📌 This Channel</option>
|
||
</select>
|
||
<button class="btn-sm btn-accent" id="channel-roles-assign-btn" data-i18n="modals.channel_roles.assign_btn">Assign</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Role Configuration -->
|
||
<div class="channel-roles-right">
|
||
<h4 class="channel-roles-section-title" data-i18n="modals.channel_roles.role_config_title">Role Configuration</h4>
|
||
<div class="channel-roles-role-list" id="channel-roles-role-list"></div>
|
||
<button class="btn-sm btn-accent btn-full" id="channel-roles-create-btn" data-i18n="modals.channel_roles.new_role_btn" style="margin-top:6px">+ New Role</button>
|
||
<div class="channel-roles-role-detail" id="channel-roles-role-detail">
|
||
<p class="muted-text" style="padding:12px;text-align:center;font-size:0.82rem" data-i18n="modals.channel_roles.select_to_configure">Select a role to configure</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn-sm btn-accent" id="channel-roles-done-btn" data-i18n="modals.common.done">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="webhook-modal" style="display:none">
|
||
<div class="modal" style="max-width:520px">
|
||
<h3>🤖 <span data-i18n="modals.webhook.title">Webhooks</span></h3>
|
||
<p class="modal-desc" id="webhook-modal-channel-name"></p>
|
||
|
||
<!-- Create new webhook -->
|
||
<div class="form-group" style="display:flex;gap:8px;align-items:flex-end">
|
||
<div style="flex:1">
|
||
<label data-i18n="modals.webhook.name_label">New Webhook Name</label>
|
||
<input type="text" id="webhook-name-input" class="input" maxlength="32" data-i18n-placeholder="modals.webhook.name_placeholder" placeholder="My Bot" style="width:100%">
|
||
</div>
|
||
<button class="btn-sm btn-accent" id="webhook-create-btn" data-i18n="modals.webhook.create_btn" style="height:36px">Create</button>
|
||
</div>
|
||
|
||
<!-- Webhook list -->
|
||
<div id="webhook-list" style="margin-top:14px;max-height:280px;overflow-y:auto"></div>
|
||
|
||
<!-- Token reveal (shown once after creation) -->
|
||
<div id="webhook-token-reveal" style="display:none;margin-top:14px;padding:12px;border-radius:8px;background:rgba(0,0,0,0.25);border:1px solid var(--accent-color, #00e5ff)">
|
||
<p style="font-size:0.85rem;margin:0 0 8px;opacity:0.7">⚠️ <span data-i18n="modals.webhook.token_warning">Copy this URL now — you won't see the full token again!</span></p>
|
||
<div style="display:flex;gap:6px">
|
||
<input type="text" id="webhook-url-display" class="input" readonly style="flex:1;font-size:0.8rem;font-family:monospace">
|
||
<button class="btn-sm" id="webhook-copy-url-btn">📋 <span data-i18n="modals.webhook.copy_btn">Copy</span></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top:16px">
|
||
<button class="btn-sm" id="webhook-close-btn" data-i18n="modals.common.close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sub-channel Subscriptions Panel -->
|
||
<div class="modal-overlay" id="sub-panel-modal" style="display:none">
|
||
<div class="modal" style="max-width:520px;max-height:80vh;display:flex;flex-direction:column">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<h3 style="margin:0;flex:1">📡 <span data-i18n="modals.sub_panel.title">Sub-channel Subscriptions</span></h3>
|
||
<button class="btn-sm" id="sub-panel-close-btn" data-i18n="modals.common.close" style="font-size:1rem;padding:4px 10px">×</button>
|
||
</div>
|
||
<p class="modal-desc" style="margin:0 0 12px;font-size:0.8rem;opacity:0.6" data-i18n="modals.sub_panel.desc">Subscribe to get notifications. Unsubscribed channels still appear but won't notify you.</p>
|
||
<div id="sub-panel-content" style="overflow-y:auto;flex:1;min-height:0"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Sub-channel Modal -->
|
||
<div class="modal-overlay" id="create-sub-modal" style="display:none">
|
||
<div class="modal" style="max-width:380px">
|
||
<h3 data-i18n="modals.create_sub.title">Create Sub-channel</h3>
|
||
<p class="modal-desc" id="create-sub-parent-name"></p>
|
||
<label style="display:block;margin:12px 0 6px;font-size:0.85rem;opacity:0.7" data-i18n="modals.create_sub.name_label">Sub-channel name</label>
|
||
<input type="text" id="create-sub-name" class="input" maxlength="50" data-i18n-placeholder="modals.create_sub.name_placeholder" placeholder="sub-channel name..." style="width:100%">
|
||
<label class="checkbox-row" style="margin:14px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer">
|
||
<input type="checkbox" id="create-sub-private" style="width:16px;height:16px">
|
||
<span style="font-size:0.9rem">🔒 <span data-i18n="modals.create_sub.private_label">Private</span> <span style="opacity:0.5;font-size:0.8rem" data-i18n="modals.create_sub.private_hint">(invite-only, hidden from non-members)</span></span>
|
||
</label>
|
||
<label class="checkbox-row" style="margin:2px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.9rem">
|
||
<input type="checkbox" id="create-sub-temporary" style="width:16px;height:16px">
|
||
<span>⏱️ <span data-i18n="modals.create_sub.temporary_label">Temporary</span> <span style="opacity:0.5;font-size:0.8rem" data-i18n="modals.create_sub.temporary_hint">(auto-delete)</span></span>
|
||
</label>
|
||
<div id="sub-temp-duration-row" style="display:none;margin:0 0 8px;padding-left:24px">
|
||
<div style="display:flex;gap:6px;align-items:center">
|
||
<input type="number" id="create-sub-duration" min="1" max="720" value="24" style="width:60px;font-size:0.8rem" placeholder="Hours">
|
||
<span style="font-size:0.75rem;opacity:0.6" data-i18n="modals.create_sub.hours">hours</span>
|
||
</div>
|
||
</div>
|
||
<p class="muted-text" style="font-size:0.75rem;margin:0 0 8px;opacity:0.6">ℹ️ <span data-i18n="modals.create_sub.admin_note">Admins always have access to all channels, including private ones.</span></p>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
|
||
<button class="btn-sm" id="create-sub-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
<button class="btn-sm btn-accent" id="create-sub-confirm-btn" data-i18n="modals.create_sub.create_btn">Create</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stackable effect overlay container -->
|
||
<div id="fx-layers"></div>
|
||
|
||
<!-- Push Notification Error Modal (center-screen, requires acknowledgement) -->
|
||
<!-- Activities / Games Launcher Modal -->
|
||
<div class="modal-overlay" id="activities-modal" style="display:none">
|
||
<div class="modal modal-activities">
|
||
<div class="activities-header">
|
||
<h3>🎮 <span data-i18n="modals.activities.title">Activities</span></h3>
|
||
<button class="settings-close-btn" id="close-activities-btn">×</button>
|
||
</div>
|
||
<div class="activities-grid" id="activities-grid">
|
||
<!-- Populated dynamically by JS from GAMES_REGISTRY -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Iframe Game Container (fullscreen overlay) -->
|
||
<div class="game-iframe-overlay" id="game-iframe-overlay" style="display:none">
|
||
<div class="game-iframe-header">
|
||
<span class="game-iframe-title" id="game-iframe-title">Game</span>
|
||
<div class="game-iframe-actions">
|
||
<label style="display:flex;align-items:center;gap:6px;color:var(--text-secondary);font-size:12px;margin-right:8px;" title="Game Volume">
|
||
🔊 <input type="range" id="game-volume-slider" min="0" max="100" value="80" style="width:80px;accent-color:var(--accent);">
|
||
<span id="game-volume-pct" style="min-width:28px;font-size:11px;">80%</span>
|
||
</label>
|
||
<button class="btn-sm" id="game-iframe-popout" title="Open in new window">↗️ <span data-i18n="modals.game_overlay.popout_btn">Pop Out</span></button>
|
||
<button class="btn-sm btn-danger" id="game-iframe-close" data-i18n="modals.game_overlay.close_btn" title="Close game">× Close</button>
|
||
</div>
|
||
</div>
|
||
<iframe id="game-iframe" class="game-iframe" sandbox="allow-scripts allow-same-origin allow-popups allow-forms" allowfullscreen></iframe>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="push-error-modal" style="display:none">
|
||
<div class="modal" style="max-width:420px;text-align:center;padding:28px 24px;">
|
||
<div style="font-size:48px;margin-bottom:12px;">🔕</div>
|
||
<h3 style="margin:0 0 12px;color:var(--text-primary)" data-i18n="modals.push_error.title">Push Notifications Unavailable</h3>
|
||
<p id="push-error-reason" style="color:var(--text-secondary);font-size:14px;line-height:1.5;margin:0 0 16px;"></p>
|
||
<p style="color:var(--text-muted);font-size:12px;line-height:1.5;margin:0 0 20px;" data-i18n-html="modals.push_error.recommendation">
|
||
<strong>Recommended:</strong> Google Chrome, Microsoft Edge, or Opera on desktop.<br>
|
||
iOS requires Safari 16.4+ with Haven added to your Home Screen.
|
||
</p>
|
||
<button class="btn-accent" id="push-error-dismiss-btn" style="width:100%;padding:10px;font-size:14px;font-weight:600;" data-i18n="modals.push_error.got_it">Got It</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E2E Password Prompt Modal -->
|
||
<div id="e2e-password-modal" class="modal-overlay" style="display:none">
|
||
<div class="modal e2e-password-modal">
|
||
<h3>🔐 <span data-i18n="modals.e2e_password.title">Password Required</span></h3>
|
||
<p class="e2e-pw-desc" data-i18n="modals.e2e_password.desc">End-to-end encryption requires your password to unlock your keys. Enter your account password to continue.</p>
|
||
<div class="e2e-pw-field">
|
||
<input type="password" id="e2e-pw-input" data-i18n-placeholder="modals.e2e_password.placeholder" placeholder="Enter your password" autocomplete="current-password" spellcheck="false">
|
||
</div>
|
||
<div id="e2e-pw-error" class="e2e-pw-error" style="display:none"></div>
|
||
<div class="e2e-pw-actions">
|
||
<button class="btn-accent" id="e2e-pw-submit-btn" data-i18n="modals.e2e_password.unlock_btn">Unlock</button>
|
||
<button class="btn-sm" id="e2e-pw-cancel-btn" data-i18n="modals.common.cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Desktop App Promo Popup -->
|
||
<!-- Android App Promo Modal -->
|
||
<div class="modal-overlay" id="android-beta-modal" style="display:none">
|
||
<div class="modal android-beta-promo">
|
||
<div class="android-beta-promo-icon">📱</div>
|
||
<span class="android-beta-promo-badge" data-i18n="modals.android_beta.badge" style="background:var(--accent-bright,#4caf50)">NOW AVAILABLE</span>
|
||
<h3 class="android-beta-promo-title"><span data-i18n="modals.android_beta.title">Amni-Haven Android</span></h3>
|
||
<p class="android-beta-promo-subtitle" data-i18n="modals.android_beta.subtitle">A native Android app for Haven, built from the ground up by Amnibro!</p>
|
||
<ul class="android-beta-promo-features">
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_native">Native Android experience</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_push">Push notifications via Google Play</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_chat">Full chat & voice support</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.android_beta.feature_built">Built from the ground up for Haven</span></li>
|
||
</ul>
|
||
<p class="android-beta-promo-note" data-i18n="modals.android_beta.note">Available now on Google Play:</p>
|
||
<div class="android-beta-form">
|
||
<a id="android-beta-submit" href="https://play.google.com/store/apps/details?id=com.havenapp.mobile&gl=US" target="_blank" rel="noopener" class="btn btn-accent android-beta-submit" style="text-decoration:none;display:inline-block">📲 <span data-i18n="modals.android_beta.request_btn">Get it on Google Play</span></a>
|
||
</div>
|
||
<p class="android-beta-promo-credit" data-i18n-html="modals.android_beta.credit">Built with ❤️ by <strong>Amnibro</strong> — thank you for your incredible work on the Amni-Haven Android app!</p>
|
||
<div class="android-beta-promo-actions">
|
||
<button id="android-beta-later" class="btn android-beta-promo-later" data-i18n="modals.android_beta.later_btn">Maybe later</button>
|
||
</div>
|
||
<label class="android-beta-promo-dismiss-label">
|
||
<input type="checkbox" id="android-beta-dismiss-check"> <span data-i18n="modals.android_beta.dont_show">Don't show this again</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="desktop-promo-modal" style="display:none">
|
||
<div class="modal desktop-promo">
|
||
<div class="desktop-promo-icon">🖥️</div>
|
||
<h3 class="desktop-promo-title"><span data-i18n="modals.desktop_promo.title">Haven Desktop App</span> <span class="desktop-promo-badge" data-i18n="modals.desktop_promo.badge">BETA</span></h3>
|
||
<p class="desktop-promo-subtitle" data-i18n="modals.desktop_promo.subtitle">Now available for your platform!</p>
|
||
<ul class="desktop-promo-features">
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_notifs">Native system notifications</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_tray">Minimize to system tray</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_audio">Per-app audio routing</span></li>
|
||
<li><span class="promo-check">✓</span> <span data-i18n="modals.desktop_promo.feature_launcher">Built-in server launcher</span></li>
|
||
</ul>
|
||
<p class="desktop-promo-meta" id="desktop-promo-meta">Windows Installer • v1.0.0</p>
|
||
<div class="desktop-promo-actions">
|
||
<a id="desktop-promo-install" class="btn btn-accent desktop-promo-install" href="https://ancsemi.github.io/Haven/#download" target="_blank" rel="noopener">⬇ <span data-i18n="modals.desktop_promo.install_btn">Install Haven</span></a>
|
||
<button id="desktop-promo-later" class="btn desktop-promo-later" data-i18n="modals.desktop_promo.later_btn">Maybe later</button>
|
||
</div>
|
||
<label class="desktop-promo-dismiss-label">
|
||
<input type="checkbox" id="desktop-promo-dismiss-check"> <span data-i18n="modals.desktop_promo.dont_show">Don't show this again</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Donors Thank You Modal -->
|
||
<div class="modal-overlay" id="donors-modal" style="display:none">
|
||
<div class="modal donors-modal-box">
|
||
<button class="donors-close-btn" id="donors-close-btn" title="Close">×</button>
|
||
<div class="donors-heart-icon">❤️</div>
|
||
<h2 class="donors-title" data-i18n="modals.donors.title">Thank You</h2>
|
||
<p class="donors-subtitle" data-i18n="modals.donors.subtitle">Haven is built with love and kept alive by the generosity of people like you. Every donation — big or small — means the world.</p>
|
||
|
||
<div class="donors-sort-toggle" id="donors-sort-toggle" style="display:none">
|
||
<button class="donors-sort-btn active" data-sort="chronological" data-i18n="modals.donors.sort_chronological">Early Believers</button>
|
||
<button class="donors-sort-btn" data-sort="featured" data-i18n="modals.donors.sort_featured">Biggest Thanks!</button>
|
||
</div>
|
||
|
||
<div class="donors-tiers" id="donors-tiers">
|
||
<div class="donors-tier">
|
||
<h3 class="donors-tier-title donors-sponsor-title">✨ <span data-i18n="modals.donors.sponsors_title">Sponsors</span></h3>
|
||
<div class="donors-grid" id="sponsors-grid"></div>
|
||
</div>
|
||
|
||
<div class="donors-tier">
|
||
<h3 class="donors-tier-title donors-donor-title">💛 <span data-i18n="modals.donors.donors_title">Donors</span></h3>
|
||
<div class="donors-grid" id="donors-grid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="donors-note" data-i18n="modals.donors.note">New donors will be added each release. If you've donated and don't see your name, it'll be in the next update!</p>
|
||
|
||
<a href="https://ko-fi.com/ancsemi" target="_blank" rel="noopener noreferrer" class="donors-kofi-btn">
|
||
<span>☕ <span data-i18n="modals.donors.kofi_btn">Support Haven on Ko-fi</span></span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/i18n.js"></script>
|
||
<script>i18n.init();</script>
|
||
<script src="/socket.io/socket.io.js"></script>
|
||
|
||
<!-- Image Lightbox Overlay -->
|
||
<div id="image-lightbox" class="image-lightbox" style="display:none">
|
||
<img id="lightbox-img" class="lightbox-img" src="" alt="">
|
||
</div>
|
||
<script src="/js/theme.js?v=2.7.3"></script>
|
||
<script src="/js/notifications.js?v=2.6.0"></script>
|
||
<script src="/js/servers.js?v=2.6.0"></script>
|
||
<script src="/js/voice.js?v=2.6.0"></script>
|
||
<script src="/js/modmode.js?v=2.6.0"></script>
|
||
<script src="/js/e2e.js?v=2.6.0"></script>
|
||
<script type="module" src="/js/app.js?v=2.7.11"></script>
|
||
<script src="/js/plugin-loader.js?v=2.6.0"></script>
|
||
</body>
|
||
</html>
|