From d021246c24998d219244ac5abfd8de38f15f72e7 Mon Sep 17 00:00:00 2001
From: Forbirdden <102984166+Forbirdden@users.noreply.github.com>
Date: Thu, 26 Jun 2025 14:04:14 +0500
Subject: [PATCH 01/20] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20?=
=?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D1=84=D0=B0?=
=?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2=20=D1=82=D1=81=D0=BC=20=D0=B0=D0=BA?=
=?UTF-8?q?=D0=B0=20=D1=81=D0=BD=D0=BE=D0=B2=D0=B4=D1=80=D0=BE=D0=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
оно конечно работает но нужно доделать
---
index.html | 106 +++++++++
proto/rac.js | 1 +
proto/wrac.js | 23 ++
src/chat.js | 111 +++++++++
src/panel.js | 348 ++++++++++++++++++++++++++++
src/script.js | 89 +++++++
themes/styles.css | 574 ++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 1252 insertions(+)
create mode 100644 index.html
create mode 100644 proto/rac.js
create mode 100644 proto/wrac.js
create mode 100644 src/chat.js
create mode 100644 src/panel.js
create mode 100644 src/script.js
create mode 100644 themes/styles.css
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..7d2a5f6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,106 @@
+
+
+
+
diff --git a/proto/wrac.js b/proto/wrac.js
index efa5110..58dd5e5 100644
--- a/proto/wrac.js
+++ b/proto/wrac.js
@@ -1,4 +1,4 @@
- function wRAC(onOpenCb) {
+ async function wRAC(onOpenCb) {
if (!connectedServer) return;
if (ws && ws.readyState === 1) return onOpenCb();
if (ws) { ws.onclose = null; ws.close(); }
@@ -20,4 +20,57 @@
showMessages();
}
};
- }
\ No newline at end of file
+ }
+
+ async function checkServerInfo({ proto, address, port }) {
+ protocolCheck.state = "loading";
+ protocolCheck.protoVersion = null;
+ protocolCheck.serverSoftware = null;
+ updateServerInfoLabels();
+ let url;
+ try {
+ url = buildServerUrl({ proto, address, port });
+ let wsTest;
+ if (proto === "wRACs" || proto === "wRAC") {
+ wsTest = new WebSocket(url);
+ } else {
+ protocolCheck.state = "error";
+ updateServerInfoLabels();
+ return;
+ }
+ wsTest.binaryType = "arraybuffer";
+ let resolved = false;
+ wsTest.onopen = () => {
+ wsTest.send(new Uint8Array([0x69]));
+ };
+ wsTest.onmessage = (e) => {
+ if (resolved) return;
+ resolved = true;
+ let arr = new Uint8Array(e.data);
+ if (arr.length < 2) {
+ protocolCheck.state = "error";
+ updateServerInfoLabels();
+ wsTest.close();
+ return;
+ }
+ let versionByte = arr[0];
+ let ver = "";
+ if (versionByte === 0x01) ver = "v1";
+ else if (versionByte === 0x02) ver = "x1.99";
+ else if (versionByte === 0x03) ver = "v2";
+ else ver = "Unknown";
+ protocolCheck.protoVersion = ver;
+ protocolCheck.serverSoftware = new TextDecoder().decode(arr.slice(1));
+ protocolCheck.state = "done";
+ updateServerInfoLabels();
+ wsTest.close();
+ };
+ wsTest.onerror = () => {
+ protocolCheck.state = "error";
+ updateServerInfoLabels();
+ };
+ } catch (e) {
+ protocolCheck.state = "error";
+ updateServerInfoLabels();
+ }
+}
\ No newline at end of file
diff --git a/src/panel.js b/src/panel.js
index 5827f61..05dc368 100644
--- a/src/panel.js
+++ b/src/panel.js
@@ -1,348 +1,479 @@
- 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: "en",
- messageFormat: "ඞ<{name}> {text}",
- messageFormatPreset: "snowdrop"
+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: "en",
+ messageFormat: "ඞ<{name}> {text}",
+ messageFormatPreset: "snowdrop",
+ theme: "dark"
+};
+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;
+
+// THEME HANDLING
+
+function applyTheme(theme) {
+ // Удаляем все другие темы
+ let dark = document.getElementById('theme-dark-css');
+ let light = document.getElementById('theme-light-css');
+ if (dark) dark.remove();
+ if (light) light.remove();
+
+ if (theme === "light") {
+ let link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = 'themes/light.css';
+ link.id = 'theme-light-css';
+ document.head.appendChild(link);
+ } else {
+ let link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = 'themes/dark.css';
+ link.id = 'theme-dark-css';
+ document.head.appendChild(link);
+ }
+}
+applyTheme(settings.theme);
+
+//--- SERVER MODAL PROTOCOL DETECTION
+let protocolCheck = {
+ state: "idle", // idle | loading | done | error
+ protoVersion: null,
+ serverSoftware: null,
+ lastFields: {}
+};
+
+function updateServerInfoLabels() {
+ const protoInfo = document.getElementById('proto-version-info');
+ const swInfo = document.getElementById('server-software-info');
+ if (!protoInfo || !swInfo) return;
+ if (protocolCheck.state === "idle" || protocolCheck.state === "error") {
+ protoInfo.style.display = "none";
+ swInfo.style.display = "none";
+ } else if (protocolCheck.state === "loading") {
+ protoInfo.style.display = "";
+ protoInfo.textContent = t('protocolChecking');
+ swInfo.style.display = "";
+ swInfo.textContent = t('serverChecking');
+ } else if (protocolCheck.state === "done") {
+ protoInfo.style.display = "";
+ protoInfo.textContent = t('protocolVersion', { version: protocolCheck.protoVersion });
+ swInfo.style.display = "";
+ swInfo.textContent = t('serverSoftware', { software: protocolCheck.serverSoftware });
+ }
+}
+
+//--- RENDER CHANNELS
+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 = `
+
+
${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();
};
- function getSettings() {
- try {
- let s = JSON.parse(localStorage.getItem("snowdrop_settings") || "{}");
- return { ...DEFAULT_SETTINGS, ...s };
- } catch (e) {
- return { ...DEFAULT_SETTINGS };
+ 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();
}
- }
- function saveSettings(s) {
- localStorage.setItem("snowdrop_settings", JSON.stringify(s));
- }
- let settings = getSettings();
+ };
+ div.querySelector('.edit-server-btn').onclick = function (e) {
+ e.stopPropagation();
+ openEditModal(idx);
+ };
+ channels.appendChild(div);
+ });
+}
- 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 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);
+
+ // Protocol info
+ protocolCheck.state = "idle";
+ updateServerInfoLabels();
+
+ function triggerCheck() {
+ if (modalAddress.value && modalPort.value && modalProto.value) {
+ checkServerInfo({
+ proto: modalProto.value,
+ address: modalAddress.value,
+ port: modalPort.value
+ });
+ } else {
+ protocolCheck.state = "idle";
+ updateServerInfoLabels();
}
- 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}`;
+ }
+ modalAddress.oninput = triggerCheck;
+ modalPort.oninput = triggerCheck;
+ modalProto.onchange = triggerCheck;
+ triggerCheck();
+
+ 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;
}
- 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: "" };
+ if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) {
+ modalError.textContent = t('invalidTitle');
+ return;
}
- 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;
+ if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) {
+ modalError.textContent = t('invalidAddress');
+ return;
}
- function saveServers(arr) {
- localStorage.setItem('snowdrop_servers', JSON.stringify(arr));
+ if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) {
+ modalError.textContent = t('invalidPort');
+ return;
}
- 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 = `
-
-
${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);
- });
+ 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 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);
+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 || "" };
+}
- 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;
+}
- 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');
- 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');
+// Add info labels for server modal if not exist
+if (!document.getElementById("server-software-info")) {
+ const swInfo = document.createElement("div");
+ swInfo.id = "server-software-info";
+ swInfo.style.fontSize = "0.95em";
+ swInfo.style.color = "#aaa";
+ swInfo.style.marginTop = "-8px";
+ swInfo.style.marginBottom = "3px";
+ swInfo.style.display = "none";
+ modalAddress.parentElement.appendChild(swInfo);
+}
+if (!document.getElementById("proto-version-info")) {
+ const protoInfo = document.createElement("div");
+ protoInfo.id = "proto-version-info";
+ protoInfo.style.fontSize = "0.95em";
+ protoInfo.style.color = "#aaa";
+ protoInfo.style.marginTop = "1px";
+ protoInfo.style.marginBottom = "3px";
+ protoInfo.style.display = "none";
+ modalProto.parentElement.appendChild(protoInfo);
+}
- 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);
+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');
+ protocolCheck.state = "idle";
+ updateServerInfoLabels();
- 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 triggerCheck() {
+ if (modalAddress.value && modalPort.value && modalProto.value) {
+ checkServerInfo({
+ proto: modalProto.value,
+ address: modalAddress.value,
+ port: modalPort.value
});
+ } else {
+ protocolCheck.state = "idle";
+ updateServerInfoLabels();
}
+ }
+ modalAddress.oninput = triggerCheck;
+ modalPort.oninput = triggerCheck;
+ modalProto.onchange = triggerCheck;
+ triggerCheck();
- function openSettingsModal() {
- updateSettingsModalFields();
- settingsModalError.textContent = "";
- settingsModalBg.style.display = "flex";
- setTimeout(() => settingsLangSelect.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;
}
- function closeSettingsModal() { settingsModalBg.style.display = "none"; }
+ 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";
+ protocolCheck.state = "idle";
+ updateServerInfoLabels();
+}
+document.getElementById('add-server-btn').onclick = openModal;
+cancelBtn.onclick = closeModal;
- settingsBtn.onclick = openSettingsModal;
- cancelSettingsBtn.onclick = closeSettingsModal;
+// Настройки
+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');
- 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");
- };
- });
+const settingsThemeSelect = document.getElementById('settings-theme-select');
- 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();
- };
+function updateSettingsModalFields() {
+ settingsLangSelect.value = settings.lang || "ru";
+ settingsFormatInput.value = settings.messageFormat || DEFAULT_SETTINGS.messageFormat;
+ settingsThemeSelect.value = settings.theme || "dark";
+ let id = getCurrentFormatPresetId(settingsFormatInput.value);
+ settingsModalPresets.forEach(btn => {
+ btn.classList.toggle("selected", btn.dataset.id === id);
+ });
+}
- settingsModalBg.addEventListener("click", function (e) {
- if (e.target === settingsModalBg) closeSettingsModal();
- });
+function openSettingsModal() {
+ updateSettingsModalFields();
+ settingsModalError.textContent = "";
+ settingsModalBg.style.display = "flex";
+ setTimeout(() => settingsLangSelect.focus(), 50);
+}
+function closeSettingsModal() { settingsModalBg.style.display = "none"; }
- 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();
- }
+settingsBtn.onclick = openSettingsModal;
+cancelSettingsBtn.onclick = closeSettingsModal;
- settingsLangSelect.onchange = function () {
- settings.lang = settingsLangSelect.value;
- saveSettings(settings);
- updateUIStrings();
- };
+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");
+ };
+});
- updateUIStrings();
- settingsBtn.addEventListener("click", updateSettingsModalFields);
- renderChannels();
+saveSettingsBtn.onclick = function () {
+ const lang = settingsLangSelect.value;
+ const format = settingsFormatInput.value.trim();
+ const theme = settingsThemeSelect.value;
+ settings = {
+ ...settings,
+ lang,
+ messageFormat: format,
+ messageFormatPreset: getCurrentFormatPresetId(format) || null,
+ theme
+ };
+ saveSettings(settings);
+ applyTheme(theme);
+ closeSettingsModal();
+ updateUIStrings();
+};
+
+settingsThemeSelect.onchange = function () {
+ settings.theme = settingsThemeSelect.value;
+ saveSettings(settings);
+ applyTheme(settings.theme);
+};
+
+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');
+ document.getElementById('settings-label-theme').textContent = t('settingsLabelTheme') || "Theme";
+ 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
index 69a4021..2cb5238 100644
--- a/src/script.js
+++ b/src/script.js
@@ -1,89 +1,99 @@
- 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"
- }
- };
+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: "Формат сообщения",
+ settingsLabelTheme: "Тема",
+ protocolChecking: "Проверка версии протокола...",
+ serverChecking: "Проверка ПО сервера...",
+ protocolVersion: "Версия протокола: {version}",
+ serverSoftware: "ПО сервера: {software}"
+ },
+ en: {
+ appName: "Snowdrop",
+ addServer: "+ Add server",
+ serverTitle: "Server title",
+ address: "IP/domain",
+ port: "Port",
+ protocol: "Protocol",
+ username: "Username",
+ password: "Password",
+ 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",
+ settingsLabelTheme: "Theme",
+ protocolChecking: "Checking protocol version...",
+ serverChecking: "Checking server software...",
+ protocolVersion: "Protocol version: {version}",
+ serverSoftware: "Server software: {software}"
+ }
+};
- 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;
- }
+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" }, // сновдроп
- ];
+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
+window.onload = () => { fetchMessages(); };
\ No newline at end of file
diff --git a/themes/dark.css b/themes/dark.css
new file mode 100644
index 0000000..92975fa
--- /dev/null
+++ b/themes/dark.css
@@ -0,0 +1,143 @@
+body {
+ background: #18191c;
+ color: #dee2e6;
+}
+#app-container {
+ background: #18191c;
+}
+#channel-list {
+ background: #141517;
+ border-right: 1px solid #101114;
+}
+#channel-list-header {
+ color: #fff;
+ border-bottom: 1px solid #101114;
+}
+#channels {
+ background: inherit;
+}
+.channel {
+ color: #8a99a8;
+ background: inherit;
+}
+.channel.selected, .channel:hover {
+ background: #23272a;
+ color: #fff;
+}
+.proto-label {
+ color: #fff;
+ background: #5b8fbd;
+}
+.proto-label.wRAC {
+ background: #555;
+}
+.proto-label.wRACs {
+ background: #5b8fbd;
+}
+.proto-label.RAC {
+ background: #aa4000;
+}
+.proto-label.RACs {
+ background: #007c40;
+}
+.channel-title {
+ color: #fff;
+}
+.channel-address {
+ color: #888;
+}
+#add-server-btn {
+ background: #23272a;
+ color: #5b8fbd;
+}
+#add-server-btn:hover {
+ background: #222e39;
+ color: #fff;
+}
+#main {
+ background: #18191c;
+}
+#chat-area {
+ background: #18191c;
+ color: #fff;
+}
+.message .msg {
+ color: #fff;
+}
+.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; }
+.time { color: #8a8a8a; }
+#chat-input-area {
+ background: #18191c;
+ border-top: 1px solid #101114;
+}
+#chat-input {
+ background: #101114;
+ color: #fff;
+}
+#chat-input:focus {
+ background: #222327;
+}
+#send-btn {
+ background: #5b8fbd;
+ color: #fff;
+}
+#send-btn:active {
+ background: #3976a1;
+}
+#server-modal-bg, #settings-modal-bg {
+ background: rgba(0,0,0,0.65);
+}
+#server-modal, #settings-modal {
+ background: #15171b;
+ color: #ddd;
+ box-shadow: 0 10px 36px #000a;
+}
+#server-modal label, #settings-modal label {
+ color: #ddd;
+}
+#server-modal input, #server-modal select,
+#settings-modal input, #settings-modal select {
+ background: #23272a;
+ color: #fff;
+ border: none;
+}
+#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;
+}
+.settings-format-preset-btn {
+ background: #23272a;
+ color: #fff;
+}
+.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; }
\ No newline at end of file
diff --git a/themes/light.css b/themes/light.css
new file mode 100644
index 0000000..b318edd
--- /dev/null
+++ b/themes/light.css
@@ -0,0 +1,146 @@
+body {
+ background: #f6f6f7;
+ color: #222;
+}
+#app-container {
+ background: #f6f6f7;
+}
+#channel-list {
+ background: #fafbfc;
+ border-right: 1px solid #e3e6ea;
+}
+#channel-list-header {
+ color: #323638;
+ border-bottom: 1px solid #e3e6ea;
+}
+#channels {
+ background: inherit;
+}
+.channel {
+ color: #4c5862;
+ background: inherit;
+}
+.channel.selected, .channel:hover {
+ background: #e6ecf4;
+ color: #222;
+}
+.proto-label {
+ background: #a8d0f0;
+ color: #222;
+}
+.proto-label.wRAC {
+ background: #ccc;
+}
+.proto-label.wRACs {
+ background: #a8d0f0;
+}
+.proto-label.RAC {
+ background: #ffb080;
+}
+.proto-label.RACs {
+ background: #6ee7b7;
+}
+.channel-title {
+ color: #222;
+}
+.channel-address {
+ color: #a5a5a5;
+}
+#add-server-btn {
+ background: #e6ecf4;
+ color: #5b8fbd;
+}
+#add-server-btn:hover {
+ background: #d2e2f5;
+ color: #222;
+}
+#main {
+ background: #fff;
+}
+#chat-area {
+ background: #fff;
+ color: #222;
+}
+.message .msg {
+ color: #222;
+}
+.nick-green { color: #1bb800; }
+.nick-lightred { color: #e76b6b; }
+.nick-lightmagenta { color: #c359d7; }
+.nick-gold { color: #bda700; }
+.nick-cyan { color: #358dd1; }
+.nick-unauth { color: #aaa; }
+.nick-amogus { color: #7fa800; }
+.msg { color: #222; }
+.time { color: #888; }
+#chat-input-area {
+ background: #fafbfc;
+ border-top: 1px solid #e3e6ea;
+}
+#chat-input {
+ background: #f0f2f4;
+ color: #222;
+}
+#chat-input:focus {
+ background: #e6ecf4;
+}
+#send-btn {
+ background: #a8d0f0;
+ color: #222;
+}
+#send-btn:active {
+ background: #5b8fbd;
+ color: #fff;
+}
+#server-modal-bg, #settings-modal-bg {
+ background: rgba(255,255,255,0.7);
+}
+#server-modal, #settings-modal {
+ background: #fff;
+ color: #222;
+ box-shadow: 0 10px 36px #0002;
+}
+#server-modal label, #settings-modal label {
+ color: #333;
+}
+#server-modal input, #server-modal select,
+#settings-modal input, #settings-modal select {
+ background: #f0f2f4;
+ color: #222;
+ border: 1px solid #e3e6ea;
+}
+#save-server-btn, #save-settings-btn {
+ background: #a8d0f0;
+ color: #222;
+}
+#save-server-btn:hover, #save-settings-btn:hover {
+ background: #5b8fbd;
+ color: #fff;
+}
+#cancel-server-btn, #cancel-settings-btn {
+ background: #e6ecf4;
+ color: #888;
+}
+#cancel-server-btn:hover, #cancel-settings-btn:hover {
+ background: #c3d7ec;
+ color: #222;
+}
+#server-modal-error, #settings-modal-error {
+ color: #e76b6b;
+}
+.settings-format-preset-btn {
+ background: #e6ecf4;
+ color: #222;
+}
+.settings-format-preset-btn.selected,
+.settings-format-preset-btn:hover {
+ background: #c3d7ec;
+ color: #222;
+}
+.preset-snowdrop { color: #7fa800; }
+.preset-brac { color: #1bb800; }
+.preset-crab { color: #e76b6b; }
+.preset-mefidroniy { color: #c359d7; }
+.preset-crack { color: #bda700; }
+.preset-default { color: #358dd1; }
+.preset-plain { color: #222; }
\ No newline at end of file
diff --git a/themes/styles.css b/themes/styles.css
index 6db4e18..a3a9795 100644
--- a/themes/styles.css
+++ b/themes/styles.css
@@ -1,574 +1,386 @@
- 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;
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'gg sans', 'Segoe UI', Arial, sans-serif;
+ height: 100vh;
+ overflow: hidden;
+}
+#app-container {
+ display: flex;
+ height: 100vh;
+}
+#channel-list {
+ width: 260px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ box-sizing: border-box;
+}
+#channel-list-header {
+ padding: 16px 18px 8px 18px;
+ font-weight: bold;
+ font-size: 1rem;
+ letter-spacing: 1px;
+ 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;
+ display: block;
+}
+#channels {
+ flex: 1;
+ padding: 10px 0;
+ overflow-y: auto;
+}
+.channel {
+ padding: 10px 20px;
+ 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-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;
+ border-radius: 4px;
+ padding: 2px 8px;
+ margin-right: 10px;
+ text-align: center;
+}
+.channel-title {
+ font-size: 1.05em;
+ font-weight: normal;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 120px;
+}
+.channel-address {
+ font-size: 0.92em;
+ 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;
+ 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;
+ display: block;
+ pointer-events: none;
+}
+#add-server-btn {
+ width: 95%;
+ margin: 10px auto 15px auto;
+ padding: 10px 0;
+ border: none;
+ border-radius: 7px;
+ font-size: 1em;
+ font-weight: bold;
+ cursor: pointer;
+ transition: background 0.2s, color 0.2s;
+ display: block;
+}
+ #sidebar-footer {
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;
+ margin: 9px 0 0 18px;
+ font-size: 0.81em;
+ color: #999;
+ line-height: 1.33;
+ padding-bottom: 8px;
}
-
- #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 {
+ #sidebar-footer a {
color: #5b8fbd;
+ text-decoration: none;
+ font-size: 0.81em;
}
-
- .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
+ #add-server-btn {
+ margin-bottom: 7px !important;
+ }
+#main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+#chat-area {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px 24px 12px 24px;
+ 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 {}
+.nick-lightred {}
+.nick-lightmagenta {}
+.nick-gold {}
+.nick-cyan {}
+.nick-unauth {}
+.nick-amogus {}
+.msg {
+ flex: 1;
+ text-align: left;
+ font-family: inherit;
+ white-space: pre-wrap;
+ word-break: break-word;
+ padding-right: 16px;
+}
+.time {
+ 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;
+ border-top: 1px solid;
+}
+#chat-input-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+#chat-input {
+ flex: 1;
+ padding: 10px 16px;
+ font-size: 1rem;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ margin-right: 6px;
+ transition: background 0.2s;
+}
+#send-btn {
+ 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;
+}
+#server-modal-bg,
+#settings-modal-bg {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ display: none;
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+}
+#server-modal, #settings-modal {
+ 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 {
+ 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;
+}
+.settings-format-presets {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 9px;
+ margin-bottom: 5px;
+}
+.settings-format-preset-btn {
+ border: none;
+ border-radius: 6px;
+ 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-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
From fb18799f2f48291005414dcb056e84f55aec3c07 Mon Sep 17 00:00:00 2001
From: Forbirdden <102984166+Forbirdden@users.noreply.github.com>
Date: Fri, 27 Jun 2025 22:15:51 +0500
Subject: [PATCH 06/20] Update README.md
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index d0ff0ce..1fdd1d1 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,10 @@ Roadmap:
- [x] WRAC(S) support
- [x] User Agents
- [x] Server list Management
-- [ ] Server info packet
-- [ ] Settings
+- [x] Server info packet
+- [x] Settings
- [x] Authentication
-- [ ] GitHub repository
+- [ ] Design
- [ ] Web version
- [ ] Mobile UI
- [ ] Mobile version
From 4f1bad3ac5a31b62f834cc361e0ab655889d6e22 Mon Sep 17 00:00:00 2001
From: Forbirdden <102984166+Forbirdden@users.noreply.github.com>
Date: Fri, 27 Jun 2025 22:19:57 +0500
Subject: [PATCH 07/20] Update README.md
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 1fdd1d1..282097b 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,5 @@ Roadmap:
- [ ] Web version
- [ ] Mobile UI
- [ ] Mobile version
-- [ ] Proxy for RAC(S)
- [ ] RAC(S) support
- [ ] Desktop Version
From bfb66c9f74fdc2d4792875535e691862d0e1c105 Mon Sep 17 00:00:00 2001
From: Forbirdden <102984166+Forbirdden@users.noreply.github.com>
Date: Fri, 27 Jun 2025 22:29:22 +0500
Subject: [PATCH 08/20] =?UTF-8?q?dragon=20drop=20=D1=81=D0=B5=D1=80=D0=B2?=
=?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=B2=20(=D0=BF=D0=B5=D1=80=D0=B5=D0=BC?=
=?UTF-8?q?=D0=B5=D1=89=D0=B5=D0=BD=D0=B8=D0=B5)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/panel.js | 67 +++++++++++++++++++++++++++++++++++++++++++++--
themes/styles.css | 13 ++++++++-
2 files changed, 77 insertions(+), 3 deletions(-)
diff --git a/src/panel.js b/src/panel.js
index 05dc368..3d4d830 100644
--- a/src/panel.js
+++ b/src/panel.js
@@ -121,10 +121,64 @@ function updateServerInfoLabels() {
}
}
+//--- DRAG & DROP SERVERS
+let dragSrcIdx = null;
+
+function handleDragStart(e) {
+ dragSrcIdx = +this.getAttribute('data-server-idx');
+ this.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', dragSrcIdx);
+}
+
+function handleDragOver(e) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ this.classList.add('drag-over');
+}
+
+function handleDragLeave(e) {
+ this.classList.remove('drag-over');
+}
+
+function handleDrop(e) {
+ e.preventDefault();
+ this.classList.remove('drag-over');
+ const fromIdx = dragSrcIdx;
+ const toIdx = +this.getAttribute('data-server-idx');
+ if (fromIdx === toIdx || fromIdx == null || toIdx == null) return;
+
+ // Перемещаем сервер
+ const moved = servers.splice(fromIdx, 1)[0];
+ servers.splice(toIdx, 0, moved);
+ saveServers(servers);
+
+ // Если перемещённый сервер был выбран, пересчитываем connectedServer
+ if (connectedServer === buildServerUrl(moved)) {
+ connectedServer = buildServerUrl(servers[toIdx]);
+ }
+
+ renderChannels();
+ fetchMessages();
+}
+
+function handleDragEnd(e) {
+ document.querySelectorAll('#channels .channel').forEach(c => c.classList.remove('dragging', 'drag-over'));
+ dragSrcIdx = null;
+}
+
//--- RENDER CHANNELS
function renderChannels() {
const channels = document.getElementById('channels');
+ if (!channels) return; // Исправлено: если элемент не найден — ничего не делаем
channels.innerHTML = '';
+
+ // Проверка: если серверов нет, можно показать сообщение или оставить пусто
+ if (!servers || !servers.length) {
+ // channels.innerHTML = '
Нет серверов
';
+ return;
+ }
+
servers.forEach((srv, idx) => {
const label = getProtoLabel(srv.proto);
const url = buildServerUrl(srv);
@@ -132,6 +186,15 @@ function renderChannels() {
const div = document.createElement('div');
div.className = 'channel' + (isSelected ? ' selected' : '');
div.setAttribute('data-server-idx', idx);
+
+ // --- Drag & Drop handlers
+ div.setAttribute('draggable', 'true');
+ div.ondragstart = handleDragStart;
+ div.ondragover = handleDragOver;
+ div.ondragleave = handleDragLeave;
+ div.ondrop = handleDrop;
+ div.ondragend = handleDragEnd;
+
div.innerHTML = `
${srv.address}:${srv.port}
diff --git a/themes/styles.css b/themes/styles.css
index a3a9795..fe5687c 100644
--- a/themes/styles.css
+++ b/themes/styles.css
@@ -351,6 +351,17 @@ body {
gap: 17px;
margin-bottom: 7px;
}
+
+/* --- DRAG & DROP --- */
+.channel.dragging {
+ opacity: 0.5;
+ border: 2px dashed #99bbee;
+}
+.channel.drag-over {
+ background: #e4eaf1 !important;
+ border: 2px dashed #8db3e6;
+}
+
@media (max-width: 800px) {
#app-container {
flex-direction: column;
@@ -383,4 +394,4 @@ body {
width: 95vw;
min-width: 0;
}
-}
\ No newline at end of file
+}
From e7b11175bad0c8abeee9440f7792aeb4e4723995 Mon Sep 17 00:00:00 2001
From: Forbirdden <102984166+Forbirdden@users.noreply.github.com>
Date: Fri, 27 Jun 2025 22:35:11 +0500
Subject: [PATCH 09/20] =?UTF-8?q?=D0=98=D0=9A=D0=9E=D0=9D=D0=9A=D0=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/panel.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/panel.js b/src/panel.js
index 3d4d830..d5e53dc 100644
--- a/src/panel.js
+++ b/src/panel.js
@@ -202,10 +202,10 @@ function renderChannels() {
${srv.title}