diff --git a/index.html b/index.html new file mode 100644 index 0000000..7d2a5f6 --- /dev/null +++ b/index.html @@ -0,0 +1,106 @@ + + + + + Snowdrop Messenger + + + +
+
+
+ Snowdrop + +
+
+ +
+
+
+
+
+ + +
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+ + +
+
+
+ +
+
+ +
+ +
+ + +
+ + + + + + +
+
+
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/proto/rac.js b/proto/rac.js new file mode 100644 index 0000000..d342d07 --- /dev/null +++ b/proto/rac.js @@ -0,0 +1 @@ +// ъуъ \ No newline at end of file diff --git a/proto/wrac.js b/proto/wrac.js new file mode 100644 index 0000000..efa5110 --- /dev/null +++ b/proto/wrac.js @@ -0,0 +1,23 @@ + function wRAC(onOpenCb) { + if (!connectedServer) return; + if (ws && ws.readyState === 1) return onOpenCb(); + if (ws) { ws.onclose = null; ws.close(); } + ws = new WebSocket(connectedServer); + ws.binaryType = "arraybuffer"; + ws.onopen = function () { if (onOpenCb) onOpenCb(); }; + ws.onerror = function () { }; + ws.onclose = function () { }; + ws.onmessage = function (event) { + if (typeof event.data === "string") return; + let buf = new Uint8Array(event.data); + if (buf.length === 1 && (buf[0] === 0x01 || buf[0] === 0x02)) return; + let str = new TextDecoder().decode(buf).trim(); + if (/^\d+$/.test(str)) { + ws.send(new Uint8Array([0x00, 0x01])); + } else { + let lines = str.split('\n').filter(Boolean); + messages = lines; + showMessages(); + } + }; + } \ No newline at end of file diff --git a/src/chat.js b/src/chat.js new file mode 100644 index 0000000..0c34dc4 --- /dev/null +++ b/src/chat.js @@ -0,0 +1,111 @@ + function extractNickColor(str) { + let m = str.match(/<([^>]+)>/); + if (!m) return { nick: "unauth", colorClass: "nick-unauth" }; + let beforeNick = str.substring(0, m.index); + let nick = m[1]; + let colorClass = "nick-cyan"; + for (const { marker, color } of nickMarkers) { + if (beforeNick.includes(marker)) { + colorClass = color; + break; + } + } + return { nick, colorClass }; + } + + function parseMsg(msg) { + let date = ""; + let nick = ""; + let text = msg.trim(); + let colorClass = ""; + + let m = text.match(/^\[(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\]\s*(.*)$/); + if (m) { + date = m[1]; + text = m[2]; + } + + let nickMatch = text.match(/^(.*?<[^>]+>)(\s?)(.*)$/); + if (nickMatch) { + let prefix = nickMatch[1]; + let afterNick = nickMatch[3]; + let ext = extractNickColor(prefix); + nick = ext.nick; + colorClass = ext.colorClass; + text = afterNick; + return { nick, text, date, colorClass }; + } + + if (date && !nick) { + return { nick: "unauth", text, date, colorClass: "nick-unauth" }; + } + return { nick: "unauth", text, date: "", colorClass: "nick-unauth" }; + } + + function getVisibleMessages(messages) { + if (connectedServer && connectedServer.startsWith("wss://meex.lol:52667") && messages.length > 3) { + return messages.slice(3); + } + return messages; + } + + function showMessages() { + let chat = document.getElementById('chat-area'); + chat.innerHTML = ''; + let displayMessages = getVisibleMessages([...messages].reverse()); + for (let msg of displayMessages) { + let { nick, text, date, colorClass } = parseMsg(msg); + chat.innerHTML += ` +
+ ${nick ? nick : ""} + ${(text ?? "").replace(/")} + ${date ? "[" + date + "]" : ""} +
`; + } + } + + function sendMsg() { + const msg = document.getElementById('chat-input').value.trim(); + if (!msg || !connectedServer) return; + const { username, password } = getActiveServerCreds(); + wRAC(() => { + let arr; + let format = (settings && settings.messageFormat) ? settings.messageFormat : DEFAULT_SETTINGS.messageFormat; + let formatted = format; + if (formatted.includes("{name}")) formatted = formatted.replace("{name}", username ?? ""); + if (formatted.includes("{text}")) formatted = formatted.replace("{text}", msg ?? ""); + if (username && password) { + let enc = new TextEncoder(); + let uname = enc.encode(username); + let pass = enc.encode(password); + let text = enc.encode(formatted); + let total = new Uint8Array(1 + uname.length + 1 + pass.length + 1 + text.length); + total[0] = 0x02; + total.set(uname, 1); + total[1 + uname.length] = 10; + total.set(pass, 1 + uname.length + 1); + total[1 + uname.length + 1 + pass.length] = 10; + total.set(text, 1 + uname.length + 1 + pass.length + 1); + arr = total; + } else { + arr = [0x01, ...new TextEncoder().encode(formatted)]; + arr = new Uint8Array(arr); + } + ws.send(arr); + document.getElementById('chat-input').value = ""; + setTimeout(fetchMessages, 200); + }); + } + + function fetchMessages() { + wRAC(() => { + ws.send(new Uint8Array([0x00])); + }); + } + + document.getElementById('send-btn').onclick = sendMsg; + document.getElementById('chat-input').addEventListener('keydown', function (e) { + if (e.key === "Enter") sendMsg(); + }); + + setInterval(fetchMessages, 6000); \ No newline at end of file diff --git a/src/panel.js b/src/panel.js new file mode 100644 index 0000000..ccf1978 --- /dev/null +++ b/src/panel.js @@ -0,0 +1,348 @@ + const MESSAGE_FORMAT_PRESETS = [ + { id: "snowdrop", label: "Snowdrop", format: "ඞ<{name}> {text}", class: "preset-snowdrop" }, + { id: "brac", label: "bRAC", format: "리㹰<{name}> {text}", class: "preset-brac" }, + { id: "crab", label: "CRAB", format: "═══<{name}> {text}", class: "preset-crab" }, + { id: "mefidroniy", label: "Mefidroniy", format: "°ʘ<{name}> {text}", class: "preset-mefidroniy" }, + { id: "crack", label: "cRACk", format: "⁂<{name}> {text}", class: "preset-crack" }, + { id: "default", label: "Default (clRAC)", format: "<{name}> {text}", class: "preset-default" } + ]; + const DEFAULT_SETTINGS = { + lang: "ru", + messageFormat: "ඞ<{name}> {text}", + messageFormatPreset: "snowdrop" + }; + function getSettings() { + try { + let s = JSON.parse(localStorage.getItem("snowdrop_settings") || "{}"); + return { ...DEFAULT_SETTINGS, ...s }; + } catch (e) { + return { ...DEFAULT_SETTINGS }; + } + } + function saveSettings(s) { + localStorage.setItem("snowdrop_settings", JSON.stringify(s)); + } + let settings = getSettings(); + + function getProtoLabel(proto) { + let label = { proto, className: proto }; + if (proto === "wRACs") label.className = "wRACs"; + else if (proto === "wRAC") label.className = "wRAC"; + else if (proto === "RACs") label.className = "RACs"; + else if (proto === "RAC") label.className = "RAC"; + else label.className = ""; + return label; + } + function buildServerUrl({ proto, address, port }) { + if (proto === "wRACs") return `wss://${address}:${port}`; + if (proto === "wRAC") return `ws://${address}:${port}`; + if (proto === "RACs") return `rac+tls://${address}:${port}`; + if (proto === "RAC") return `rac://${address}:${port}`; + return `${address}:${port}`; + } + function parseServerUrl(url) { + let m; + if (m = url.match(/^wss:\/\/([^:\/]+):(\d+)$/)) return { proto: "wRACs", address: m[1], port: m[2] }; + if (m = url.match(/^ws:\/\/([^:\/]+):(\d+)$/)) return { proto: "wRAC", address: m[1], port: m[2] }; + if (m = url.match(/^rac\+tls:\/\/([^:\/]+):(\d+)$/)) return { proto: "RACs", address: m[1], port: m[2] }; + if (m = url.match(/^rac:\/\/([^:\/]+):(\d+)$/)) return { proto: "RAC", address: m[1], port: m[2] }; + return { proto: "wRACs", address: url, port: "" }; + } + function getSavedServers() { + let arr = []; + try { arr = JSON.parse(localStorage.getItem('snowdrop_servers') || "[]") } catch (e) { } + if (arr.length && typeof arr[0] === "string") { + arr = arr.map(url => { + const { proto, address, port } = parseServerUrl(url); + return { title: address, proto, address, port }; + }); + } + return arr; + } + function saveServers(arr) { + localStorage.setItem('snowdrop_servers', JSON.stringify(arr)); + } + let servers = getSavedServers(); + let ws = null; + let messages = []; + let connectedServer = servers[0] ? buildServerUrl(servers[0]) : null; + + function renderChannels() { + const channels = document.getElementById('channels'); + channels.innerHTML = ''; + servers.forEach((srv, idx) => { + const label = getProtoLabel(srv.proto); + const url = buildServerUrl(srv); + const isSelected = connectedServer === url || (!connectedServer && idx === 0); + const div = document.createElement('div'); + div.className = 'channel' + (isSelected ? ' selected' : ''); + div.setAttribute('data-server-idx', idx); + div.innerHTML = ` +
+
+ ${t(label.proto)} + ${srv.title} +
+ + +
+ ${srv.address}:${srv.port} + `; + div.querySelector('.channel-header-main').onclick = function (e) { + document.querySelectorAll('#channels .channel').forEach(c => c.classList.remove('selected')); + div.classList.add('selected'); + connectedServer = url; + fetchMessages(); + }; + div.querySelector('.delete-server-btn').onclick = function (e) { + e.stopPropagation(); + if (confirm(t('confirmDelete', { title: srv.title }))) { + servers.splice(idx, 1); + saveServers(servers); + if (servers.length) { + connectedServer = buildServerUrl(servers[Math.min(idx, servers.length - 1)]); + } else { + connectedServer = null; + messages = []; + showMessages(); + } + renderChannels(); + fetchMessages(); + } + }; + div.querySelector('.edit-server-btn').onclick = function (e) { + e.stopPropagation(); + openEditModal(idx); + }; + channels.appendChild(div); + }); + } + + function openEditModal(idx) { + const srv = servers[idx]; + modalTitle.value = srv.title || ""; + modalAddress.value = srv.address || ""; + modalPort.value = srv.port || ""; + modalProto.value = srv.proto || "wRACs"; + modalUsername.value = srv.username || ""; + modalPassword.value = srv.password || ""; + modalError.textContent = ""; + modalBg.style.display = "flex"; + setTimeout(() => modalTitle.focus(), 50); + + saveBtn.onclick = function () { + const title = modalTitle.value.trim(); + const address = modalAddress.value.trim(); + const port = modalPort.value.trim(); + const proto = modalProto.value; + const username = modalUsername.value.trim(); + const password = modalPassword.value; + if (!title || !address || !port || !proto) { + modalError.textContent = t('fillAllFields'); + return; + } + if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) { + modalError.textContent = t('invalidTitle'); + return; + } + if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) { + modalError.textContent = t('invalidAddress'); + return; + } + if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) { + modalError.textContent = t('invalidPort'); + return; + } + if (servers.some((srv2, i) => i !== idx && srv2.proto === proto && srv2.address === address && srv2.port === port)) { + modalError.textContent = t('duplicateServer'); + return; + } + servers[idx] = { title, proto, address, port, username, password }; + saveServers(servers); + connectedServer = buildServerUrl(servers[idx]); + closeModal(); + renderChannels(); + fetchMessages(); + }; + } + + function getActiveServerCreds() { + if (!connectedServer) return {}; + let idx = servers.findIndex( + s => buildServerUrl(s) === connectedServer + ); + if (idx === -1) return {}; + let { username, password } = servers[idx]; + return { username: username || "", password: password || "" }; + } + + function getCurrentFormatPresetId(fmt) { + for (const preset of MESSAGE_FORMAT_PRESETS) { + if (preset.format === fmt) return preset.id; + } + return null; + } + + const modalBg = document.getElementById('server-modal-bg'); + const modalForm = document.getElementById('server-modal'); + const modalTitle = document.getElementById('modal-server-title'); + const modalAddress = document.getElementById('modal-server-address'); + const modalPort = document.getElementById('modal-server-port'); + const modalProto = document.getElementById('modal-server-proto'); + const modalUsername = document.getElementById('modal-server-username'); + const modalPassword = document.getElementById('modal-server-password'); + const modalError = document.getElementById('server-modal-error'); + const saveBtn = document.getElementById('save-server-btn'); + const cancelBtn = document.getElementById('cancel-server-btn'); + + function openModal() { + modalTitle.value = ""; + modalAddress.value = ""; + modalPort.value = ""; + modalProto.value = "wRACs"; + modalUsername.value = ""; + modalPassword.value = ""; + modalError.textContent = ""; + modalBg.style.display = "flex"; + setTimeout(() => modalTitle.focus(), 50); + + saveBtn.onclick = function () { + const title = modalTitle.value.trim(); + const address = modalAddress.value.trim(); + const port = modalPort.value.trim(); + const proto = modalProto.value; + const username = modalUsername.value.trim(); + const password = modalPassword.value; + if (!title || !address || !port || !proto) { + modalError.textContent = t('fillAllFields'); + return; + } + if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) { + modalError.textContent = t('invalidTitle'); + return; + } + if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) { + modalError.textContent = t('invalidAddress'); + return; + } + if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) { + modalError.textContent = t('invalidPort'); + return; + } + if (servers.some(srv => srv.proto === proto && srv.address === address && srv.port === port)) { + modalError.textContent = t('duplicateServer'); + return; + } + servers.push({ title, proto, address, port, username, password }); + saveServers(servers); + connectedServer = buildServerUrl(servers[servers.length - 1]); + closeModal(); + renderChannels(); + fetchMessages(); + }; + } + function closeModal() { + modalBg.style.display = "none"; + } + document.getElementById('add-server-btn').onclick = openModal; + cancelBtn.onclick = closeModal; + + // Настройки + const settingsBtn = document.getElementById('header-settings-btn'); + const settingsModalBg = document.getElementById('settings-modal-bg'); + const settingsModal = document.getElementById('settings-modal'); + const settingsLangSelect = document.getElementById('settings-lang-select'); + const settingsFormatInput = document.getElementById('settings-format-input'); + const settingsModalError = document.getElementById('settings-modal-error'); + const settingsModalPresets = document.querySelectorAll('.settings-format-preset-btn'); + const saveSettingsBtn = document.getElementById('save-settings-btn'); + const cancelSettingsBtn = document.getElementById('cancel-settings-btn'); + + function updateSettingsModalFields() { + settingsLangSelect.value = settings.lang || "ru"; + settingsFormatInput.value = settings.messageFormat || DEFAULT_SETTINGS.messageFormat; + let id = getCurrentFormatPresetId(settingsFormatInput.value); + settingsModalPresets.forEach(btn => { + btn.classList.toggle("selected", btn.dataset.id === id); + }); + } + + function openSettingsModal() { + updateSettingsModalFields(); + settingsModalError.textContent = ""; + settingsModalBg.style.display = "flex"; + setTimeout(() => settingsLangSelect.focus(), 50); + } + function closeSettingsModal() { settingsModalBg.style.display = "none"; } + + settingsBtn.onclick = openSettingsModal; + cancelSettingsBtn.onclick = closeSettingsModal; + + settingsModalPresets.forEach(btn => { + btn.onclick = function () { + settingsFormatInput.value = btn.getAttribute("data-format") + .replace(/</g, "<").replace(/>/g, ">"); + settingsModalPresets.forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + }; + }); + + saveSettingsBtn.onclick = function () { + const lang = settingsLangSelect.value; + const format = settingsFormatInput.value.trim(); + settings = { + ...settings, + lang, + messageFormat: format, + messageFormatPreset: getCurrentFormatPresetId(format) || null + }; + saveSettings(settings); + closeSettingsModal(); + updateUIStrings(); + }; + + settingsModalBg.addEventListener("click", function (e) { + if (e.target === settingsModalBg) closeSettingsModal(); + }); + + function updateUIStrings() { + // Панелька + document.getElementById('app-title').textContent = t('appName'); + document.getElementById('add-server-btn').textContent = t('addServer'); + document.getElementById('header-settings-btn').title = t('settings'); + // Добавление серверовй + document.getElementById('label-server-title').textContent = t('serverTitle'); + document.getElementById('label-server-address').textContent = t('address'); + document.getElementById('label-server-port').textContent = t('port'); + document.getElementById('label-server-proto').textContent = t('protocol'); + document.getElementById('label-server-username').textContent = t('username'); + document.getElementById('label-server-password').textContent = t('password'); + document.getElementById('save-server-btn').textContent = t('save'); + document.getElementById('cancel-server-btn').textContent = t('cancel'); + // Настройки + document.getElementById('settings-label-lang').textContent = t('settingsLabelLang'); + document.getElementById('settings-label-format').textContent = t('settingsLabelFormat'); + saveSettingsBtn.textContent = t('settingsSave'); + cancelSettingsBtn.textContent = t('settingsCancel'); + settingsFormatInput.placeholder = t('messageFormat'); + // Ввод сообщений + document.getElementById('chat-input').placeholder = t('writeMessage'); + document.getElementById('send-btn').title = t('send'); + renderChannels(); + } + + settingsLangSelect.onchange = function () { + settings.lang = settingsLangSelect.value; + saveSettings(settings); + updateUIStrings(); + }; + + updateUIStrings(); + settingsBtn.addEventListener("click", updateSettingsModalFields); + renderChannels(); \ No newline at end of file diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..69a4021 --- /dev/null +++ b/src/script.js @@ -0,0 +1,89 @@ + const translations = { + ru: { + appName: "Snowdrop", + addServer: "+ Добавить сервер", + serverTitle: "Название сервера", + address: "IP/домен", + port: "Порт", + protocol: "Протокол", + username: "Имя пользователя (необязательно)", + password: "Пароль (необязательно)", + save: "Сохранить", + cancel: "Отмена", + editServer: "Изменить сервер", + deleteServer: "Удалить сервер", + confirmDelete: 'Удалить сервер "{title}"?', + settings: "Настройки", + language: "Язык", + messageFormat: "Формат сообщения", + settingsSave: "Сохранить", + settingsCancel: "Отмена", + invalidTitle: "Некорректное название", + invalidAddress: "Некорректный адрес", + invalidPort: "Некорректный порт", + duplicateServer: "Такой сервер уже добавлен", + fillAllFields: "Заполните все поля", + writeMessage: "Написать сообщение...", + send: "Отправить", + plain: "Простой", + wRACs: "wRACs", + wRAC: "wRAC (open snowdrop using http://)", + RACs: "RACs (WIP)", + RAC: "RAC (WIP)", + settingsLabelLang: "Язык", + settingsLabelFormat: "Формат сообщения" + }, + en: { + appName: "Snowdrop", + addServer: "+ Add server", + serverTitle: "Server title", + address: "IP/domain", + port: "Port", + protocol: "Protocol", + username: "Username (optional)", + password: "Password (optional)", + save: "Save", + cancel: "Cancel", + editServer: "Edit server", + deleteServer: "Delete server", + confirmDelete: 'Delete server "{title}"?', + settings: "Settings", + language: "Language", + messageFormat: "Message format", + settingsSave: "Save", + settingsCancel: "Cancel", + invalidTitle: "Invalid title", + invalidAddress: "Invalid address", + invalidPort: "Invalid port", + duplicateServer: "This server is already added", + fillAllFields: "Fill all fields", + writeMessage: "Write a message...", + send: "Send", + plain: "Plain", + wRACs: "wRACs", + wRAC: "wRAC (open snowdrop using http://)", + RACs: "RACs (WIP)", + RAC: "RAC (WIP)", + settingsLabelLang: "Language", + settingsLabelFormat: "Message format" + } + }; + + function t(key, vars = {}) { + const lang = settings && settings.lang || "ru"; + let str = translations[lang][key] || translations['ru'][key] || key; + Object.keys(vars).forEach(k => { + str = str.replace("{" + k + "}", vars[k]); + }); + return str; + } + + const nickMarkers = [ + { marker: "\uB9AC\u3E70", color: "nick-green" }, // бракованный + { marker: "\u2550\u2550\u2550", color: "nick-lightred" },// краб + { marker: "\u00B0\u0298", color: "nick-lightmagenta" }, // меф + { marker: "\u2042", color: "nick-gold" }, // кря + { marker: "\u0D9E", color: "nick-amogus" }, // сновдроп + ]; + + window.onload = () => { fetchMessages(); }; \ No newline at end of file diff --git a/themes/styles.css b/themes/styles.css new file mode 100644 index 0000000..6db4e18 --- /dev/null +++ b/themes/styles.css @@ -0,0 +1,574 @@ + body { + margin: 0; + padding: 0; + background: #18191c; + color: #dee2e6; + font-family: 'gg sans', 'Segoe UI', Arial, sans-serif; + height: 100vh; + overflow: hidden; + } + + #app-container { + display: flex; + height: 100vh; + } + + #channel-list { + width: 260px; + background: #141517; + border-right: 1px solid #101114; + display: flex; + flex-direction: column; + padding: 0; + box-sizing: border-box; + } + + #channel-list-header { + padding: 16px 18px 8px 18px; + font-weight: bold; + color: #fff; + font-size: 1rem; + letter-spacing: 1px; + border-bottom: 1px solid #101114; + display: flex; + align-items: center; + justify-content: space-between; + } + + .header-settings-btn { + background: none; + border: none; + margin-left: 8px; + padding: 1px 0 0 7px; + display: flex; + align-items: center; + cursor: pointer; + opacity: 0.8; + transition: opacity 0.18s; + } + + .header-settings-btn:hover { + opacity: 1; + } + + .header-settings-btn svg { + width: 23px; + height: 23px; + fill: #aaa; + } + + #channels { + flex: 1; + padding: 10px 0; + overflow-y: auto; + } + + .channel { + padding: 10px 20px; + color: #8a99a8; + cursor: pointer; + font-size: 1rem; + display: flex; + flex-direction: column; + border-radius: 6px; + transition: background 0.2s, color 0.2s; + margin-bottom: 2px; + position: relative; + } + + .channel.selected, .channel:hover { + background: #23272a; + color: #fff; + } + + .channel-header-row { + display: flex; + align-items: center; + justify-content: space-between; + } + + .channel-header-main { + display: flex; + align-items: center; + min-width: 0; + } + + .proto-label { + display: inline-block; + min-width: 48px; + font-family: monospace; + font-size: 1em; + font-weight: bold; + color: #fff; + background: #5b8fbd; + border-radius: 4px; + padding: 2px 8px; + margin-right: 10px; + text-align: center; + } + + .proto-label.wRAC { + background: #555; + } + + .proto-label.wRACs { + background: #5b8fbd; + } + + .proto-label.RAC { + background: #aa4000; + } + + .proto-label.RACs { + background: #007c40; + } + + .channel-title { + font-size: 1.05em; + color: #fff; + font-weight: normal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 120px; + } + + .channel-address { + font-size: 0.92em; + color: #888; + margin-left: 5px; + margin-top: 1px; + word-break: break-all; + } + + .edit-server-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + margin-left: 0; + align-self: flex-start; + opacity: 0.7; + transition: opacity 0.15s; + margin-right: 4px; + display: flex; + align-items: center; + justify-content: center; + } + + .edit-server-btn:hover { + opacity: 1; + } + + .edit-server-btn svg { + width: 19px; + height: 19px; + fill: #888; + display: block; + pointer-events: none; + } + + .delete-server-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + margin-left: 0; + align-self: flex-start; + opacity: 0.7; + transition: opacity 0.15s; + display: flex; + align-items: center; + justify-content: center; + } + + .delete-server-btn:hover { + opacity: 1; + } + + .delete-server-btn svg { + width: 19px; + height: 19px; + fill: #888; + display: block; + pointer-events: none; + } + + .channel.selected .delete-server-btn svg, + .channel:hover .delete-server-btn svg, + .channel.selected .edit-server-btn svg, + .channel:hover .edit-server-btn svg { + fill: #ff9191; + } + + #add-server-btn { + width: 95%; + margin: 10px auto 15px auto; + padding: 10px 0; + background: #23272a; + color: #5b8fbd; + border: none; + border-radius: 7px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: background 0.2s, color 0.2s; + display: block; + } + + #add-server-btn:hover { + background: #222e39; + color: #fff; + } + + #main { + flex: 1; + display: flex; + flex-direction: column; + background: #18191c; + } + + #chat-area { + flex: 1; + overflow-y: auto; + padding: 24px 24px 12px 24px; + background: #18191c; + display: flex; + flex-direction: column-reverse; + font-family: "Fira Mono", "Consolas", "Menlo", "monospace"; + font-size: 1.09rem; + line-height: 1.7; + } + + .message { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 2px 0 2px 0; + white-space: pre-wrap; + word-break: break-word; + min-height: 32px; + } + + .nick { + font-weight: bold; + margin-right: 10px; + min-width: 130px; + text-align: left; + flex-shrink: 0; + font-family: inherit; + } + + .nick-green { + color: #18b800; + } + + .nick-lightred { + color: #ff9191; + } + + .nick-lightmagenta { + color: #e782ff; + } + + .nick-gold { + color: #ffd700; + } + + .nick-cyan { + color: #5b8fbd; + } + + .nick-unauth { + color: #666; + font-style: italic; + } + + .nick-amogus { + color: #aeff00; + } + + .msg { + color: #fff; + flex: 1; + text-align: left; + font-family: inherit; + white-space: pre-wrap; + word-break: break-word; + padding-right: 16px; + } + + .time { + color: #8a8a8a; + font-size: 0.94em; + margin-left: 0; + min-width: 145px; + text-align: right; + flex-shrink: 0; + font-family: inherit; + } + + #chat-input-area { + display: flex; + flex-direction: column; + padding: 10px 18px 16px 18px; + background: #18191c; + border-top: 1px solid #101114; + } + + #chat-input-row { + display: flex; + align-items: center; + gap: 10px; + } + + #chat-input { + flex: 1; + background: #101114; + color: #fff; + padding: 10px 16px; + font-size: 1rem; + border-radius: 8px; + border: none; + outline: none; + margin-right: 6px; + transition: background 0.2s; + } + + #chat-input:focus { + background: #222327; + } + + #send-btn { + background: #5b8fbd; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.2s; + padding: 0 8px 0 8px; + display: flex; + align-items: center; + justify-content: center; + height: 40px; + width: 48px; + min-width: 48px; + } + + #send-btn svg { + width: 28px; + height: 28px; + display: block; + } + + #send-btn:active { + background: #3976a1; + } + + #server-modal-bg, + #settings-modal-bg { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + background: rgba(0,0,0,0.65); + display: none; + z-index: 1000; + align-items: center; + justify-content: center; + } + + #server-modal, #settings-modal { + background: #15171b; + color: #ddd; + border-radius: 12px; + padding: 26px 28px 18px 28px; + width: 340px; + box-shadow: 0 10px 36px #000a; + display: flex; + flex-direction: column; + gap: 14px; + } + + #settings-modal { + width: 400px; + min-height: 240px; + } + + #server-modal label, #settings-modal label { + font-size: 1em; + margin-bottom: 2px; + } + + #server-modal input, #server-modal select, + #settings-modal input, #settings-modal select { + background: #23272a; + color: #fff; + border: none; + border-radius: 6px; + padding: 7px 10px; + font-size: 1em; + margin-bottom: 8px; + width: 100%; + box-sizing: border-box; + } + + #server-modal .small-row { + display: flex; + gap: 8px; + } + + #server-modal .small-row > div { + flex: 1; + } + + #server-modal-btns, #settings-modal-btns { + display: flex; + gap: 10px; + justify-content: flex-end; + } + + #save-server-btn, #cancel-server-btn, + #save-settings-btn, #cancel-settings-btn { + font-size: 1em; + border-radius: 7px; + border: none; + padding: 8px 18px; + cursor: pointer; + font-weight: bold; + } + + #save-server-btn, #save-settings-btn { + background: #5b8fbd; + color: #fff; + } + + #save-server-btn:hover, #save-settings-btn:hover { + background: #3976a1; + } + + #cancel-server-btn, #cancel-settings-btn { + background: #23272a; + color: #aaa; + } + + #cancel-server-btn:hover, #cancel-settings-btn:hover { + background: #393c42; + color: #fff; + } + + #server-modal-error, #settings-modal-error { + color: #ff9191; + font-size: 0.98em; + min-height: 18px; + } + + .settings-format-presets { + display: flex; + flex-wrap: wrap; + gap: 9px; + margin-bottom: 5px; + } + + .settings-format-preset-btn { + background: #23272a; + border: none; + border-radius: 6px; + color: #fff; + padding: 6px 10px; + font-size: 1em; + cursor: pointer; + outline: none; + font-family: inherit; + font-weight: 500; + display: flex; + align-items: center; + transition: background 0.16s, color 0.16s; + } + + .settings-format-preset-btn.selected, + .settings-format-preset-btn:hover { + background: #1c2330; + } + + .preset-snowdrop { + color: #aeff00; + } + + .preset-brac { + color: #18b800; + } + + .preset-crab { + color: #ff9191; + } + + .preset-mefidroniy { + color: #e782ff; + } + + .preset-crack { + color: #ffd700; + } + + .preset-default { + color: #5b8fbd; + } + + .preset-plain { + color: #fff; + } + + #settings-format-input { + margin-top: 0px; + margin-bottom: 0px; + } + + #settings-modal-lang-row { + display: flex; + align-items: center; + gap: 17px; + margin-bottom: 7px; + } + + @media (max-width: 800px) { + #app-container { + flex-direction: column; + } + + #channel-list { + width: 100vw; + border-right: none; + } + + #main { + width: 100vw; + } + + .nick { + min-width: 90px; + font-size: 0.99em; + } + + .time { + min-width: 90px; + font-size: 0.89em; + } + + #send-btn { + width: 40px; + min-width: 40px; + height: 36px; + } + + #send-btn svg { + width: 22px; + height: 22px; + } + + #server-modal, #settings-modal { + width: 95vw; + min-width: 0; + } + } \ No newline at end of file