UI - Change
This commit is contained in:
500
src/UI.js
Normal file
500
src/UI.js
Normal file
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
// this is needed for Tauri to work properly!
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
//
|
||||
|
||||
let greetInputEl;
|
||||
let greetMsgEl;
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
greetInputEl = document.querySelector("#greet-input");
|
||||
greetMsgEl = document.querySelector("#greet-msg");
|
||||
document.querySelector("#greet-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
// UI State Management
|
||||
const uiState = {
|
||||
connected: false,
|
||||
activeTab: null,
|
||||
channels: [],
|
||||
collapsedSidebar: false,
|
||||
userColors: new Map(),
|
||||
currentUser: 'IRCDUser'
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const sidebar = document.getElementById('channels');
|
||||
const sidebarResizeHandle = document.getElementById('sidebarResizeHandle');
|
||||
const toggleSidebarBtn = document.getElementById('toggleSidebar');
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
const joinBtn = document.getElementById('joinBtn');
|
||||
const partBtn = document.getElementById('partBtn');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('status');
|
||||
const tabBar = document.getElementById('tabBar');
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
const channelList = document.getElementById('channelList');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const channelInput = document.getElementById('newChannel');
|
||||
const commandSuggestions = document.getElementById('commandSuggestions');
|
||||
|
||||
// Color palette for users
|
||||
const userColors = [
|
||||
'#4b82ff', '#ff6b6b', '#7edba5', '#ffa94d',
|
||||
'#cc5de8', '#20c997', '#f06595', '#748ffc',
|
||||
'#ffd43b', '#69db7c', '#a9e34b', '#ff8787'
|
||||
];
|
||||
|
||||
// Initialize UI
|
||||
function initUI() {
|
||||
// Set current user from nickname input
|
||||
const nicknameInput = document.getElementById('nickname');
|
||||
uiState.currentUser = nicknameInput.value;
|
||||
nicknameInput.addEventListener('change', (e) => {
|
||||
uiState.currentUser = e.target.value;
|
||||
});
|
||||
|
||||
// Add event listeners
|
||||
toggleSidebarBtn.addEventListener('click', toggleSidebar);
|
||||
connectBtn.addEventListener('click', handleConnect);
|
||||
disconnectBtn.addEventListener('click', handleDisconnect);
|
||||
joinBtn.addEventListener('click', handleJoin);
|
||||
partBtn.addEventListener('click', handlePart);
|
||||
sendBtn.addEventListener('click', handleSend);
|
||||
messageInput.addEventListener('keydown', handleMessageInput);
|
||||
channelInput.addEventListener('keydown', handleChannelInput);
|
||||
|
||||
// Set up sidebar resizing
|
||||
setupSidebarResize();
|
||||
|
||||
// Set active tab
|
||||
setActiveTab('welcome');
|
||||
}
|
||||
|
||||
// Setup sidebar resizing
|
||||
function setupSidebarResize() {
|
||||
let isResizing = false;
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
sidebarResizeHandle.classList.add('resizing');
|
||||
document.body.style.cursor = 'col-resize';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const newWidth = e.clientX;
|
||||
if (newWidth > 200 && newWidth < 500) {
|
||||
sidebar.style.width = `${newWidth}px`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
sidebarResizeHandle.classList.remove('resizing');
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
function toggleSidebar() {
|
||||
uiState.collapsedSidebar = !uiState.collapsedSidebar;
|
||||
sidebar.classList.toggle('collapsed', uiState.collapsedSidebar);
|
||||
|
||||
// Update toggle button icon
|
||||
const icon = toggleSidebarBtn.querySelector('svg');
|
||||
if (uiState.collapsedSidebar) {
|
||||
icon.innerHTML = '<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
} else {
|
||||
icon.innerHTML = '<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
}
|
||||
}
|
||||
|
||||
// Get color for a user
|
||||
function getUserColor(username) {
|
||||
if (!uiState.userColors.has(username)) {
|
||||
// Assign a color from the palette based on username hash
|
||||
const hash = username.split('').reduce((a, b) => {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0);
|
||||
return a & a;
|
||||
}, 0);
|
||||
const colorIndex = Math.abs(hash) % userColors.length;
|
||||
uiState.userColors.set(username, userColors[colorIndex]);
|
||||
}
|
||||
return uiState.userColors.get(username);
|
||||
}
|
||||
|
||||
// Connection handlers
|
||||
function handleConnect() {
|
||||
uiState.connected = true;
|
||||
updateConnectionStatus();
|
||||
addSystemMessage(`Connecting to ${document.getElementById('server').value}...`);
|
||||
|
||||
// Simulate connection
|
||||
setTimeout(() => {
|
||||
addSystemMessage('Connected to server!');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
uiState.connected = false;
|
||||
updateConnectionStatus();
|
||||
addSystemMessage('Disconnected from server.');
|
||||
}
|
||||
|
||||
function updateConnectionStatus() {
|
||||
if (uiState.connected) {
|
||||
statusDot.classList.add('online');
|
||||
statusText.textContent = 'Connected';
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
} else {
|
||||
statusDot.classList.remove('online');
|
||||
statusText.textContent = 'Disconnected';
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Channel handlers
|
||||
function handleJoin() {
|
||||
let channel = channelInput.value.trim();
|
||||
if (channel && !channel.startsWith('#')) {
|
||||
channel = '#' + channel;
|
||||
channelInput.value = channel;
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
addChannel(channel);
|
||||
channelInput.value = '';
|
||||
addSystemMessage(`Joined ${channel}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePart() {
|
||||
if (uiState.activeTab && uiState.activeTab !== 'welcome') {
|
||||
const channel = uiState.activeTab;
|
||||
removeChannel(channel);
|
||||
addSystemMessage(`Left ${channel}`);
|
||||
}
|
||||
}
|
||||
|
||||
function addChannel(name) {
|
||||
if (!uiState.channels.includes(name)) {
|
||||
uiState.channels.push(name);
|
||||
|
||||
// Add to channel list
|
||||
const channelItem = document.createElement('div');
|
||||
channelItem.className = 'channel-item';
|
||||
channelItem.innerHTML = `
|
||||
<div class="channel-icon">#</div>
|
||||
<div class="channel-name">${name}</div>
|
||||
`;
|
||||
channelItem.addEventListener('click', () => setActiveTab(name));
|
||||
channelList.appendChild(channelItem);
|
||||
|
||||
// Add tab
|
||||
addTab(name);
|
||||
|
||||
// Auto-join the channel when connected
|
||||
if (uiState.connected) {
|
||||
invoke('join_channel', { channel: name }).catch(e => {
|
||||
addSystemMessage(`Failed to join ${name}: ${e}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeChannel(name) {
|
||||
const index = uiState.channels.indexOf(name);
|
||||
if (index > -1) {
|
||||
uiState.channels.splice(index, 1);
|
||||
|
||||
// Remove from channel list
|
||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||
channelItems.forEach(item => {
|
||||
if (item.querySelector('.channel-name').textContent === name) {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove tab
|
||||
removeTab(name);
|
||||
|
||||
// Auto-leave the channel when connected
|
||||
if (uiState.connected) {
|
||||
invoke('part_channel', { channel: name }).catch(e => {
|
||||
addSystemMessage(`Failed to leave ${name}: ${e}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab management
|
||||
function addTab(name) {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'tab';
|
||||
tab.dataset.tab = name;
|
||||
tab.innerHTML = `
|
||||
<span>${name}</span>
|
||||
<span class="tab-close">×</span>
|
||||
`;
|
||||
|
||||
tab.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('tab-close')) {
|
||||
removeTab(name);
|
||||
} else {
|
||||
setActiveTab(name);
|
||||
}
|
||||
});
|
||||
|
||||
tabBar.appendChild(tab);
|
||||
setActiveTab(name);
|
||||
}
|
||||
|
||||
function removeTab(name) {
|
||||
const tab = tabBar.querySelector(`[data-tab="${name}"]`);
|
||||
if (tab) {
|
||||
tab.remove();
|
||||
|
||||
// If this was the active tab, switch to another
|
||||
if (uiState.activeTab === name) {
|
||||
if (uiState.channels.length > 0) {
|
||||
setActiveTab(uiState.channels[0]);
|
||||
} else {
|
||||
setActiveTab('welcome');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveTab(name) {
|
||||
uiState.activeTab = name;
|
||||
|
||||
// Update tabs
|
||||
const tabs = tabBar.querySelectorAll('.tab');
|
||||
tabs.forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === name);
|
||||
});
|
||||
|
||||
// Update channels
|
||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||
channelItems.forEach(item => {
|
||||
const channelName = item.querySelector('.channel-name').textContent;
|
||||
item.classList.toggle('active', channelName === name);
|
||||
});
|
||||
|
||||
// Update messages
|
||||
updateMessagesForTab(name);
|
||||
}
|
||||
|
||||
function updateMessagesForTab(name) {
|
||||
// Clear messages
|
||||
messagesContainer.innerHTML = '';
|
||||
|
||||
// Add appropriate messages for the tab
|
||||
if (name === 'welcome') {
|
||||
addSystemMessage('IRCD - Welcome! SystemTime: ' + new Date().toLocaleString());
|
||||
} else {
|
||||
addSystemMessage(`Welcome to #${name}!`);
|
||||
addSystemMessage('This is the beginning of the channel.');
|
||||
}
|
||||
}
|
||||
|
||||
// Message handling
|
||||
function handleSend() {
|
||||
const message = messageInput.value.trim();
|
||||
if (message) {
|
||||
if (message.startsWith('/')) {
|
||||
handleCommand(message);
|
||||
} else {
|
||||
const target = uiState.activeTab;
|
||||
if (target && target !== 'welcome') {
|
||||
addMessage(uiState.currentUser, message, true);
|
||||
// Send the message to the server
|
||||
invoke('send_message', { target, message }).catch(e => {
|
||||
addSystemMessage(`Failed to send message: ${e}`);
|
||||
});
|
||||
} else {
|
||||
addSystemMessage('Join a channel to send messages.');
|
||||
}
|
||||
}
|
||||
messageInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommand(command) {
|
||||
addSystemMessage(`Command: ${command}`);
|
||||
// Handle specific commands here
|
||||
if (command.startsWith('/join ')) {
|
||||
const channel = command.split(' ')[1];
|
||||
if (channel) {
|
||||
addChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(sender, content, isUser = false) {
|
||||
const messageLine = document.createElement('div');
|
||||
messageLine.className = 'message-line';
|
||||
|
||||
const senderElement = document.createElement('div');
|
||||
senderElement.className = `message-sender ${isUser ? 'you' : ''}`;
|
||||
senderElement.textContent = isUser ? `${sender} (You):` : `${sender}:`;
|
||||
senderElement.style.color = isUser ? '' : getUserColor(sender);
|
||||
|
||||
const contentElement = document.createElement('div');
|
||||
contentElement.className = 'message-content';
|
||||
contentElement.textContent = content;
|
||||
|
||||
messageLine.appendChild(senderElement);
|
||||
messageLine.appendChild(contentElement);
|
||||
|
||||
messagesContainer.appendChild(messageLine);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function addSystemMessage(content) {
|
||||
const systemMessage = document.createElement('div');
|
||||
systemMessage.className = 'system-message';
|
||||
systemMessage.textContent = content;
|
||||
|
||||
messagesContainer.appendChild(systemMessage);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function addIRCMessage(content) {
|
||||
const ircMessage = document.createElement('div');
|
||||
ircMessage.className = 'irc-message';
|
||||
ircMessage.textContent = content;
|
||||
|
||||
messagesContainer.appendChild(ircMessage);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Input handlers
|
||||
function handleMessageInput(e) {
|
||||
if (e.key === 'Enter') {
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
|
||||
function handleChannelInput(e) {
|
||||
if (e.key === 'Enter') {
|
||||
handleJoin();
|
||||
}
|
||||
}
|
||||
|
||||
// Format IRC messages
|
||||
function formatIRCMessage(message) {
|
||||
try {
|
||||
// Parse the message object from Tauri
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
|
||||
// Handle different message types
|
||||
if (message.command) {
|
||||
const command = message.command;
|
||||
|
||||
// Handle PRIVMSG (regular messages)
|
||||
if (command === 'PRIVMSG' && message.params && message.params.length >= 2) {
|
||||
const sender = message.prefix ? extractNickname(message.prefix) : 'Unknown';
|
||||
const target = message.params[0];
|
||||
const content = message.params[1];
|
||||
|
||||
// Check if this message is for the active tab
|
||||
if (target === uiState.activeTab || (target === uiState.currentUser && uiState.activeTab === sender)) {
|
||||
addMessage(sender, content);
|
||||
}
|
||||
return `${sender} -> ${target}: ${content}`;
|
||||
}
|
||||
|
||||
// Handle JOIN
|
||||
if (command === 'JOIN' && message.params && message.params.length >= 1) {
|
||||
const sender = message.prefix ? extractNickname(message.prefix) : 'Unknown';
|
||||
const channel = message.params[0];
|
||||
|
||||
if (sender === uiState.currentUser) {
|
||||
addSystemMessage(`You joined ${channel}`);
|
||||
if (!uiState.channels.includes(channel)) {
|
||||
addChannel(channel);
|
||||
}
|
||||
} else {
|
||||
addSystemMessage(`${sender} joined ${channel}`);
|
||||
}
|
||||
return `${sender} joined ${channel}`;
|
||||
}
|
||||
|
||||
// Handle PART
|
||||
if (command === 'PART' && message.params && message.params.length >= 1) {
|
||||
const sender = message.prefix ? extractNickname(message.prefix) : 'Unknown';
|
||||
const channel = message.params[0];
|
||||
const reason = message.params[1] || '';
|
||||
|
||||
if (sender === uiState.currentUser) {
|
||||
addSystemMessage(`You left ${channel}${reason ? `: ${reason}` : ''}`);
|
||||
removeChannel(channel);
|
||||
} else {
|
||||
addSystemMessage(`${sender} left ${channel}${reason ? `: ${reason}` : ''}`);
|
||||
}
|
||||
return `${sender} left ${channel}`;
|
||||
}
|
||||
|
||||
// Handle server responses
|
||||
if (typeof command === 'object' && command.Response) {
|
||||
const [code, ...params] = command.Response;
|
||||
return `[${code}] ${params.join(' ')}`;
|
||||
}
|
||||
|
||||
// Handle raw commands
|
||||
if (typeof command === 'object' && command.Raw) {
|
||||
const [code, ...params] = command.Raw;
|
||||
return `[RAW ${code}] ${params.join(' ')}`;
|
||||
}
|
||||
|
||||
// Fallback: stringify the message
|
||||
return JSON.stringify(message);
|
||||
}
|
||||
|
||||
return String(message);
|
||||
} catch (e) {
|
||||
console.error('Error formatting IRC message:', e);
|
||||
return String(message);
|
||||
}
|
||||
}
|
||||
|
||||
function extractNickname(prefix) {
|
||||
if (typeof prefix === 'string') {
|
||||
return prefix.split('!')[0];
|
||||
}
|
||||
if (prefix && prefix.Nickname) {
|
||||
return prefix.Nickname[0];
|
||||
}
|
||||
if (prefix && prefix.ServerName) {
|
||||
return prefix.ServerName;
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
// Initialize the UI when the page loads
|
||||
document.addEventListener('DOMContentLoaded', initUI);
|
||||
@@ -1,42 +0,0 @@
|
||||
function formatIRCMessage(msg) {
|
||||
// Tauri gives us the message as a stringified Rust Debug object
|
||||
// e.g. Message { tags: None, prefix: Some(Nickname("Nick", "", "")), command: PRIVMSG("#chan", "Hello") }
|
||||
// We'll try to parse key info using regex
|
||||
|
||||
try {
|
||||
// Extract the prefix (who sent it)
|
||||
const prefixMatch = msg.match(/prefix: Some\(([^)]+)\)/);
|
||||
let sender = prefixMatch ? prefixMatch[1] : "Unknown";
|
||||
|
||||
// Extract the command and arguments
|
||||
const cmdMatch = msg.match(/command: (\w+)\((.*)\)/);
|
||||
if (!cmdMatch) return msg;
|
||||
|
||||
const command = cmdMatch[1];
|
||||
const argsRaw = cmdMatch[2];
|
||||
|
||||
// Simple handler for common IRC commands
|
||||
switch(command) {
|
||||
case 'PRIVMSG': {
|
||||
const args = argsRaw.split(/,(.+)/); // split first comma
|
||||
const target = args[0].replace(/^"|"$/g, '');
|
||||
const message = args[1].replace(/^"|"$/g, '');
|
||||
return `[${target}] <${sender}> ${message}`;
|
||||
}
|
||||
case 'NOTICE': {
|
||||
const args = argsRaw.split(/,(.+)/);
|
||||
const target = args[0].replace(/^"|"$/g, '');
|
||||
const message = args[1].replace(/^"|"$/g, '');
|
||||
return `[NOTICE ${target}] ${message}`;
|
||||
}
|
||||
case 'Response':
|
||||
case 'Raw':
|
||||
case 'UserMODE':
|
||||
return `[${command}] ${argsRaw}`;
|
||||
default:
|
||||
return msg; // fallback for unknown messages
|
||||
}
|
||||
} catch(e) {
|
||||
return msg; // fallback if parsing fails
|
||||
}
|
||||
}
|
||||
123
src/index.html
123
src/index.html
@@ -1,49 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tauri IRC Client</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin:0; display:flex; flex-direction:column; height:100vh; }
|
||||
header { background:#222; color:#fff; padding:10px; display:flex; gap:10px; align-items:center; }
|
||||
main { flex:1; display:flex; overflow:hidden; }
|
||||
#channels { width:200px; background:#f0f0f0; padding:10px; overflow-y:auto; }
|
||||
#chat { flex:1; display:flex; flex-direction:column; padding:10px; }
|
||||
#messages { flex:1; overflow-y:auto; border:1px solid #ccc; padding:5px; margin-bottom:5px; background:#fff; }
|
||||
#input { display:flex; gap:5px; }
|
||||
input, button { padding:5px; }
|
||||
button { cursor:pointer; }
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IRCd</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="./styles/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="channels">
|
||||
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
||||
<div class="sidebar-header">
|
||||
<h1>IRCd</h1>
|
||||
<button class="toggle-sidebar" id="toggleSidebar">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<input id="server" placeholder="Server" value="irc.libera.chat">
|
||||
<input id="port" type="number" placeholder="Port" value="6667">
|
||||
<input id="nickname" placeholder="Nickname" value="TauriUser">
|
||||
<button id="connectBtn">Connect</button>
|
||||
<button id="disconnectBtn" disabled>Disconnect</button>
|
||||
<span id="status">Disconnected</span>
|
||||
</header>
|
||||
<div class="connection-section">
|
||||
<h3 class="section-title">Connection</h3>
|
||||
<div class="form-group">
|
||||
<label for="server">Server</label>
|
||||
<input id="server" type="text" value="irc.libera.chat" placeholder="irc.example.net">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="port">Port</label>
|
||||
<input id="port" type="number" value="6667" min="1" max="9999">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="nickname">Nickname</label>
|
||||
<input id="nickname" type="text" value="IRCDUser">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="connectBtn" class="btn-primary">Connect</button>
|
||||
<button id="disconnectBtn" disabled>Disconnect</button>
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="status">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div id="channels">
|
||||
<h4>Channels</h4>
|
||||
<ul id="channelList"></ul>
|
||||
<input id="newChannel" placeholder="#channel">
|
||||
<button id="joinBtn">Join</button>
|
||||
<button id="partBtn">Part</button>
|
||||
<div class="channel-section">
|
||||
<h3 class="section-title">Channels</h3>
|
||||
<div class="form-group">
|
||||
<label for="newChannel">Join Channel</label>
|
||||
<input id="newChannel" type="text" placeholder="#channel">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="joinBtn">Join</button>
|
||||
<button id="partBtn">Leave</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="channel-list" id="channelList">
|
||||
<!-- Channels will be added here dynamically -->
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="tabs" id="tabBar">
|
||||
<!-- Tabs will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<section class="messages" id="messages">
|
||||
<!-- Messages will be added here dynamically -->
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="composer">
|
||||
<div class="composer-input">
|
||||
<input id="messageInput" type="text" placeholder="Type message...">
|
||||
<div id="commandSuggestions" class="suggestions hidden"></div>
|
||||
</div>
|
||||
<button id="sendBtn">Send</button>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<div id="chat">
|
||||
<div id="messages"></div>
|
||||
<div id="input">
|
||||
<input id="messageInput" placeholder="Type message...">
|
||||
<button id="sendBtn">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="main.js"></script>
|
||||
</main>
|
||||
|
||||
|
||||
<script src="tauri.js"></script>
|
||||
<script src="UI.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
148
src/main.js
148
src/main.js
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
// this is needed for Tauri to work properly!
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
//
|
||||
|
||||
let greetInputEl;
|
||||
let greetMsgEl;
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
greetInputEl = document.querySelector("#greet-input");
|
||||
greetMsgEl = document.querySelector("#greet-msg");
|
||||
document.querySelector("#greet-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
});
|
||||
});
|
||||
*/
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
|
||||
const serverEl = document.getElementById('server');
|
||||
const portEl = document.getElementById('port');
|
||||
const nicknameEl = document.getElementById('nickname');
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
const channelListEl = document.getElementById('channelList');
|
||||
const newChannelEl = document.getElementById('newChannel');
|
||||
const joinBtn = document.getElementById('joinBtn');
|
||||
const partBtn = document.getElementById('partBtn');
|
||||
|
||||
const messagesEl = document.getElementById('messages');
|
||||
const messageInputEl = document.getElementById('messageInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
|
||||
function addMessage(msg) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = msg;
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
async function refreshChannels() {
|
||||
try {
|
||||
const channels = await invoke("list_channels");
|
||||
channelListEl.innerHTML = '';
|
||||
channels.forEach(ch => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = ch;
|
||||
li.dataset.channel = ch;
|
||||
li.onclick = () => { newChannelEl.value = ch; };
|
||||
channelListEl.appendChild(li);
|
||||
});
|
||||
} catch (e) {
|
||||
addMessage("Failed to refresh channels: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.onclick = async () => {
|
||||
const server = serverEl.value.trim();
|
||||
const port = Number(portEl.value);
|
||||
const nickname = nicknameEl.value.trim();
|
||||
|
||||
if (!server || !port || !nickname) {
|
||||
addMessage('Please fill in server, port, and nickname.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('connect', {
|
||||
server: serverEl.value.trim(),
|
||||
port: Number(portEl.value),
|
||||
nickname: nicknameEl.value.trim(),
|
||||
useTls: false // must match Rust parameter exactly
|
||||
});
|
||||
|
||||
|
||||
statusEl.textContent = 'Connected';
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
addMessage('Connected to ' + server);
|
||||
refreshChannels();
|
||||
} catch(e) {
|
||||
addMessage('Connection failed: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
disconnectBtn.onclick = async () => {
|
||||
try {
|
||||
await invoke('disconnect');
|
||||
statusEl.textContent = 'Disconnected';
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
channelListEl.innerHTML = '';
|
||||
addMessage('Disconnected');
|
||||
} catch(e) {
|
||||
addMessage('Disconnect failed: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
joinBtn.onclick = async () => {
|
||||
const channel = newChannelEl.value.trim();
|
||||
if (!channel) return;
|
||||
try {
|
||||
await invoke('join_channel', { channel });
|
||||
addMessage(`Joined ${channel}`);
|
||||
refreshChannels();
|
||||
} catch(e) {
|
||||
addMessage(`Failed to join ${channel}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
partBtn.onclick = async () => {
|
||||
const channel = newChannelEl.value.trim();
|
||||
if (!channel) return;
|
||||
try {
|
||||
await invoke('part_channel', { channel });
|
||||
addMessage(`Left ${channel}`);
|
||||
refreshChannels();
|
||||
} catch(e) {
|
||||
addMessage(`Failed to leave ${channel}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
sendBtn.onclick = async () => {
|
||||
const target = newChannelEl.value.trim();
|
||||
const message = messageInputEl.value.trim();
|
||||
if (!target || !message) return;
|
||||
try {
|
||||
await invoke('send_message', { target, message });
|
||||
addMessage(`<You> ${message}`);
|
||||
messageInputEl.value = '';
|
||||
} catch(e) {
|
||||
addMessage(`Failed to send message: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to backend IRC messages
|
||||
listen('irc-message', event => {
|
||||
const formatted = formatIRCMessage(event.payload);
|
||||
});
|
||||
|
||||
112
src/styles.css
112
src/styles.css
@@ -1,112 +0,0 @@
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #ffe21c);
|
||||
}
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
BIN
src/styles/font/JetBrainsMono-Bold.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Bold.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-BoldItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraBold.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraBoldItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraLight.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraLightItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Italic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Italic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Light.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Light.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-LightItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-LightItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Medium.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Medium.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-MediumItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Regular.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Regular.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-SemiBold.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-SemiBold.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-SemiBoldItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Thin.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Thin.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ThinItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ThinItalic.woff2
Normal file
Binary file not shown.
568
src/styles/styles.css
Normal file
568
src/styles/styles.css
Normal file
@@ -0,0 +1,568 @@
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-Thin.woff2") format("woff2");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-Light.woff2") format("woff2");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-LightItalic.woff2") format("woff2");
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-Italic.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-Medium.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-Bold.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("./styles/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: "Inter", "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
background: #1a1c21;
|
||||
color: #f1f1f1;
|
||||
--sidebar-width: 280px;
|
||||
--sidebar-collapsed-width: 60px;
|
||||
--primary-color: #4b82ff;
|
||||
--success-color: #7edba5;
|
||||
--danger-color: #d36d6d;
|
||||
--bg-dark: #111217;
|
||||
--bg-panel: #181a1f;
|
||||
--bg-content: #1a1c21;
|
||||
--border-color: #2a2d35;
|
||||
--text-muted: #9ea4b9;
|
||||
--message-bg: #1e2028;
|
||||
--message-bg-user: #2d4fcc;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-dark);
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: var(--bg-panel);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
resize: horizontal;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle:hover,
|
||||
.sidebar-resize-handle.resizing {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: var(--sidebar-collapsed-width);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toggle-sidebar {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-sidebar:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.connection-section,
|
||||
.channel-section {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #3a3f4d;
|
||||
border-radius: 4px;
|
||||
background: #101218;
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #3a3f4d;
|
||||
background: #242730;
|
||||
color: #f1f1f1;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #2c303a;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #5a92ff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger-color);
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--danger-color);
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background: var(--success-color);
|
||||
}
|
||||
|
||||
.channel-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.channel-item {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.channel-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.channel-item.active {
|
||||
background: rgba(75, 130, 255, 0.15);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.channel-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Main Content Styles */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-content);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: #14151a;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: 1px solid #2f323a;
|
||||
border-bottom: none;
|
||||
background: #1b1d23;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #23252d;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 4px;
|
||||
opacity: 0.6;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message-line {
|
||||
display: flex;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
min-width: 120px;
|
||||
font-weight: 500;
|
||||
padding-right: 8px;
|
||||
text-align: right;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.message-sender.you {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.irc-message {
|
||||
color: #a0a0a0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
|
||||
.composer-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #2a2e35;
|
||||
border-radius: 24px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.composer-input input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #f1f1f1;
|
||||
padding: 12px 0;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.composer-input input::placeholder {
|
||||
color: #6a7283;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
bottom: 110%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #101116;
|
||||
border: 1px solid #3a3f4d;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.suggestions button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.suggestions button:hover,
|
||||
.suggestions button.active {
|
||||
background: #222430;
|
||||
}
|
||||
|
||||
.suggestions.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Collapsed sidebar styles */
|
||||
.sidebar.collapsed .section-title,
|
||||
.sidebar.collapsed .form-group,
|
||||
.sidebar.collapsed .button-group,
|
||||
.sidebar.collapsed .status-indicator,
|
||||
.sidebar.collapsed .channel-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-header h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .connection-section,
|
||||
.sidebar.collapsed .channel-section {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .channel-item {
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar:not(.collapsed) {
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.composer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
147
src/tauri.js
Normal file
147
src/tauri.js
Normal file
@@ -0,0 +1,147 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
|
||||
// Original Tauri integration code
|
||||
async function refreshChannels() {
|
||||
try {
|
||||
const channels = await invoke("list_channels");
|
||||
// Clear existing channels that aren't in the new list
|
||||
const currentChannels = [...uiState.channels];
|
||||
currentChannels.forEach(ch => {
|
||||
if (!channels.includes(ch)) {
|
||||
removeChannel(ch);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new channels
|
||||
channels.forEach(ch => {
|
||||
if (!uiState.channels.includes(ch)) {
|
||||
addChannel(ch);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
addSystemMessage("Failed to refresh channels: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Override the connect button handler to use Tauri
|
||||
connectBtn.onclick = async () => {
|
||||
const server = document.getElementById('server').value.trim();
|
||||
const port = Number(document.getElementById('port').value);
|
||||
const nickname = document.getElementById('nickname').value.trim();
|
||||
|
||||
if (!server || !port || !nickname) {
|
||||
addSystemMessage('Please fill in server, port, and nickname.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('connect', {
|
||||
server: server,
|
||||
port: port,
|
||||
nickname: nickname,
|
||||
useTls: false
|
||||
});
|
||||
|
||||
uiState.connected = true;
|
||||
updateConnectionStatus();
|
||||
addSystemMessage('Connected to ' + server);
|
||||
|
||||
// Refresh channels after connection
|
||||
setTimeout(refreshChannels, 1000);
|
||||
} catch(e) {
|
||||
addSystemMessage('Connection failed: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
// Override the disconnect button handler to use Tauri
|
||||
disconnectBtn.onclick = async () => {
|
||||
try {
|
||||
await invoke('disconnect');
|
||||
uiState.connected = false;
|
||||
updateConnectionStatus();
|
||||
|
||||
// Clear channels and tabs
|
||||
uiState.channels.forEach(channel => {
|
||||
removeChannel(channel);
|
||||
});
|
||||
setActiveTab('welcome');
|
||||
addSystemMessage('Disconnected');
|
||||
} catch(e) {
|
||||
addSystemMessage('Disconnect failed: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
// Override the join button handler to use Tauri
|
||||
joinBtn.onclick = async () => {
|
||||
let channel = channelInput.value.trim();
|
||||
if (!channel) return;
|
||||
|
||||
if (!channel.startsWith('#')) {
|
||||
channel = '#' + channel;
|
||||
channelInput.value = channel;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('join_channel', { channel });
|
||||
addChannel(channel);
|
||||
channelInput.value = '';
|
||||
} catch(e) {
|
||||
addSystemMessage(`Failed to join ${channel}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Override the part button handler to use Tauri
|
||||
partBtn.onclick = async () => {
|
||||
const channel = uiState.activeTab;
|
||||
if (!channel || channel === 'welcome') {
|
||||
addSystemMessage('Select a channel to leave first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('part_channel', { channel });
|
||||
removeChannel(channel);
|
||||
} catch(e) {
|
||||
addSystemMessage(`Failed to leave ${channel}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Override the send button handler to use Tauri
|
||||
sendBtn.onclick = async () => {
|
||||
const target = uiState.activeTab;
|
||||
const message = messageInput.value.trim();
|
||||
|
||||
if (!target || target === 'welcome') {
|
||||
addSystemMessage('Join a channel to send messages.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message) return;
|
||||
|
||||
try {
|
||||
await invoke('send_message', { target, message });
|
||||
addMessage(uiState.currentUser, message, true);
|
||||
messageInput.value = '';
|
||||
} catch(e) {
|
||||
addSystemMessage(`Failed to send message: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to backend IRC messages
|
||||
listen('irc-message', event => {
|
||||
if (event.payload) {
|
||||
/*const formattedMessage = formatIRCMessage(event.payload);
|
||||
if (formattedMessage) {
|
||||
addIRCMessage(formattedMessage);
|
||||
}*/
|
||||
addIRCMessage(event.payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for channel updates
|
||||
listen('channel-update', event => {
|
||||
if (event.payload) {
|
||||
refreshChannels();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user