UI - Change

This commit is contained in:
2025-11-18 20:53:30 +01:00
parent 755d026e43
commit fc5f285a57
23 changed files with 1298 additions and 342 deletions

500
src/UI.js Normal file
View 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">&times;</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);

View File

@@ -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
}
}

View File

@@ -3,47 +3,90 @@
<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>
<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>
<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>
</header>
</div>
</div>
<main>
<div id="channels">
<h4>Channels</h4>
<ul id="channelList"></ul>
<input id="newChannel" placeholder="#channel">
<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">Part</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>
<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>
</footer>
</main>
</div>
<script src="tauri.js"></script>
<script src="UI.js"></script>
</body>
</html>

View File

@@ -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);
});

View File

@@ -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;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

568
src/styles/styles.css Normal file
View 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
View 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();
}
});