Initial commit
This commit is contained in:
420
src/lib/components/MudConnection.svelte
Normal file
420
src/lib/components/MudConnection.svelte
Normal file
@@ -0,0 +1,420 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { MudConnection } from '$lib/connection/MudConnection';
|
||||
import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
|
||||
import { TriggerSystem } from '$lib/triggers/TriggerSystem';
|
||||
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
|
||||
import {
|
||||
connections,
|
||||
connectionStatus,
|
||||
activeProfileId,
|
||||
activeProfile,
|
||||
addToOutputHistory,
|
||||
updateGmcpData,
|
||||
accessibilitySettings,
|
||||
uiSettings
|
||||
} from '$lib/stores/mudStore';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Props
|
||||
export let profileId: string;
|
||||
export let autoConnect = false;
|
||||
|
||||
// Local state
|
||||
let connection: MudConnection | null = null;
|
||||
let gmcpHandler: GmcpHandler | null = null;
|
||||
let triggerSystem: TriggerSystem | null = null;
|
||||
let accessibilityManager: AccessibilityManager | null = null;
|
||||
|
||||
/**
|
||||
* Handle keyboard events for speech control
|
||||
*/
|
||||
function handleKeyDown(event: KeyboardEvent): void {
|
||||
// Control key stops speech - the key code for Control is 17
|
||||
if (event.key === 'Control' || event.ctrlKey || event.keyCode === 17) {
|
||||
// Stop propagation to ensure no other handlers interfere
|
||||
event.stopPropagation();
|
||||
|
||||
if (accessibilityManager && accessibilityManager.isSpeaking()) {
|
||||
console.log('Control key pressed - stopping speech');
|
||||
accessibilityManager.stopSpeech();
|
||||
}
|
||||
}
|
||||
|
||||
// Enter key stops speech if the setting is enabled
|
||||
if ((event.key === 'Enter' || event.keyCode === 13) && $accessibilitySettings.interruptSpeechOnEnter) {
|
||||
if (accessibilityManager && accessibilityManager.isSpeaking()) {
|
||||
console.log('Enter key pressed - interrupting speech');
|
||||
accessibilityManager.stopSpeech();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Initialize components
|
||||
initializeComponents();
|
||||
|
||||
// Set up keyboard listener for speech control - use capture phase and make it top-level
|
||||
document.addEventListener('keydown', handleKeyDown, true); // Using document instead of window
|
||||
|
||||
// Auto-connect if enabled
|
||||
if (autoConnect) {
|
||||
console.log('Auto-connecting profile:', profileId);
|
||||
connect();
|
||||
}
|
||||
|
||||
// Update connection status - ensure UI shows status as 'disconnected' at start
|
||||
connectionStatus.update(statuses => ({
|
||||
...statuses,
|
||||
[profileId]: 'disconnected'
|
||||
}));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Remove keyboard listener
|
||||
document.removeEventListener('keydown', handleKeyDown, true); // Match document and capture phase
|
||||
|
||||
// Clean up connection
|
||||
if (connection) {
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
// Remove from connections store
|
||||
connections.update(conns => {
|
||||
const newConns = { ...conns };
|
||||
delete newConns[profileId];
|
||||
return newConns;
|
||||
});
|
||||
|
||||
// Update connection status
|
||||
connectionStatus.update(statuses => {
|
||||
const newStatuses = { ...statuses };
|
||||
delete newStatuses[profileId];
|
||||
return newStatuses;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize connection components
|
||||
*/
|
||||
function initializeComponents() {
|
||||
// Create GMCP handler
|
||||
gmcpHandler = new GmcpHandler();
|
||||
|
||||
// Create trigger system
|
||||
triggerSystem = new TriggerSystem();
|
||||
|
||||
// Create accessibility manager
|
||||
accessibilityManager = new AccessibilityManager();
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for components
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
if (!gmcpHandler || !triggerSystem || !accessibilityManager) return;
|
||||
|
||||
// GMCP handler events
|
||||
gmcpHandler.on('gmcp', (module, data) => {
|
||||
updateGmcpData(module, data);
|
||||
});
|
||||
|
||||
gmcpHandler.on('sendGmcp', (module, data) => {
|
||||
if (connection) {
|
||||
connection.sendGmcp(module, data);
|
||||
}
|
||||
});
|
||||
|
||||
gmcpHandler.on('playSound', (url, volume, loop) => {
|
||||
dispatch('playSound', { url, volume, loop });
|
||||
});
|
||||
|
||||
// Trigger system events
|
||||
triggerSystem.on('sendText', (text) => {
|
||||
if (connection) {
|
||||
connection.send(text);
|
||||
}
|
||||
});
|
||||
|
||||
triggerSystem.on('highlight', (text, pattern, color, isRegex) => {
|
||||
dispatch('highlight', { text, pattern, color, isRegex });
|
||||
});
|
||||
|
||||
// Set up TTS subscription - use direct subscription to avoid circular updates
|
||||
const unsubscribeTts = accessibilitySettings.subscribe(settings => {
|
||||
try {
|
||||
// Set speech enabled directly based on the store value
|
||||
if (accessibilityManager) {
|
||||
console.log('Setting TTS from store:', settings.textToSpeech);
|
||||
accessibilityManager.setSpeechEnabled(settings.textToSpeech);
|
||||
|
||||
// Update speech options when settings change
|
||||
accessibilityManager.updateSpeechOptions({
|
||||
rate: settings.speechRate,
|
||||
pitch: settings.speechPitch,
|
||||
volume: settings.speechVolume
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating TTS setting:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up only GMCP debugging subscription
|
||||
let previousGmcpDebug = $uiSettings.debugGmcp;
|
||||
const unsubscribeUiSettings = uiSettings.subscribe(settings => {
|
||||
if (settings.debugGmcp !== previousGmcpDebug) {
|
||||
previousGmcpDebug = settings.debugGmcp;
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up subscriptions on component destruction
|
||||
onDestroy(() => {
|
||||
unsubscribeUiSettings();
|
||||
unsubscribeTts();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the MUD server
|
||||
*/
|
||||
export function connect() {
|
||||
const profile = get(activeProfile);
|
||||
console.log('Connecting to profile:', profile);
|
||||
|
||||
if (!profile) {
|
||||
addToOutputHistory('Error: No active profile selected.');
|
||||
console.error('No active profile selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
connectionStatus.update(statuses => ({
|
||||
...statuses,
|
||||
[profileId]: 'connecting'
|
||||
}));
|
||||
|
||||
try {
|
||||
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`);
|
||||
|
||||
// Create connection
|
||||
connection = new MudConnection({
|
||||
host: profile.host,
|
||||
port: profile.port,
|
||||
useSSL: profile.useSSL,
|
||||
gmcpHandler: gmcpHandler || undefined
|
||||
});
|
||||
|
||||
console.log('Connection object created:', connection);
|
||||
|
||||
// Set up connection event listeners
|
||||
connection.on('connected', handleConnected);
|
||||
connection.on('disconnected', handleDisconnected);
|
||||
connection.on('error', handleError);
|
||||
connection.on('received', handleReceived);
|
||||
connection.on('sent', handleSent);
|
||||
|
||||
// Add to connections store
|
||||
connections.update(conns => ({
|
||||
...conns,
|
||||
[profileId]: connection as MudConnection
|
||||
}));
|
||||
|
||||
// Connect
|
||||
connection.connect();
|
||||
} catch (error) {
|
||||
console.error('Failed to connect:', error);
|
||||
|
||||
connectionStatus.update(statuses => ({
|
||||
...statuses,
|
||||
[profileId]: 'error'
|
||||
}));
|
||||
|
||||
addToOutputHistory(`Error connecting to ${profile.host}:${profile.port} - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the MUD server
|
||||
*/
|
||||
export function disconnect() {
|
||||
if (connection) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection established
|
||||
*/
|
||||
function handleConnected() {
|
||||
const profile = get(activeProfile);
|
||||
|
||||
connectionStatus.update(statuses => ({
|
||||
...statuses,
|
||||
[profileId]: 'connected'
|
||||
}));
|
||||
|
||||
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
|
||||
|
||||
// Handle auto-login if enabled
|
||||
if (profile?.autoLogin?.enabled) {
|
||||
setTimeout(() => {
|
||||
// Send username
|
||||
if (profile.autoLogin?.username) {
|
||||
if (connection) connection.send(profile.autoLogin.username);
|
||||
}
|
||||
|
||||
// Send password after a delay
|
||||
if (profile.autoLogin?.password) {
|
||||
setTimeout(() => {
|
||||
if (connection) connection.send(profile.autoLogin?.password || '');
|
||||
|
||||
// Send additional commands
|
||||
if (profile.autoLogin?.commands && profile.autoLogin.commands.length > 0) {
|
||||
let delay = 500;
|
||||
profile.autoLogin.commands.forEach((cmd) => {
|
||||
setTimeout(() => {
|
||||
if (connection) connection.send(cmd);
|
||||
}, delay);
|
||||
delay += 500;
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
dispatch('connected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection closed
|
||||
*/
|
||||
function handleDisconnected() {
|
||||
connectionStatus.update(statuses => ({
|
||||
...statuses,
|
||||
[profileId]: 'disconnected'
|
||||
}));
|
||||
|
||||
addToOutputHistory('Disconnected from server.');
|
||||
dispatch('disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection error
|
||||
*/
|
||||
function handleError(error: any) {
|
||||
connectionStatus.update(statuses => ({
|
||||
...statuses,
|
||||
[profileId]: 'error'
|
||||
}));
|
||||
|
||||
// Format the error message for display
|
||||
const errorMessage = typeof error === 'object' ?
|
||||
(error.message || JSON.stringify(error)) :
|
||||
String(error);
|
||||
|
||||
addToOutputHistory(`Connection error: ${errorMessage}`, false, [
|
||||
{ pattern: 'Connection error', color: '#ff5555', isRegex: false }
|
||||
]);
|
||||
|
||||
dispatch('error', { error });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle received data
|
||||
*/
|
||||
function handleReceived(text: string) {
|
||||
// Add to output history first
|
||||
addToOutputHistory(text);
|
||||
|
||||
// Process triggers with safe error handling
|
||||
if (triggerSystem) {
|
||||
try {
|
||||
triggerSystem.processTriggers(text);
|
||||
} catch (error) {
|
||||
console.error('Error processing triggers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use text-to-speech if enabled
|
||||
if (accessibilityManager && $accessibilitySettings.textToSpeech) {
|
||||
// Use a small timeout to avoid UI blocking
|
||||
setTimeout(() => {
|
||||
try {
|
||||
accessibilityManager.speak(text);
|
||||
} catch (error) {
|
||||
console.error('Error using text-to-speech:', error);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
dispatch('received', { text });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sent data
|
||||
*/
|
||||
function handleSent(text: string) {
|
||||
dispatch('sent', { text });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $connectionStatus[profileId]}
|
||||
<div class="mud-connection-status" role="status" aria-live="polite">
|
||||
<div class="status-indicator status-{$connectionStatus[profileId]}" aria-hidden="true"></div>
|
||||
<span>Server status: {$connectionStatus[profileId]}</span>
|
||||
{#if $connectionStatus[profileId] === 'error'}
|
||||
<span class="sr-only">There was an error connecting to the MUD server. Please check your settings and try again.</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.mud-connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: #50fa7b;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background-color: #f1fa8c;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #ff5555;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user