Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- (() => {
- 'use strict';
- // Prevent duplicates if script is run multiple times
- if (window.xboxMacroInitializedV3_2) { // Updated version flag
- console.warn("[Xbox Macro] Version 3.2.0 or similar already initialized. Aborting.");
- const existingGui = document.getElementById('xboxMacroGUI');
- if (existingGui && typeof alert === 'function') { // Check if alert is available
- alert("Xbox Macro GUI is already running. Please refresh the page if you want to reload it.");
- }
- return;
- }
- window.xboxMacroInitializedV3_2 = true;
- // Configuration
- const CONFIG = {
- STORAGE_KEY: 'xboxCloudMacros_v3_2', // Updated storage key
- SETTINGS_STORAGE_KEY: 'xboxCloudMacroSettings_v3_2', // Updated settings key
- GAME_STREAM_ID: 'game-stream',
- DEFAULT_REPEAT_DELAY: 250,
- DEFAULT_ACTION_DELAY: 50, // Default delay for an action if not specified in a sequence
- DEFAULT_MOUSE_MOVEMENT_INTERNAL_DELAY: 100,
- DEFAULT_MOUSE_CLICK_DURATION: 50,
- GUI_POSITION: { top: '20px', right: '20px' },
- VERSION: '3.2.0', // Updated version
- NOTIFICATION_DURATION: 4000 // Duration for notifications in ms
- };
- // State
- const state = {
- macros: {},
- isGuiVisible: true,
- isGuiLocked: false,
- isDragging: false,
- dragOffset: { x: 0, y: 0 },
- activeTab: 'create',
- gamepads: [], // Array to hold connected gamepads
- gamepadPollingInterval: null, // Interval ID for polling
- activeGamepadIndex: 0, // Index of the currently selected gamepad
- gamepadMappings: {} // { gamepadInput: macroTriggerKey } e.g., { 'button-0': 'space', 'axis-0-pos': 'd' }
- };
- // Macro class (largely unchanged, added ID generation)
- class Macro {
- constructor(triggerKey, type, params, mode, description = '') {
- this.triggerKey = triggerKey.toLowerCase();
- this.type = type;
- this.params = params;
- this.mode = mode;
- this.description = description;
- this.isRunning = false;
- this.currentActionIndex = 0; // For sequences (key or other types in future)
- this.repeatTimeout = null;
- this.actionTimeout = null; // For individual actions within a sequence
- this.runCount = 0;
- this.createdAt = Date.now();
- this.id = `macro-${this.triggerKey}-${this.createdAt}`; // Unique ID
- }
- serialize() {
- return {
- type: this.type,
- params: this.params,
- mode: this.mode,
- description: this.description,
- createdAt: this.createdAt // Include createdAt for consistent ID generation on load
- };
- }
- start() {
- if (this.isRunning) return;
- this.isRunning = true;
- this.currentActionIndex = 0;
- this.runCount = 0;
- console.log(`[Xbox Macro] Started macro: ${this.triggerKey} (Type: ${this.type}, Mode: ${this.mode})`);
- this.executeNextAction();
- updateMacroStatus(this.triggerKey, true, this.runCount);
- }
- stop() {
- if (!this.isRunning) return;
- this.isRunning = false;
- clearTimeout(this.repeatTimeout);
- clearTimeout(this.actionTimeout);
- this.currentActionIndex = 0;
- updateMacroStatus(this.triggerKey, false, this.runCount);
- console.log(`[Xbox Macro] Stopped macro: ${this.triggerKey}`);
- }
- executeNextAction() {
- if (!this.isRunning) return;
- let nextDelayForActionOrRepeat = this.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY;
- switch (this.type) {
- case 'keySequence':
- if (this.params.actions && this.params.actions.length > 0) {
- if (this.currentActionIndex < this.params.actions.length) {
- const action = this.params.actions[this.currentActionIndex];
- simulateKeyPress(action.key);
- nextDelayForActionOrRepeat = action.delayAfter || CONFIG.DEFAULT_ACTION_DELAY;
- this.currentActionIndex++;
- if (this.currentActionIndex >= this.params.actions.length) { // End of sequence for this iteration
- this.runCount++;
- updateMacroStatus(this.triggerKey, true, this.runCount);
- this.currentActionIndex = 0; // Reset for next full sequence repeat
- nextDelayForActionOrRepeat = this.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY; // Use overall repeat delay
- this.actionTimeout = setTimeout(() => this.executeNextAction(), nextDelayForActionOrRepeat);
- } else {
- // Delay for the next action in the current sequence
- this.actionTimeout = setTimeout(() => this.executeNextAction(), nextDelayForActionOrRepeat);
- }
- return; // Return to avoid falling through to the main repeatTimeout logic yet
- }
- } else {
- console.warn(`[Xbox Macro] Action sequence for ${this.triggerKey} is empty or invalid.`);
- this.stop(); return;
- }
- break; // Should be handled by return above, but good practice
- case 'mouseMovement':
- simulateMouseMovement(
- this.params.startX, this.params.startY,
- this.params.endX, this.params.endY,
- this.params.movementInternalDelay
- );
- this.runCount++;
- nextDelayForActionOrRepeat = this.params.repeatDelay;
- break;
- case 'mouseClick':
- simulateMouseClick(
- this.params.x, this.params.y,
- this.params.button, this.params.clickDuration
- );
- this.runCount++;
- nextDelayForActionOrRepeat = this.params.repeatDelay;
- break;
- case 'gamepadButtonPress': // New Macro Type
- if (typeof this.params.buttonIndex === 'number') {
- simulateGamepadButtonPress(state.activeGamepadIndex, this.params.buttonIndex, this.params.pressDuration);
- this.runCount++;
- nextDelayForActionOrRepeat = this.params.repeatDelay;
- } else {
- console.warn(`[Xbox Macro] Invalid button index for gamepad macro: ${this.triggerKey}`);
- this.stop(); return;
- }
- break;
- case 'gamepadAxisMove': // New Macro Type
- if (typeof this.params.axisIndex === 'number' && typeof this.params.axisValue === 'number') {
- simulateGamepadAxisMove(state.activeGamepadIndex, this.params.axisIndex, this.params.axisValue);
- this.runCount++;
- nextDelayForActionOrRepeat = this.params.repeatDelay;
- } else {
- console.warn(`[Xbox Macro] Invalid axis parameters for gamepad macro: ${this.triggerKey}`);
- this.stop(); return;
- }
- break;
- default:
- console.error(`[Xbox Macro] Unknown macro type: ${this.type} for trigger: ${this.triggerKey}`);
- this.stop(); return;
- }
- updateMacroStatus(this.triggerKey, true, this.runCount);
- if (this.isRunning) {
- // For non-sequence types, or after a full sequence completes and needs to repeat
- this.repeatTimeout = setTimeout(() => this.executeNextAction(), nextDelayForActionOrRepeat);
- }
- }
- toggle() {
- this.isRunning ? this.stop() : this.start();
- }
- }
- // --- Simulation Functions (largely unchanged, added gamepad simulation) ---
- function simulateKeyPress(key) {
- const targetElement = document.getElementById(CONFIG.GAME_STREAM_ID) || document.body;
- const keyCode = key.length === 1 ? key.charCodeAt(0) : 0;
- const keyEventOptions = {
- key: key, code: key.length === 1 ? `Key${key.toUpperCase()}` : key,
- keyCode: keyCode, which: keyCode, bubbles: true, cancelable: true, composed: true, view: window
- };
- try {
- targetElement.dispatchEvent(new KeyboardEvent('keydown', keyEventOptions));
- setTimeout(() => {
- targetElement.dispatchEvent(new KeyboardEvent('keyup', keyEventOptions));
- }, 10 + Math.random() * 20);
- } catch (e) {
- console.error(`[Xbox Macro] Error dispatching key event for "${key}":`, e);
- showNotification(`Error simulating key "${key}". Check console.`, 'error');
- }
- }
- function simulateMouseMovement(startX, startY, endX, endY, internalDelay) {
- const target = window;
- // console.log(`[XCloud Sim] Simulating mouse movement from (${startX}, ${startY}) to (${endX}, ${endY})`);
- target.dispatchEvent(new PointerEvent('pointermove', {
- bubbles: true, cancelable: true, view: window, clientX: startX, clientY: startY,
- movementX: 0, movementY: 0, pointerType: 'mouse', buttons: 0
- }));
- setTimeout(() => {
- target.dispatchEvent(new PointerEvent('pointermove', {
- bubbles: true, cancelable: true, view: window, clientX: endX, clientY: endY,
- movementX: endX - startX, movementY: endY - startY, pointerType: 'mouse', buttons: 0
- }));
- }, internalDelay);
- }
- function simulateMouseClick(x, y, buttonName = 'left', duration = 50) {
- const target = window;
- let buttonCode = 0, pointerButtonProperty = 0;
- switch (buttonName.toLowerCase()) {
- case 'left': buttonCode = 1; pointerButtonProperty = 0; break;
- case 'right': buttonCode = 2; pointerButtonProperty = 2; break;
- case 'middle': buttonCode = 4; pointerButtonProperty = 1; break;
- default: buttonCode = 1; pointerButtonProperty = 0; break;
- }
- target.dispatchEvent(new PointerEvent('pointerdown', {
- bubbles: true, cancelable: true, view: window, clientX: x, clientY: y,
- pointerType: 'mouse', button: pointerButtonProperty, buttons: buttonCode
- }));
- setTimeout(() => {
- target.dispatchEvent(new PointerEvent('pointerup', {
- bubbles: true, cancelable: true, view: window, clientX: x, clientY: y,
- pointerType: 'mouse', button: pointerButtonProperty, buttons: 0
- }));
- }, duration);
- }
- // Placeholder for gamepad simulation - Actual emulation is complex and depends on the target application
- // In a browser context, we can't *truly* inject gamepad input at the system level.
- // This would typically involve sending messages to the game stream element if it has an API,
- // or relying on the game's own input handling if it listens for events on the stream element.
- // For now, these functions are placeholders or could potentially dispatch custom events
- // if the target application is designed to listen for them.
- function simulateGamepadButtonPress(gamepadIndex, buttonIndex, duration = 50) {
- console.warn(`[Xbox Macro] Gamepad button simulation is a placeholder. Attempting to simulate button ${buttonIndex} on gamepad ${gamepadIndex}.`);
- // Potential implementation: Dispatch a custom event or find a way to interact with the game stream element's input handling.
- // Example (conceptual, may not work):
- // const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
- // if (gameStreamElement && gameStreamElement.dispatchEvent) {
- // gameStreamElement.dispatchEvent(new CustomEvent('gamepadbuttondown', { detail: { gamepadIndex, buttonIndex } }));
- // setTimeout(() => {
- // gameStreamElement.dispatchEvent(new CustomEvent('gamepadbuttonup', { detail: { gamepadIndex, buttonIndex } }));
- // }, duration);
- // }
- }
- function simulateGamepadAxisMove(gamepadIndex, axisIndex, value) {
- console.warn(`[Xbox Macro] Gamepad axis simulation is a placeholder. Attempting to simulate axis ${axisIndex} to value ${value} on gamepad ${gamepadIndex}.`);
- // Potential implementation: Dispatch a custom event or find a way to interact with the game stream element's input handling.
- // Example (conceptual, may not work):
- // const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
- // if (gameStreamElement && gameStreamElement.dispatchEvent) {
- // gameStreamElement.dispatchEvent(new CustomEvent('gamepadaxismove', { detail: { gamepadIndex, axisIndex, value } }));
- // }
- }
- // --- Utility Functions ---
- function loadMacros() {
- const saved = localStorage.getItem(CONFIG.STORAGE_KEY);
- if (!saved) return;
- try {
- const data = JSON.parse(saved);
- for (const triggerKey in data) {
- const macroData = data[triggerKey];
- // Basic validation for critical properties
- if (macroData && typeof macroData.type === 'string' && typeof macroData.params === 'object' && typeof macroData.mode === 'string') {
- state.macros[triggerKey] = new Macro(
- triggerKey, macroData.type, macroData.params, macroData.mode, macroData.description || ''
- );
- // Ensure createdAt is loaded for ID consistency
- if (macroData.createdAt) state.macros[triggerKey].createdAt = macroData.createdAt;
- state.macros[triggerKey].id = `macro-${triggerKey}-${state.macros[triggerKey].createdAt || Date.now()}`;
- } else {
- console.warn(`[Xbox Macro] Skipping invalid macro data for key: ${triggerKey}`, macroData);
- }
- }
- console.log(`[Xbox Macro] Loaded ${Object.keys(state.macros).length} macros`);
- } catch (e) {
- console.error('[Xbox Macro] Error loading macros from localStorage:', e);
- localStorage.removeItem(CONFIG.STORAGE_KEY); // Clear potentially corrupt data
- }
- }
- function saveMacros() {
- const serialized = {};
- for (const key in state.macros) {
- serialized[key] = state.macros[key].serialize();
- }
- localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(serialized));
- }
- function saveSettings() {
- const guiElement = document.getElementById('xboxMacroGUI');
- const settings = {
- isGuiVisible: state.isGuiVisible,
- isGuiLocked: state.isGuiLocked,
- guiPosition: guiElement ? {
- top: guiElement.style.top, left: guiElement.style.left,
- right: guiElement.style.right, bottom: guiElement.style.bottom
- } : CONFIG.GUI_POSITION,
- activeTab: state.activeTab,
- gamepadMappings: state.gamepadMappings // Save gamepad mappings
- };
- localStorage.setItem(CONFIG.SETTINGS_STORAGE_KEY, JSON.stringify(settings));
- }
- function loadSettings() {
- const saved = localStorage.getItem(CONFIG.SETTINGS_STORAGE_KEY);
- if (!saved) return;
- try {
- const settings = JSON.parse(saved);
- state.isGuiVisible = typeof settings.isGuiVisible === 'boolean' ? settings.isGuiVisible : true;
- state.isGuiLocked = typeof settings.isGuiLocked === 'boolean' ? settings.isGuiLocked : false;
- state.activeTab = settings.activeTab || 'create';
- state.gamepadMappings = settings.gamepadMappings || {}; // Load gamepad mappings
- const guiElement = document.getElementById('xboxMacroGUI');
- if (guiElement && settings.guiPosition) {
- // Apply position, prioritizing left/top if they exist
- guiElement.style.top = settings.guiPosition.top || '';
- guiElement.style.left = settings.guiPosition.left || '';
- guiElement.style.right = settings.guiPosition.right || '';
- guiElement.style.bottom = settings.guiPosition.bottom || '';
- // Fallback to default if no position is set
- if (!guiElement.style.top && !guiElement.style.left && !guiElement.style.right && !guiElement.style.bottom) {
- guiElement.style.top = CONFIG.GUI_POSITION.top;
- guiElement.style.right = CONFIG.GUI_POSITION.right;
- }
- }
- if (guiElement) { updateGuiVisibility(guiElement); updateGuiLock(guiElement); }
- switchTab(state.activeTab, false); // Don't save settings again immediately after loading
- } catch (e) { console.error('[Xbox Macro] Error loading settings:', e); }
- }
- function getInputValue(id, type = 'string', defaultValue = '') {
- const el = document.getElementById(id);
- if (!el) return defaultValue;
- let value = el.value.trim();
- if (type === 'number') {
- const num = parseFloat(value);
- return isNaN(num) ? (defaultValue !== '' ? parseFloat(defaultValue) : NaN) : num;
- }
- return value || defaultValue;
- }
- function parseKeyActionsInput(inputString) {
- const actions = [];
- if (!inputString) return actions;
- const pairs = inputString.split(',');
- for (const pair of pairs) {
- const parts = pair.trim().split(':');
- const key = parts[0] ? parts[0].trim() : null;
- if (!key) continue; // Skip if key is empty
- const delayAfter = parts[1] ? parseInt(parts[1].trim(), 10) : CONFIG.DEFAULT_ACTION_DELAY;
- if (isNaN(delayAfter) || delayAfter < 0) {
- showNotification(`Invalid delay for key "${key}". Using default ${CONFIG.DEFAULT_ACTION_DELAY}ms.`, 'error');
- actions.push({ key: key, delayAfter: CONFIG.DEFAULT_ACTION_DELAY });
- } else {
- actions.push({ key: key, delayAfter: delayAfter });
- }
- }
- return actions;
- }
- function createMacro() {
- const triggerKeyInput = getInputValue('macroTriggerKey');
- if (!triggerKeyInput) {
- showNotification('Error: Trigger key cannot be empty.', 'error');
- return false;
- }
- const triggerKey = triggerKeyInput.toLowerCase();
- // Prevent using 'gamepad' as a trigger key as it's reserved
- if (triggerKey === 'gamepad') {
- showNotification('Error: "gamepad" is a reserved trigger key. Please choose another.', 'error');
- return false;
- }
- const type = getInputValue('macroType');
- const mode = getInputValue('macroMode');
- const description = getInputValue('macroDescription');
- let params = {};
- let isValid = true;
- switch (type) {
- case 'keySequence':
- params.actions = parseKeyActionsInput(getInputValue('macroKeyActions'));
- params.repeatDelay = getInputValue('macroRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
- if (params.actions.length === 0) {
- showNotification('Error: Key actions cannot be empty for a sequence.', 'error');
- isValid = false;
- }
- break;
- case 'mouseMovement':
- params.startX = getInputValue('macroMouseStartX', 'number');
- params.startY = getInputValue('macroMouseStartY', 'number');
- params.endX = getInputValue('macroMouseEndX', 'number');
- params.endY = getInputValue('macroMouseEndY', 'number');
- params.movementInternalDelay = getInputValue('macroMouseMovementInternalDelay', 'number', CONFIG.DEFAULT_MOUSE_MOVEMENT_INTERNAL_DELAY);
- params.repeatDelay = getInputValue('macroMouseRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
- if ([params.startX, params.startY, params.endX, params.endY].some(isNaN)) {
- showNotification('Error: Mouse movement coordinates must be valid numbers.', 'error'); isValid = false;
- }
- break;
- case 'mouseClick':
- params.x = getInputValue('macroMouseClickX', 'number');
- params.y = getInputValue('macroMouseClickY', 'number');
- params.button = getInputValue('macroMouseButton');
- params.clickDuration = getInputValue('macroMouseClickDuration', 'number', CONFIG.DEFAULT_MOUSE_CLICK_DURATION);
- params.repeatDelay = getInputValue('macroMouseClickRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
- if (isNaN(params.x) || isNaN(params.y)) {
- showNotification('Error: Mouse click coordinates must be valid numbers.', 'error'); isValid = false;
- }
- break;
- // Add cases for new gamepad macro types if needed for creation form
- // case 'gamepadButtonPress':
- // params.buttonIndex = getInputValue('macroGamepadButtonIndex', 'number');
- // params.pressDuration = getInputValue('macroGamepadButtonDuration', 'number', 50);
- // params.repeatDelay = getInputValue('macroGamepadButtonRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
- // if (isNaN(params.buttonIndex)) { showNotification('Error: Gamepad button index must be a number.', 'error'); isValid = false; }
- // break;
- // case 'gamepadAxisMove':
- // params.axisIndex = getInputValue('macroGamepadAxisIndex', 'number');
- // params.axisValue = getInputValue('macroGamepadAxisValue', 'number'); // Value between -1 and 1
- // params.repeatDelay = getInputValue('macroGamepadAxisRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
- // if (isNaN(params.axisIndex) || isNaN(params.axisValue) || params.axisValue < -1 || params.axisValue > 1) {
- // showNotification('Error: Gamepad axis index and value (-1 to 1) must be valid numbers.', 'error'); isValid = false;
- // }
- // break;
- default: showNotification('Error: Invalid macro type selected.', 'error'); return false;
- }
- if (!isValid) return false;
- const isUpdate = triggerKey in state.macros;
- if (isUpdate && state.macros[triggerKey].isRunning) state.macros[triggerKey].stop();
- const verb = isUpdate ? 'Updated' : 'Created';
- state.macros[triggerKey] = new Macro(triggerKey, type, params, mode, description);
- saveMacros();
- refreshGUI();
- showNotification(`${verb} macro for key: ${triggerKey}`, 'success');
- document.getElementById('macroForm').reset();
- updateMacroTypeSpecificFields();
- document.getElementById('macroTriggerKey').focus(); // Focus trigger key for next macro
- return true;
- }
- function deleteMacro(triggerKey) {
- triggerKey = triggerKey.toLowerCase();
- if (state.macros[triggerKey]) {
- state.macros[triggerKey].stop();
- delete state.macros[triggerKey];
- // Also remove any gamepad mappings associated with this macro
- for (const gamepadInput in state.gamepadMappings) {
- if (state.gamepadMappings[gamepadInput] === triggerKey) {
- delete state.gamepadMappings[gamepadInput];
- }
- }
- saveMacros(); saveSettings(); // Save both macros and settings (for mappings)
- refreshGUI(); refreshGamepadMappingUI(); // Refresh both GUIs
- showNotification(`Deleted macro for key: ${triggerKey}`, 'info');
- }
- }
- function deleteAllMacros() {
- if (confirm('Are you sure you want to delete ALL macros? This cannot be undone.')) {
- stopAllMacros();
- state.macros = {};
- state.gamepadMappings = {}; // Also clear mappings
- saveMacros();
- saveSettings(); // Save both
- refreshGUI();
- refreshGamepadMappingUI(); // Refresh both
- showNotification('All macros and gamepad mappings have been deleted.', 'info');
- }
- }
- function stopAllMacros() {
- Object.values(state.macros).forEach(macro => macro.stop());
- showNotification('All macros stopped', 'info');
- refreshGUI();
- }
- function exportMacros() {
- const serialized = {};
- for (const key in state.macros) serialized[key] = state.macros[key].serialize();
- const dataStr = JSON.stringify(serialized, null, 2);
- const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
- const exportName = `xbox-cloud-macros-v${CONFIG.VERSION}-${new Date().toISOString().slice(0, 10)}.json`;
- const linkElement = document.createElement('a');
- linkElement.setAttribute('href', dataUri);
- linkElement.setAttribute('download', exportName);
- linkElement.click();
- showNotification('Macros exported successfully', 'success');
- }
- function importMacros() {
- const input = document.createElement('input');
- input.type = 'file'; input.accept = 'application/json';
- input.onchange = e => {
- const file = e.target.files[0]; if (!file) return;
- const reader = new FileReader();
- reader.onload = event => {
- try {
- const importedData = JSON.parse(event.target.result);
- let importCount = 0, overwriteCount = 0;
- for (const key in importedData) {
- const macroData = importedData[key];
- if (typeof macroData === 'object' && macroData.type && macroData.params && macroData.mode) {
- const lowerKey = key.toLowerCase();
- if (state.macros[lowerKey]) {
- overwriteCount++;
- if (state.macros[lowerKey].isRunning) state.macros[lowerKey].stop();
- }
- state.macros[lowerKey] = new Macro(
- lowerKey, macroData.type, macroData.params, macroData.mode, macroData.description || ''
- );
- if (macroData.createdAt) state.macros[lowerKey].createdAt = macroData.createdAt;
- state.macros[lowerKey].id = `macro-${lowerKey}-${state.macros[lowerKey].createdAt || Date.now()}`;
- importCount++;
- }
- }
- saveMacros(); refreshGUI();
- showNotification(`Imported ${importCount} macros. ${overwriteCount} existing macros overwritten.`, 'success');
- } catch (err) {
- console.error('[Xbox Macro] Import error:', err);
- showNotification('Error importing macros: Invalid file format or content.', 'error');
- }
- };
- reader.readAsText(file);
- };
- input.click();
- }
- function showNotification(message, type = 'info') {
- let notificationArea = document.getElementById('xboxMacroNotificationArea');
- if (!notificationArea) {
- notificationArea = document.createElement('div');
- notificationArea.id = 'xboxMacroNotificationArea';
- document.body.appendChild(notificationArea);
- }
- const notification = document.createElement('div');
- notification.className = `xbox-macro-notification ${type}`;
- notification.textContent = message;
- notificationArea.prepend(notification); // Add to the top
- setTimeout(() => { notification.style.transform = 'translateX(0)'; notification.style.opacity = '1'; }, 10); // Animate in
- setTimeout(() => {
- notification.style.transform = 'translateX(120%)'; notification.style.opacity = '0'; // Animate out
- setTimeout(() => notification.remove(), 500); // Remove after animation
- }, CONFIG.NOTIFICATION_DURATION + message.length * 10); // Adjust duration based on message length
- }
- function updateMacroStatus(triggerKey, isRunning, runCount = 0) {
- const macro = state.macros[triggerKey]; if (!macro) return;
- const statusEl = document.querySelector(`.macro-item[data-trigger="${triggerKey}"] .macro-status`);
- if (statusEl) {
- statusEl.className = `macro-status ${isRunning ? 'active' : 'inactive'}`;
- statusEl.textContent = isRunning ? `● Running (Count: ${runCount})` : '○ Stopped';
- }
- }
- function formatActionsForEdit(actions) {
- if (!actions || !Array.isArray(actions)) return '';
- return actions.map(action => `${action.key}:${action.delayAfter}`).join(', ');
- }
- function editMacro(triggerKey) {
- const macro = state.macros[triggerKey]; if (!macro) return;
- switchTab('create');
- document.getElementById('macroTriggerKey').value = macro.triggerKey;
- document.getElementById('macroType').value = macro.type;
- updateMacroTypeSpecificFields(); // Update visibility first
- document.getElementById('macroMode').value = macro.mode;
- document.getElementById('macroDescription').value = macro.description;
- switch (macro.type) {
- case 'keySequence':
- document.getElementById('macroKeyActions').value = formatActionsForEdit(macro.params.actions);
- document.getElementById('macroRepeatDelay').value = macro.params.repeatDelay;
- break;
- case 'mouseMovement':
- document.getElementById('macroMouseStartX').value = macro.params.startX;
- document.getElementById('macroMouseStartY').value = macro.params.startY;
- document.getElementById('macroMouseEndX').value = macro.params.endX;
- document.getElementById('macroMouseEndY').value = macro.params.endY;
- document.getElementById('macroMouseMovementInternalDelay').value = macro.params.movementInternalDelay;
- document.getElementById('macroMouseRepeatDelay').value = macro.params.repeatDelay;
- break;
- case 'mouseClick':
- document.getElementById('macroMouseClickX').value = macro.params.x;
- document.getElementById('macroMouseClickY').value = macro.params.y;
- document.getElementById('macroMouseButton').value = macro.params.button;
- document.getElementById('macroMouseClickDuration').value = macro.params.clickDuration;
- document.getElementById('macroMouseClickRepeatDelay').value = macro.params.repeatDelay;
- break;
- // Add cases for new gamepad macro types if implementing creation form fields
- // case 'gamepadButtonPress':
- // document.getElementById('macroGamepadButtonIndex').value = macro.params.buttonIndex;
- // document.getElementById('macroGamepadButtonDuration').value = macro.params.pressDuration;
- // document.getElementById('macroGamepadButtonRepeatDelay').value = macro.params.repeatDelay;
- // break;
- // case 'gamepadAxisMove':
- // document.getElementById('macroGamepadAxisIndex').value = macro.params.axisIndex;
- // document.getElementById('macroGamepadAxisValue').value = macro.params.axisValue;
- // document.getElementById('macroGamepadAxisRepeatDelay').value = macro.params.repeatDelay;
- // break;
- }
- document.getElementById('addMacroBtn').textContent = 'Update Macro';
- }
- // --- GUI Management ---
- function toggleGuiVisibility(guiElement) {
- state.isGuiVisible = !state.isGuiVisible;
- updateGuiVisibility(guiElement);
- saveSettings();
- }
- function updateGuiVisibility(guiElement) {
- if (!guiElement) guiElement = document.getElementById('xboxMacroGUI');
- if (guiElement) guiElement.style.display = state.isGuiVisible ? 'flex' : 'none';
- const minimizeBtn = document.getElementById('minimizeGuiBtn');
- if (minimizeBtn && guiElement) { // Ensure guiElement is defined
- const content = guiElement.querySelector('.gui-content');
- if (content) { // Ensure content is defined
- // If the main GUI is hidden, content should also be considered hidden for minimize button state
- const isEffectivelyVisible = state.isGuiVisible && content.style.display !== 'none';
- minimizeBtn.textContent = isEffectivelyVisible ? '▼' : '▲';
- minimizeBtn.title = isEffectivelyVisible ? 'Minimize panel content' : 'Expand panel content';
- }
- }
- }
- function minimizePanelContent(guiElement) {
- const content = guiElement.querySelector('.gui-content');
- const minimizeBtn = document.getElementById('minimizeGuiBtn');
- if (content && minimizeBtn) {
- const isContentVisible = content.style.display !== 'none';
- content.style.display = isContentVisible ? 'none' : 'block';
- minimizeBtn.textContent = isContentVisible ? '▲' : '▼';
- minimizeBtn.title = isContentVisible ? 'Expand panel content' : 'Minimize panel content';
- // This minimized state is not saved in global settings, only overall panel visibility
- }
- }
- function toggleGuiLock(guiElement) {
- state.isGuiLocked = !state.isGuiLocked;
- updateGuiLock(guiElement);
- saveSettings();
- }
- function updateGuiLock(guiElement) {
- if (!guiElement) guiElement = document.getElementById('xboxMacroGUI');
- const lockButton = document.getElementById('lockGuiBtn');
- if (lockButton && guiElement) {
- lockButton.textContent = state.isGuiLocked ? '🔒' : '🔓';
- lockButton.title = state.isGuiLocked ? 'Unlock panel position' : 'Lock panel position';
- guiElement.classList.toggle('locked', state.isGuiLocked);
- }
- }
- function updateMacroTypeSpecificFields() {
- const type = document.getElementById('macroType').value;
- // Hide all type-specific fields first
- document.getElementById('keySequenceFields').style.display = 'none';
- document.getElementById('mouseMovementFields').style.display = 'none';
- document.getElementById('mouseClickFields').style.display = 'none';
- // document.getElementById('gamepadButtonPressFields').style.display = 'none'; // Uncomment if adding form fields
- // document.getElementById('gamepadAxisMoveFields').style.display = 'none'; // Uncomment if adding form fields
- // Show fields for the selected type
- const fieldsToShow = {
- 'keySequence': 'keySequenceFields',
- 'mouseMovement': 'mouseMovementFields',
- 'mouseClick': 'mouseClickFields',
- // 'gamepadButtonPress': 'gamepadButtonPressFields', // Uncomment if adding form fields
- // 'gamepadAxisMove': 'gamepadAxisMoveFields' // Uncomment if adding form fields
- };
- const targetFieldsId = fieldsToShow[type];
- if (targetFieldsId) {
- const targetElement = document.getElementById(targetFieldsId);
- if (targetElement) {
- targetElement.style.display = 'block';
- }
- }
- // Reset or set default values for relevant repeat delay fields when type changes
- const repeatDelayFields = {
- keySequence: 'macroRepeatDelay',
- mouseMovement: 'macroMouseRepeatDelay',
- mouseClick: 'macroMouseClickRepeatDelay',
- // gamepadButtonPress: 'macroGamepadButtonRepeatDelay', // Uncomment if adding form fields
- // gamepadAxisMove: 'macroGamepadAxisRepeatDelay' // Uncomment if adding form fields
- };
- for (const fieldType in repeatDelayFields) {
- const inputEl = document.getElementById(repeatDelayFields[fieldType]);
- if (inputEl) { // Ensure element exists
- if (type === fieldType && !inputEl.value) { // If it's the current type and empty, set default
- inputEl.value = CONFIG.DEFAULT_REPEAT_DELAY;
- }
- }
- }
- }
- function switchTab(tabId, save = true) {
- document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
- const newTabContent = document.getElementById(tabId + 'Tab');
- const newTab = document.querySelector(`.tab[data-tab="${tabId}"]`);
- if (newTabContent) newTabContent.classList.add('active');
- if (newTab) newTab.classList.add('active');
- state.activeTab = tabId;
- if (save) saveSettings();
- // Special handling for the gamepad tab
- if (tabId === 'gamepad') {
- initializeGamepadPolling();
- refreshGamepadMappingUI();
- } else {
- stopGamepadPolling();
- }
- }
- function refreshGUI() {
- const macroListElement = document.getElementById('macroList');
- if (!macroListElement) return;
- macroListElement.innerHTML = ''; // Clear current list
- // Sort macros by creation date, newest first
- const sortedMacros = Object.values(state.macros).sort((a, b) => b.createdAt - a.createdAt);
- if (sortedMacros.length === 0) {
- macroListElement.innerHTML = '<p class="no-macros">No macros created yet.</p>';
- return;
- }
- sortedMacros.forEach(macro => {
- const macroItem = document.createElement('div');
- macroItem.className = 'macro-item';
- macroItem.dataset.trigger = macro.triggerKey;
- macroItem.id = macro.id; // Set the unique ID
- const statusClass = macro.isRunning ? 'active' : 'inactive';
- const statusText = macro.isRunning ? `● Running (Count: ${macro.runCount})` : '○ Stopped';
- macroItem.innerHTML = `
- <div class="macro-header">
- <span class="macro-trigger">${macro.triggerKey.toUpperCase()}</span>
- <span class="macro-type">(${macro.type.replace(/([A-Z])/g, ' $1').trim()})</span>
- <span class="macro-mode">[${macro.mode}]</span>
- <span class="macro-status ${statusClass}">${statusText}</span>
- </div>
- <div class="macro-details">
- <p>${macro.description || 'No description'}</p>
- <p>Repeat Delay: ${macro.type === 'keySequence' ? (macro.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY) : (macro.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY)}ms</p>
- ${macro.type === 'keySequence' ? `<p>Actions: ${formatActionsForEdit(macro.params.actions)}</p>` : ''}
- ${macro.type === 'mouseMovement' ? `<p>Move from (${macro.params.startX}, ${macro.params.startY}) to (${macro.params.endX}, ${macro.params.endY})</p>` : ''}
- ${macro.type === 'mouseClick' ? `<p>Click ${macro.params.button} at (${macro.params.x}, ${macro.params.y})</p>` : ''}
- </div>
- <div class="macro-actions">
- <button class="edit-macro-btn" data-trigger="${macro.triggerKey}" title="Edit Macro">✏️</button>
- <button class="delete-macro-btn" data-trigger="${macro.triggerKey}" title="Delete Macro">🗑️</button>
- </div>
- `;
- macroListElement.appendChild(macroItem);
- });
- // Add event listeners for edit and delete buttons
- macroListElement.querySelectorAll('.edit-macro-btn').forEach(button => {
- button.addEventListener('click', (e) => {
- e.stopPropagation(); // Prevent triggering macro if it's bound to the same key
- editMacro(e.target.dataset.trigger);
- });
- });
- macroListElement.querySelectorAll('.delete-macro-btn').forEach(button => {
- button.addEventListener('click', (e) => {
- e.stopPropagation(); // Prevent triggering macro
- deleteMacro(e.target.dataset.trigger);
- });
- });
- }
- // --- Gamepad Handling ---
- function initializeGamepadAPI() {
- if (!navigator.getGamepads) {
- console.warn("[Xbox Macro] Gamepad API not supported in this browser.");
- const gamepadTab = document.querySelector('.tab[data-tab="gamepad"]');
- if (gamepadTab) gamepadTab.style.display = 'none'; // Hide gamepad tab if not supported
- const gamepadTabContent = document.getElementById('gamepadTab');
- if (gamepadTabContent) gamepadTabContent.innerHTML = '<p>Gamepad API not supported in this browser.</p>';
- return;
- }
- window.addEventListener("gamepadconnected", (e) => {
- console.log("[Xbox Macro] Gamepad connected:", e.gamepad);
- // Add the gamepad to our state, ensuring no duplicates based on index
- state.gamepads[e.gamepad.index] = e.gamepad;
- showNotification(`Gamepad ${e.gamepad.index + 1} connected: ${e.gamepad.id}`, 'info');
- refreshGamepadList();
- // If this is the first gamepad or the only one, select it
- if (state.gamepads.filter(g => g !== null).length === 1) {
- selectGamepad(e.gamepad.index);
- }
- if (state.activeTab === 'gamepad') {
- initializeGamepadPolling(); // Start polling if on gamepad tab
- }
- });
- window.addEventListener("gamepaddisconnected", (e) => {
- console.log("[Xbox Macro] Gamepad disconnected:", e.gamepad);
- // Remove the gamepad from our state
- if (state.gamepads[e.gamepad.index]) {
- state.gamepads[e.gamepad.index] = null;
- showNotification(`Gamepad ${e.gamepad.index + 1} disconnected: ${e.gamepad.id}`, 'info');
- refreshGamepadList();
- // If the disconnected gamepad was the active one, try to select another
- if (state.activeGamepadIndex === e.gamepad.index) {
- const remainingGamepads = state.gamepads.filter(g => g !== null);
- if (remainingGamepads.length > 0) {
- selectGamepad(remainingGamepads[0].index);
- } else {
- selectGamepad(0); // Select a non-existent index to indicate no gamepad selected
- }
- }
- }
- if (state.activeTab === 'gamepad' && state.gamepads.filter(g => g !== null).length === 0) {
- stopGamepadPolling(); // Stop polling if no gamepads remain and on gamepad tab
- }
- });
- // Check for already connected gamepads on load
- const gamepads = navigator.getGamepads();
- for (let i = 0; i < gamepads.length; i++) {
- if (gamepads[i]) {
- state.gamepads[gamepads[i].index] = gamepads[i];
- console.log("[Xbox Macro] Found already connected gamepad:", gamepads[i]);
- }
- }
- refreshGamepadList();
- // Select the first available gamepad if any exist
- const firstGamepad = state.gamepads.find(g => g !== null);
- if (firstGamepad) {
- selectGamepad(firstGamepad.index);
- } else {
- selectGamepad(0); // Select a non-existent index if none found
- }
- }
- function refreshGamepadList() {
- const gamepadSelect = document.getElementById('gamepadSelect');
- if (!gamepadSelect) return;
- gamepadSelect.innerHTML = ''; // Clear existing options
- const availableGamepads = state.gamepads.filter(g => g !== null);
- if (availableGamepads.length === 0) {
- const option = document.createElement('option');
- option.value = '-1'; // Use -1 to indicate no gamepad selected
- option.textContent = 'No gamepads connected';
- gamepadSelect.appendChild(option);
- selectGamepad(-1); // Ensure state reflects no gamepad selected
- return;
- }
- availableGamepads.forEach(gamepad => {
- const option = document.createElement('option');
- option.value = gamepad.index;
- option.textContent = `Gamepad ${gamepad.index + 1}: ${gamepad.id}`;
- gamepadSelect.appendChild(option);
- });
- // Select the currently active gamepad in the dropdown
- gamepadSelect.value = state.activeGamepadIndex;
- }
- function selectGamepad(index) {
- state.activeGamepadIndex = parseInt(index, 10);
- console.log(`[Xbox Macro] Selected gamepad index: ${state.activeGamepadIndex}`);
- refreshGamepadMappingUI(); // Refresh the mapping UI for the newly selected gamepad
- }
- let previousGamepadState = {}; // Store the state of buttons and axes from the previous frame
- function gamepadPollingLoop() {
- // Request the latest gamepad state
- const gamepads = navigator.getGamepads();
- const activeGamepad = gamepads[state.activeGamepadIndex];
- if (!activeGamepad) {
- // If the active gamepad is no longer available, stop polling or switch
- console.warn(`[Xbox Macro] Active gamepad ${state.activeGamepadIndex} not found during polling.`);
- stopGamepadPolling();
- refreshGamepadList(); // Update the list to reflect disconnected gamepad
- return;
- }
- // Get the SVG controller elements
- const svgController = document.getElementById('xboxControllerSVG');
- if (!svgController) {
- // If the SVG isn't rendered (e.g., not on the gamepad tab), stop polling
- stopGamepadPolling();
- return;
- }
- // Process Buttons
- activeGamepad.buttons.forEach((button, index) => {
- const buttonElement = svgController.querySelector(`.button[data-button-index="${index}"]`);
- const isPressed = button.pressed;
- const wasPressed = previousGamepadState.buttons ? previousGamepadState.buttons[index]?.pressed : false;
- if (buttonElement) {
- // Visual feedback: Add/remove an 'active' class based on pressed state
- buttonElement.classList.toggle('active', isPressed);
- }
- // Check for button press (transition from not pressed to pressed)
- if (isPressed && !wasPressed) {
- console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Button ${index} Pressed`);
- // Trigger macro if mapped
- const mappingKey = `button-${index}`;
- if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]]) {
- const macro = state.macros[state.gamepadMappings[mappingKey]];
- if (macro.mode === 'toggle') macro.toggle();
- else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
- }
- // Update mapping UI if currently mapping a button
- if (state.isMapping && state.mappingType === 'button') {
- setMappingInput(`button-${index}`);
- }
- }
- // Check for button release (transition from pressed to not pressed)
- else if (!isPressed && wasPressed) {
- console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Button ${index} Released`);
- // Stop 'hold' macro if mapped
- const mappingKey = `button-${index}`;
- if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]] && state.macros[state.gamepadMappings[mappingKey]].mode === 'hold') {
- state.macros[state.gamepadMappings[mappingKey]].stop();
- }
- }
- });
- // Process Axes
- activeGamepad.axes.forEach((axisValue, index) => {
- // Axes have a range typically from -1 to 1
- // We might want to trigger events based on a threshold
- const threshold = 0.5; // Define a threshold for considering an axis "active"
- const previousAxisValue = previousGamepadState.axes ? previousGamepadState.axes[index] : 0;
- // Visual feedback for analog sticks/triggers
- const axisElement = svgController.querySelector(`.axis[data-axis-index="${index}"]`);
- if (axisElement) {
- // Simple visual: change color based on absolute value
- const intensity = Math.abs(axisValue);
- axisElement.style.fill = `rgba(0, 255, 0, ${intensity})`; // Green based on intensity
- }
- // Check for positive axis movement (e.g., right stick right, right trigger pull)
- if (axisValue > threshold && previousAxisValue <= threshold) {
- console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Axis ${index} Positive (> ${threshold})`);
- // Trigger macro if mapped to positive direction
- const mappingKey = `axis-${index}-pos`;
- if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]]) {
- const macro = state.macros[state.gamepadMappings[mappingKey]];
- if (macro.mode === 'toggle') macro.toggle();
- else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
- }
- // Update mapping UI if currently mapping an axis
- if (state.isMapping && state.mappingType === 'axis') {
- setMappingInput(`axis-${index}-pos`);
- }
- }
- // Check for negative axis movement (e.g., right stick left, left trigger pull)
- else if (axisValue < -threshold && previousAxisValue >= -threshold) {
- console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Axis ${index} Negative (< ${-threshold})`);
- // Trigger macro if mapped to negative direction
- const mappingKey = `axis-${index}-neg`;
- if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]]) {
- const macro = state.macros[state.gamepadMappings[mappingKey]];
- if (macro.mode === 'toggle') macro.toggle();
- else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
- }
- // Update mapping UI if currently mapping an axis
- if (state.isMapping && state.mappingType === 'axis') {
- setMappingInput(`axis-${index}-neg`);
- }
- }
- // Check for axis returning to center (for 'hold' macros)
- if (Math.abs(axisValue) <= threshold && Math.abs(previousAxisValue) > threshold) {
- console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Axis ${index} Centered`);
- // Stop 'hold' macros mapped to either direction
- const mappingKeyPos = `axis-${index}-pos`;
- if (state.gamepadMappings[mappingKeyPos] && state.macros[state.gamepadMappings[mappingKeyPos]] && state.macros[state.gamepadMappings[mappingKeyPos]].mode === 'hold') {
- state.macros[state.gamepadMappings[mappingKeyPos]].stop();
- }
- const mappingKeyNeg = `axis-${index}-neg`;
- if (state.gamepadMappings[mappingKeyNeg] && state.macros[state.gamepadMappings[mappingKeyNeg]] && state.macros[state.gamepadMappings[mappingKeyNeg]].mode === 'hold') {
- state.macros[state.gamepadMappings[mappingKeyNeg]].stop();
- }
- }
- });
- // Store current state for the next frame
- previousGamepadState = {
- buttons: activeGamepad.buttons.map(b => ({ pressed: b.pressed, value: b.value })),
- axes: [...activeGamepad.axes] // Create a copy
- };
- // Continue the loop
- state.gamepadPollingInterval = requestAnimationFrame(gamepadPollingLoop);
- }
- function initializeGamepadPolling() {
- if (state.gamepadPollingInterval === null && state.gamepads.filter(g => g !== null).length > 0) {
- console.log("[Xbox Macro] Starting gamepad polling.");
- // Reset previous state when starting polling
- previousGamepadState = {};
- gamepadPollingLoop(); // Start the animation frame loop
- }
- }
- function stopGamepadPolling() {
- if (state.gamepadPollingInterval !== null) {
- console.log("[Xbox Macro] Stopping gamepad polling.");
- cancelAnimationFrame(state.gamepadPollingInterval);
- state.gamepadPollingInterval = null;
- // Clear visual feedback on SVG when polling stops
- const svgController = document.getElementById('xboxControllerSVG');
- if (svgController) {
- svgController.querySelectorAll('.button.active').forEach(el => el.classList.remove('active'));
- svgController.querySelectorAll('.axis').forEach(el => el.style.fill = 'transparent'); // Or default color
- }
- }
- }
- function refreshGamepadMappingUI() {
- const mappingListElement = document.getElementById('gamepadMappingList');
- const gamepadInfoElement = document.getElementById('selectedGamepadInfo');
- const svgContainer = document.getElementById('xboxControllerSVGContainer');
- const mapButtonPrompt = document.getElementById('mapButtonPrompt');
- if (!mappingListElement || !gamepadInfoElement || !svgContainer || !mapButtonPrompt) return;
- const activeGamepad = state.gamepads[state.activeGamepadIndex];
- if (!activeGamepad) {
- gamepadInfoElement.textContent = 'No gamepad selected or connected.';
- mappingListElement.innerHTML = '<p>Connect a gamepad and select it above to set up mappings.</p>';
- svgContainer.style.display = 'none';
- mapButtonPrompt.style.display = 'none';
- return;
- }
- svgContainer.style.display = 'block';
- mapButtonPrompt.style.display = 'block';
- gamepadInfoElement.textContent = `Selected: Gamepad ${activeGamepad.index + 1} - ${activeGamepad.id}`;
- mappingListElement.innerHTML = ''; // Clear current mappings
- // Add a button to start mapping a new input
- const addMappingBtn = document.createElement('button');
- addMappingBtn.textContent = 'Map New Gamepad Input';
- addMappingBtn.className = 'add-mapping-btn';
- addMappingBtn.addEventListener('click', startMapping);
- mappingListElement.appendChild(addMappingBtn);
- // Display current mappings
- const currentMappingsHeader = document.createElement('h4');
- currentMappingsHeader.textContent = 'Current Mappings:';
- mappingListElement.appendChild(currentMappingsHeader);
- const mappingsExist = Object.keys(state.gamepadMappings).length > 0;
- if (!mappingsExist) {
- const noMappings = document.createElement('p');
- noMappings.textContent = 'No mappings set yet.';
- mappingListElement.appendChild(noMappings);
- } else {
- const mappingList = document.createElement('ul');
- mappingList.className = 'current-mappings-list';
- for (const gamepadInput in state.gamepadMappings) {
- const macroTriggerKey = state.gamepadMappings[gamepadInput];
- const macro = state.macros[macroTriggerKey];
- if (macro) {
- const listItem = document.createElement('li');
- listItem.className = 'mapping-item';
- listItem.innerHTML = `
- <span class="gamepad-input">${formatGamepadInput(gamepadInput)}</span>
- <span> maps to </span>
- <span class="macro-trigger-key">${macroTriggerKey.toUpperCase()} (${macro.type.replace(/([A-Z])/g, ' $1').trim()})</span>
- <button class="remove-mapping-btn" data-input="${gamepadInput}" title="Remove Mapping">❌</button>
- `;
- mappingList.appendChild(listItem);
- } else {
- // Clean up orphaned mappings if the macro no longer exists
- delete state.gamepadMappings[gamepadInput];
- saveSettings(); // Save updated mappings
- refreshGamepadMappingUI(); // Refresh UI after cleanup
- return; // Exit to prevent further processing of stale data
- }
- }
- mappingListElement.appendChild(mappingList);
- // Add event listeners for remove buttons
- mappingListElement.querySelectorAll('.remove-mapping-btn').forEach(button => {
- button.addEventListener('click', (e) => {
- const inputToRemove = e.target.dataset.input;
- removeMapping(inputToRemove);
- });
- });
- }
- // Update the macro dropdown for mapping selection
- const macroSelect = document.getElementById('macroToMapSelect');
- if (macroSelect) {
- macroSelect.innerHTML = '<option value="">-- Select Macro --</option>';
- Object.values(state.macros).sort((a, b) => a.triggerKey.localeCompare(b.triggerKey)).forEach(macro => {
- const option = document.createElement('option');
- option.value = macro.triggerKey;
- option.textContent = `${macro.triggerKey.toUpperCase()} (${macro.type.replace(/([A-Z])/g, ' $1').trim()})`;
- macroSelect.appendChild(option);
- });
- }
- }
- function formatGamepadInput(inputKey) {
- const parts = inputKey.split('-');
- if (parts[0] === 'button') {
- return `Button ${parts[1]}`;
- } else if (parts[0] === 'axis') {
- const direction = parts[2] === 'pos' ? 'Positive' : 'Negative';
- return `Axis ${parts[1]} (${direction})`;
- }
- return inputKey; // Fallback
- }
- // Mapping State
- state.isMapping = false;
- state.mappingInputKey = null; // e.g., 'button-0', 'axis-1-pos'
- state.mappingType = null; // 'button' or 'axis'
- function startMapping() {
- if (state.isMapping) {
- showNotification('Already in mapping mode. Press a gamepad input or Cancel.', 'warning');
- return;
- }
- const macroToMapSelect = document.getElementById('macroToMapSelect');
- const selectedMacroTrigger = macroToMapSelect ? macroToMapSelect.value : '';
- if (!selectedMacroTrigger) {
- showNotification('Please select a macro to map.', 'error');
- return;
- }
- state.isMapping = true;
- state.mappingInputKey = null; // Reset input key
- state.mappingType = null; // Reset type
- const mapButtonPrompt = document.getElementById('mapButtonPrompt');
- if (mapButtonPrompt) {
- mapButtonPrompt.innerHTML = `
- <p>Press a button or move an axis on the gamepad...</p>
- <button id="cancelMappingBtn">Cancel</button>
- `;
- document.getElementById('cancelMappingBtn').addEventListener('click', cancelMapping);
- }
- showNotification('Mapping mode started. Press a gamepad input.', 'info');
- }
- function setMappingInput(inputKey) {
- if (!state.isMapping) return;
- state.mappingInputKey = inputKey;
- state.mappingType = inputKey.startsWith('button') ? 'button' : 'axis';
- const mapButtonPrompt = document.getElementById('mapButtonPrompt');
- if (mapButtonPrompt) {
- mapButtonPrompt.innerHTML = `
- <p>Mapping <span class="gamepad-input">${formatGamepadInput(inputKey)}</span> to:</p>
- <select id="macroToMapSelect"></select>
- <button id="confirmMappingBtn">Confirm Mapping</button>
- <button id="cancelMappingBtn">Cancel</button>
- `;
- // Re-populate the macro select dropdown
- const macroSelect = document.getElementById('macroToMapSelect');
- if (macroSelect) {
- macroSelect.innerHTML = '<option value="">-- Select Macro --</option>';
- Object.values(state.macros).sort((a, b) => a.triggerKey.localeCompare(b.triggerKey)).forEach(macro => {
- const option = document.createElement('option');
- option.value = macro.triggerKey;
- option.textContent = `${macro.triggerKey.toUpperCase()} (${macro.type.replace(/([A-Z])/g, ' $1').trim()})`;
- macroSelect.appendChild(option);
- });
- // Pre-select the currently mapped macro if it exists
- if (state.gamepadMappings[inputKey]) {
- macroSelect.value = state.gamepadMappings[inputKey];
- }
- }
- document.getElementById('confirmMappingBtn').addEventListener('click', confirmMapping);
- document.getElementById('cancelMappingBtn').addEventListener('click', cancelMapping);
- }
- showNotification(`Gamepad input detected: ${formatGamepadInput(inputKey)}. Select a macro to map.`, 'info');
- }
- function confirmMapping() {
- if (!state.isMapping || !state.mappingInputKey) {
- showNotification('Mapping process not active.', 'error');
- return;
- }
- const macroToMapSelect = document.getElementById('macroToMapSelect');
- const selectedMacroTrigger = macroToMapSelect ? macroToMapSelect.value : '';
- if (!selectedMacroTrigger) {
- showNotification('Please select a macro to map.', 'error');
- return;
- }
- // Check if this macro is already mapped to another gamepad input
- const existingInput = Object.keys(state.gamepadMappings).find(input => state.gamepadMappings[input] === selectedMacroTrigger);
- if (existingInput && existingInput !== state.mappingInputKey) {
- if (!confirm(`Macro "${selectedMacroTrigger.toUpperCase()}" is already mapped to ${formatGamepadInput(existingInput)}. Do you want to overwrite?`)) {
- cancelMapping();
- return;
- }
- // Remove the old mapping
- delete state.gamepadMappings[existingInput];
- }
- state.gamepadMappings[state.mappingInputKey] = selectedMacroTrigger;
- saveSettings();
- showNotification(`Mapped ${formatGamepadInput(state.mappingInputKey)} to macro "${selectedMacroTrigger.toUpperCase()}".`, 'success');
- cancelMapping(); // Exit mapping mode
- refreshGamepadMappingUI(); // Update the displayed mappings
- }
- function cancelMapping() {
- state.isMapping = false;
- state.mappingInputKey = null;
- state.mappingType = null;
- const mapButtonPrompt = document.getElementById('mapButtonPrompt');
- if (mapButtonPrompt) {
- mapButtonPrompt.innerHTML = '<p>Press "Map New Gamepad Input" to start mapping.</p>';
- }
- showNotification('Mapping cancelled.', 'info');
- refreshGamepadMappingUI(); // Refresh to show the "Map New Input" button again
- }
- function removeMapping(inputKey) {
- if (state.gamepadMappings[inputKey]) {
- const macroTrigger = state.gamepadMappings[inputKey];
- delete state.gamepadMappings[inputKey];
- saveSettings();
- showNotification(`Removed mapping for ${formatGamepadInput(inputKey)} (was mapped to "${macroTrigger.toUpperCase()}").`, 'info');
- refreshGamepadMappingUI();
- }
- }
- // --- Initial Setup ---
- function createGUI() {
- if (document.getElementById('xboxMacroGUI')) {
- console.warn("[Xbox Macro] GUI element already exists.");
- return;
- }
- const guiElement = document.createElement('div');
- guiElement.id = 'xboxMacroGUI';
- guiElement.className = 'xbox-macro-gui';
- guiElement.style.position = 'fixed';
- guiElement.style.zIndex = '9999'; // Ensure it's on top
- guiElement.style.display = 'flex'; // Use flexbox for layout
- guiElement.style.flexDirection = 'column';
- guiElement.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'; // Darker, slightly transparent
- guiElement.style.color = '#eee';
- guiElement.style.fontFamily = 'sans-serif';
- guiElement.style.fontSize = '14px';
- guiElement.style.borderRadius = '8px';
- guiElement.style.overflow = 'hidden'; // Hide overflow for rounded corners
- guiElement.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
- guiElement.style.width = '350px'; // Fixed width for now
- guiElement.style.maxHeight = '90vh'; // Max height to prevent overflow
- // Apply initial position from config (will be overwritten by loadSettings)
- guiElement.style.top = CONFIG.GUI_POSITION.top;
- guiElement.style.right = CONFIG.GUI_POSITION.right;
- guiElement.innerHTML = `
- <style>
- .xbox-macro-gui {
- /* Styles defined above */
- transition: all 0.2s ease-in-out; /* Smooth transitions for position/size */
- }
- .xbox-macro-gui.dragging {
- cursor: grabbing !important;
- opacity: 0.9;
- }
- .xbox-macro-gui.locked .gui-header {
- cursor: default !important; /* No drag cursor when locked */
- }
- .gui-header {
- background-color: rgba(50, 50, 50, 0.95);
- padding: 10px;
- cursor: grab; /* Indicate draggable */
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #444;
- }
- .gui-header h3 {
- margin: 0;
- font-size: 16px;
- color: #fff;
- }
- .header-buttons button {
- background: none;
- border: none;
- color: #eee;
- font-size: 18px;
- cursor: pointer;
- margin-left: 5px;
- padding: 2px;
- transition: color 0.2s ease;
- }
- .header-buttons button:hover {
- color: #fff;
- }
- .gui-content {
- padding: 15px;
- flex-grow: 1; /* Allow content to fill space */
- overflow-y: auto; /* Enable scrolling for content */
- }
- .tabs {
- display: flex;
- margin-bottom: 15px;
- border-bottom: 1px solid #444;
- }
- .tab {
- padding: 8px 15px;
- cursor: pointer;
- border: none;
- background-color: transparent;
- color: #bbb;
- font-size: 14px;
- transition: color 0.2s ease, border-bottom-color 0.2s ease;
- border-bottom: 2px solid transparent;
- }
- .tab:hover {
- color: #fff;
- }
- .tab.active {
- color: #fff;
- border-bottom-color: #0078d4; /* Xbox blue */
- }
- .tab-content {
- display: none;
- }
- .tab-content.active {
- display: block;
- }
- /* Create Tab Styles */
- #createTab label {
- display: block;
- margin-bottom: 5px;
- font-weight: bold;
- color: #ccc;
- }
- #createTab input,
- #createTab select,
- #createTab textarea {
- width: calc(100% - 18px); /* Adjust for padding/border */
- padding: 8px;
- margin-bottom: 10px;
- border: 1px solid #555;
- border-radius: 4px;
- background-color: #333;
- color: #eee;
- font-size: 13px;
- }
- #createTab textarea {
- resize: vertical;
- min-height: 60px;
- }
- #createTab button {
- background-color: #0078d4; /* Xbox blue */
- color: white;
- border: none;
- padding: 10px 15px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- transition: background-color 0.2s ease;
- }
- #createTab button:hover {
- background-color: #005a9e; /* Darker blue */
- }
- .macro-type-fields > div {
- border: 1px dashed #555;
- padding: 10px;
- margin-bottom: 10px;
- border-radius: 4px;
- }
- /* List Tab Styles */
- #listTab .macro-item {
- background-color: #282828;
- border: 1px solid #444;
- border-radius: 4px;
- padding: 10px;
- margin-bottom: 10px;
- word-break: break-word; /* Prevent long text overflow */
- }
- .macro-item .macro-header {
- display: flex;
- align-items: center;
- margin-bottom: 5px;
- cursor: pointer; /* Indicate clickable to expand */
- }
- .macro-item .macro-header:hover {
- text-decoration: underline;
- }
- .macro-item .macro-trigger {
- font-weight: bold;
- color: #0078d4;
- margin-right: 5px;
- }
- .macro-item .macro-type,
- .macro-item .macro-mode {
- font-size: 11px;
- color: #aaa;
- margin-right: 5px;
- }
- .macro-item .macro-status {
- font-size: 11px;
- margin-left: auto; /* Push to the right */
- }
- .macro-status.active { color: #4CAF50; /* Green */ }
- .macro-status.inactive { color: #f44336; /* Red */ }
- .macro-item .macro-details {
- font-size: 12px;
- color: #bbb;
- margin-left: 10px;
- border-left: 2px solid #555;
- padding-left: 10px;
- display: none; /* Hidden by default */
- }
- .macro-item.expanded .macro-details {
- display: block; /* Show when expanded */
- }
- .macro-item .macro-details p {
- margin: 3px 0;
- }
- .macro-item .macro-actions {
- margin-top: 10px;
- text-align: right;
- }
- .macro-item .macro-actions button {
- background: none;
- border: none;
- color: #bbb;
- font-size: 14px;
- cursor: pointer;
- margin-left: 5px;
- transition: color 0.2s ease;
- }
- .macro-item .macro-actions button:hover {
- color: #fff;
- }
- .no-macros {
- text-align: center;
- color: #aaa;
- }
- /* Settings Tab Styles */
- #settingsTab button {
- background-color: #555;
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- margin-right: 5px;
- transition: background-color 0.2s ease;
- }
- #settingsTab button:hover {
- background-color: #777;
- }
- #settingsTab button:last-child {
- margin-right: 0;
- }
- #settingsTab .danger-button {
- background-color: #f44336;
- }
- #settingsTab .danger-button:hover {
- background-color: #d32f2f;
- }
- /* Notifications Area */
- #xboxMacroNotificationArea {
- position: fixed;
- top: 10px;
- right: 10px;
- z-index: 10000; /* Above the GUI */
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- }
- .xbox-macro-notification {
- background-color: #333;
- color: #eee;
- padding: 10px 15px;
- margin-bottom: 8px;
- border-radius: 4px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- max-width: 300px;
- word-break: break-word;
- opacity: 0;
- transform: translateX(120%); /* Start off-screen */
- transition: transform 0.4s ease-out, opacity 0.4s ease-out;
- }
- .xbox-macro-notification.info { border-left: 4px solid #2196F3; /* Blue */ }
- .xbox-macro-notification.success { border-left: 4px solid #4CAF50; /* Green */ }
- .xbox-macro-notification.warning { border-left: 4px solid #FF9800; /* Orange */ }
- .xbox-macro-notification.error { border-left: 4px solid #f44336; /* Red */ }
- /* Gamepad Tab Styles */
- #gamepadTab .gamepad-select-container {
- margin-bottom: 15px;
- padding-bottom: 15px;
- border-bottom: 1px solid #444;
- }
- #gamepadTab label {
- display: block;
- margin-bottom: 5px;
- font-weight: bold;
- color: #ccc;
- }
- #gamepadTab select {
- width: 100%;
- padding: 8px;
- border: 1px solid #555;
- border-radius: 4px;
- background-color: #333;
- color: #eee;
- font-size: 13px;
- }
- #selectedGamepadInfo {
- margin-top: 10px;
- font-size: 12px;
- color: #aaa;
- }
- #xboxControllerSVGContainer {
- width: 100%;
- max-width: 300px; /* Max width for the SVG */
- margin: 15px auto; /* Center the SVG */
- background-color: #1a1a1a; /* Dark background for SVG area */
- border-radius: 8px;
- padding: 10px;
- }
- #xboxControllerSVG .button,
- #xboxControllerSVG .axis {
- fill: #555; /* Default color */
- transition: fill 0.05s ease; /* Quick visual feedback */
- }
- #xboxControllerSVG .button.active {
- fill: #0078d4; /* Xbox Blue when active */
- }
- #xboxControllerSVG .axis {
- fill: transparent; /* Axes are transparent by default, filled by JS */
- }
- #xboxControllerSVG .outline {
- stroke: #888;
- stroke-width: 2px;
- fill: #333;
- }
- #xboxControllerSVG text {
- font-family: sans-serif;
- font-size: 10px;
- fill: #ccc;
- text-anchor: middle;
- pointer-events: none; /* Don't interfere with clicks */
- }
- #mapButtonPrompt {
- text-align: center;
- margin-bottom: 15px;
- padding-bottom: 15px;
- border-bottom: 1px solid #444;
- }
- #mapButtonPrompt button {
- background-color: #0078d4;
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- margin-top: 10px;
- transition: background-color 0.2s ease;
- }
- #mapButtonPrompt button:hover {
- background-color: #005a9e;
- }
- #mapButtonPrompt .gamepad-input {
- font-weight: bold;
- color: #0078d4;
- }
- #gamepadMappingList .add-mapping-btn {
- background-color: #4CAF50; /* Green */
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- margin-bottom: 15px;
- transition: background-color 0.2s ease;
- }
- #gamepadMappingList .add-mapping-btn:hover {
- background-color: #388E3C; /* Darker Green */
- }
- #gamepadMappingList .current-mappings-list {
- list-style: none;
- padding: 0;
- margin: 0;
- }
- #gamepadMappingList .mapping-item {
- background-color: #282828;
- border: 1px solid #444;
- border-radius: 4px;
- padding: 8px;
- margin-bottom: 8px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 13px;
- }
- #gamepadMappingList .mapping-item .gamepad-input {
- font-weight: bold;
- color: #0078d4;
- flex-grow: 1; /* Allow input text to take space */
- margin-right: 10px;
- }
- #gamepadMappingList .mapping-item .macro-trigger-key {
- font-weight: bold;
- color: #ccc;
- }
- #gamepadMappingList .mapping-item .remove-mapping-btn {
- background: none;
- border: none;
- color: #f44336; /* Red */
- font-size: 14px;
- cursor: pointer;
- margin-left: 10px;
- transition: color 0.2s ease;
- }
- #gamepadMappingList .mapping-item .remove-mapping-btn:hover {
- color: #d32f2f; /* Darker Red */
- }
- #gamepadMappingList p {
- text-align: center;
- color: #aaa;
- }
- </style>
- <div class="gui-header">
- <h3>Xbox Macro v${CONFIG.VERSION}</h3>
- <div class="header-buttons">
- <button id="lockGuiBtn" title="Lock panel position">🔓</button>
- <button id="minimizeGuiBtn" title="Minimize panel content">▼</button>
- <button id="closeGuiBtn" title="Hide panel">✖</button>
- </div>
- </div>
- <div class="gui-content">
- <div class="tabs">
- <button class="tab active" data-tab="create">Create</button>
- <button class="tab" data-tab="list">List</button>
- <button class="tab" data-tab="gamepad">Gamepad</button>
- <button class="tab" data-tab="settings">Settings</button>
- </div>
- <div id="createTab" class="tab-content active">
- <form id="macroForm">
- <div>
- <label for="macroTriggerKey">Trigger Key:</label>
- <input type="text" id="macroTriggerKey" required placeholder="e.g., f, space, ArrowUp">
- </div>
- <div>
- <label for="macroType">Macro Type:</label>
- <select id="macroType">
- <option value="keySequence">Key Sequence</option>
- <option value="mouseMovement">Mouse Movement</option>
- <option value="mouseClick">Mouse Click</option>
- </select>
- </div>
- <div>
- <label for="macroMode">Mode:</label>
- <select id="macroMode">
- <option value="toggle">Toggle (Press to Start/Stop)</option>
- <option value="hold">Hold (Hold to Run, Release to Stop)</option>
- </select>
- </div>
- <div>
- <label for="macroDescription">Description (Optional):</label>
- <textarea id="macroDescription" placeholder="e.g., Auto-loot, Sprint toggle"></textarea>
- </div>
- <div class="macro-type-fields">
- <div id="keySequenceFields">
- <label for="macroKeyActions">Key Actions (key:delay, ...):</label>
- <input type="text" id="macroKeyActions" placeholder="e.g., w:50, space:100, w:50">
- <label for="macroRepeatDelay">Repeat Delay (ms):</label>
- <input type="number" id="macroRepeatDelay" value="${CONFIG.DEFAULT_REPEAT_DELAY}" min="0">
- </div>
- <div id="mouseMovementFields">
- <label>Start Position (X, Y):</label>
- <input type="number" id="macroMouseStartX" placeholder="e.g., 500">
- <input type="number" id="macroMouseStartY" placeholder="e.g., 300">
- <label>End Position (X, Y):</label>
- <input type="number" id="macroMouseEndX" placeholder="e.g., 800">
- <input type="number" id="macroMouseEndY" placeholder="e.g., 600">
- <label for="macroMouseMovementInternalDelay">Movement Step Delay (ms):</label>
- <input type="number" id="macroMouseMovementInternalDelay" value="${CONFIG.DEFAULT_MOUSE_MOVEMENT_INTERNAL_DELAY}" min="0">
- <label for="macroMouseRepeatDelay">Repeat Delay (ms):</label>
- <input type="number" id="macroMouseRepeatDelay" value="${CONFIG.DEFAULT_REPEAT_DELAY}" min="0">
- </div>
- <div id="mouseClickFields">
- <label>Click Position (X, Y):</label>
- <input type="number" id="macroMouseClickX" placeholder="e.g., 700">
- <input type="number" id="macroMouseClickY" placeholder="e.g., 450">
- <label for="macroMouseButton">Mouse Button:</label>
- <select id="macroMouseButton">
- <option value="left">Left</option>
- <option value="right">Right</option>
- <option value="middle">Middle</option>
- </select>
- <label for="macroMouseClickDuration">Click Duration (ms):</label>
- <input type="number" id="macroMouseClickDuration" value="${CONFIG.DEFAULT_MOUSE_CLICK_DURATION}" min="0">
- <label for="macroMouseClickRepeatDelay">Repeat Delay (ms):</label>
- <input type="number" id="macroMouseClickRepeatDelay" value="${CONFIG.DEFAULT_REPEAT_DELAY}" min="0">
- </div>
- </div>
- <button type="submit" id="addMacroBtn">Add Macro</button>
- </form>
- </div>
- <div id="listTab" class="tab-content">
- <div id="macroList">
- </div>
- </div>
- <div id="gamepadTab" class="tab-content">
- <div class="gamepad-select-container">
- <label for="gamepadSelect">Select Gamepad:</label>
- <select id="gamepadSelect"></select>
- <div id="selectedGamepadInfo"></div>
- </div>
- <div id="xboxControllerSVGContainer">
- <svg id="xboxControllerSVG" viewBox="0 0 500 300" xmlns="http://www.w3.org/2000/svg">
- <rect x="50" y="50" width="400" height="200" rx="20" ry="20" class="outline"/>
- <circle cx="150" cy="150" r="30" class="axis" data-axis-index="0"/> <circle cx="350" cy="150" r="30" class="axis" data-axis-index="1"/> <rect x="100" y="100" width="50" height="50" rx="5" ry="5" class="button" data-button-index="12"/> <rect x="100" y="150" width="50" height="50" rx="5" ry="5" class="button" data-button-index="13"/> <rect x="50" y="150" width="50" height="50" rx="5" ry="5" class="button" data-button-index="14"/> <rect x="150" y="150" width="50" height="50" rx="5" ry="5" class="button" data-button-index="15"/> <circle cx="380" cy="120" r="15" class="button" data-button-index="0"/> <circle cx="410" cy="90" r="15" class="button" data-button-index="1"/> <circle cx="350" cy="90" r="15" class="button" data-button-index="2"/> <circle cx="320" cy="120" r="15" class="button" data-button-index="3"/> <rect x="60" y="40" width="80" height="20" rx="5" ry="5" class="button" data-button-index="4"/> <rect x="360" y="40" width="80" height="20" rx="5" ry="5" class="button" data-button-index="5"/> <rect x="60" y="20" width="80" height="20" rx="5" ry="5" class="axis" data-axis-index="2"/> <rect x="360" y="20" width="80" height="20" rx="5" ry="5" class="axis" data-axis-index="3"/> <circle cx="220" cy="150" r="10" class="button" data-button-index="8"/> <circle cx="280" cy="150" r="10" class="button" data-button-index="9"/> <circle cx="250" cy="90" r="15" class="button" data-button-index="16"/> <circle cx="150" cy="150" r="10" class="button" data-button-index="10"/> <circle cx="350" cy="150" r="10" class="button" data-button-index="11"/> <text x="380" y="125" dy="3">A</text>
- <text x="410" y="95" dy="3">B</text>
- <text x="350" y="95" dy="3">X</text>
- <text x="320" y="125" dy="3">Y</text>
- <text x="100" y="55" dy="3">LB</text>
- <text x="400" y="55" dy="3">RB</text>
- <text x="100" y="35" dy="3">LT</text>
- <text x="400" y="35" dy="3">RT</text>
- <text x="220" y="155" dy="3">VIEW</text>
- <text x="280" y="155" dy="3">MENU</text>
- <text x="250" y="95" dy="3">XBOX</text>
- </svg>
- </div>
- <div id="mapButtonPrompt">
- <p>Press "Map New Gamepad Input" to start mapping.</p>
- </div>
- <div id="gamepadMappingList">
- <div class="mapping-controls">
- <label for="macroToMapSelect">Map to Macro:</label>
- <select id="macroToMapSelect"></select>
- </div>
- </div>
- </div>
- <div id="settingsTab" class="tab-content">
- <h4>Data Management</h4>
- <button id="exportMacrosBtn">Export Macros</button>
- <button id="importMacrosBtn">Import Macros</button>
- <button id="deleteAllMacrosBtn" class="danger-button">Delete All Macros & Mappings</button>
- </div>
- </div>
- `;
- document.body.appendChild(guiElement);
- // Add event listeners for GUI controls
- const closeBtn = document.getElementById('closeGuiBtn');
- if (closeBtn) closeBtn.addEventListener('click', () => toggleGuiVisibility(guiElement));
- const minimizeBtn = document.getElementById('minimizeGuiBtn');
- if (minimizeBtn) minimizeBtn.addEventListener('click', () => minimizePanelContent(guiElement));
- const lockBtn = document.getElementById('lockGuiBtn');
- if (lockBtn) lockBtn.addEventListener('click', () => toggleGuiLock(guiElement));
- // Add event listeners for tabs
- document.querySelectorAll('.tab').forEach(tab => {
- tab.addEventListener('click', () => switchTab(tab.dataset.tab));
- });
- // Add event listener for the macro form submission
- const macroForm = document.getElementById('macroForm');
- if (macroForm) macroForm.addEventListener('submit', (e) => {
- e.preventDefault();
- createMacro();
- });
- // Add event listener for macro type change to update fields
- const macroTypeSelect = document.getElementById('macroType');
- if (macroTypeSelect) macroTypeSelect.addEventListener('change', updateMacroTypeSpecificFields);
- // Add event listeners for settings buttons
- const exportBtn = document.getElementById('exportMacrosBtn');
- if (exportBtn) exportBtn.addEventListener('click', exportMacros);
- const importBtn = document.getElementById('importMacrosBtn');
- if (importBtn) importBtn.addEventListener('click', importMacros);
- const deleteAllBtn = document.getElementById('deleteAllMacrosBtn');
- if (deleteAllBtn) deleteAllBtn.addEventListener('click', deleteAllMacros);
- // Add event listener for gamepad selection change
- const gamepadSelect = document.getElementById('gamepadSelect');
- if (gamepadSelect) {
- gamepadSelect.addEventListener('change', (e) => {
- selectGamepad(e.target.value);
- // Restart polling if a gamepad is selected and we are on the gamepad tab
- if (state.activeTab === 'gamepad' && parseInt(e.target.value, 10) !== -1) {
- initializeGamepadPolling();
- } else {
- stopGamepadPolling();
- }
- });
- }
- // Initial calls
- loadSettings(); // Load settings first to get GUI position and active tab
- loadMacros();
- refreshGUI(); // Populate the macro list
- updateMacroTypeSpecificFields(); // Set initial visibility for macro type fields
- initializeGamepadAPI(); // Setup gamepad event listeners
- setupDragHandlers(guiElement); // Setup drag functionality
- updateGuiVisibility(guiElement); // Apply visibility from settings
- updateGuiLock(guiElement); // Apply lock state from settings
- // Gamepad polling is started when the gamepad tab is activated
- }
- // --- Event Handlers ---
- function handleKeyDown(e) {
- // Ignore if typing in an input field
- if (document.activeElement && ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
- if (e.key === "Escape") document.activeElement.blur(); // Allow escape to exit input
- return;
- }
- // Ignore if GUI is locked and key is used by GUI controls (e.g., Escape to close)
- if (state.isGuiLocked && ['escape', '`', '~'].includes(e.key.toLowerCase())) {
- // Allow these specific keys to still control GUI visibility/lock even when locked
- } else if (state.isGuiLocked && document.getElementById('xboxMacroGUI').contains(e.target)) {
- // If GUI is locked and event originated within the GUI, don't process as macro trigger
- return;
- }
- const key = e.key.toLowerCase();
- const macro = state.macros[key];
- // Check for GUI toggle key (e.g., ` or ~)
- if (key === '`' || key === '~') {
- const guiElement = document.getElementById('xboxMacroGUI');
- if (guiElement) {
- toggleGuiVisibility(guiElement);
- e.preventDefault(); // Prevent the character from appearing in inputs if focused
- e.stopPropagation();
- }
- return; // Don't process as a macro trigger
- }
- // Check for GUI lock toggle (e.g., Ctrl + `)
- if (e.ctrlKey && (key === '`' || key === '~')) {
- const guiElement = document.getElementById('xboxMacroGUI');
- if (guiElement) {
- toggleGuiLock(guiElement);
- e.preventDefault();
- e.stopPropagation();
- }
- return; // Don't process as a macro trigger
- }
- // Check for macro trigger
- if (!macro) return; // Not a macro trigger key
- // If GUI is visible and not locked, and the event target is NOT the game stream,
- // prevent macro execution to avoid accidental triggers while interacting with GUI.
- const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
- const isTargetGameStream = gameStreamElement && gameStreamElement.contains(e.target);
- if (state.isGuiVisible && !state.isGuiLocked && !isTargetGameStream) {
- // If GUI is visible and unlocked, assume user is interacting with the page/GUI, not the game.
- // Only allow macro execution if the event target is specifically the game stream element.
- // This prevents macros triggering while typing in other inputs on the page.
- console.log("[Xbox Macro] Ignoring macro trigger while GUI is visible and unlocked (event target not game stream).");
- return;
- }
- e.preventDefault(); // Prevent default browser action (e.g., scrolling for arrow keys)
- e.stopPropagation(); // Stop event from propagating further
- if (macro.mode === 'toggle') macro.toggle();
- else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
- }
- function handleKeyUp(e) {
- // Ignore if typing in an input field
- if (document.activeElement && ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return;
- // Ignore if GUI is locked and event originated within the GUI
- if (state.isGuiLocked && document.getElementById('xboxMacroGUI').contains(e.target)) {
- return;
- }
- const key = e.key.toLowerCase();
- const macro = state.macros[key];
- // If GUI is visible and not locked, and the event target is NOT the game stream, ignore.
- const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
- const isTargetGameStream = gameStreamElement && gameStreamElement.contains(e.target);
- if (state.isGuiVisible && !state.isGuiLocked && !isTargetGameStream) {
- return;
- }
- if (!macro || macro.mode !== 'hold') return; // Not a hold macro trigger key
- e.preventDefault();
- e.stopPropagation();
- macro.stop();
- }
- function setupDragHandlers(guiElement) {
- const header = guiElement.querySelector('.gui-header'); if (!header) return;
- header.addEventListener('mousedown', e => {
- // Only allow drag if GUI is not locked and the click is directly on the header, not a button
- if (state.isGuiLocked || e.target !== header) return;
- state.isDragging = true;
- const rect = guiElement.getBoundingClientRect();
- state.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
- guiElement.classList.add('dragging');
- document.body.style.userSelect = 'none'; // Prevent text selection while dragging
- });
- document.addEventListener('mousemove', e => {
- if (!state.isDragging) return;
- e.preventDefault(); // Prevent default drag behavior
- e.stopPropagation();
- let x = e.clientX - state.dragOffset.x;
- let y = e.clientY - state.dragOffset.y;
- // Constrain the GUI to the viewport
- x = Math.max(0, Math.min(x, window.innerWidth - guiElement.offsetWidth));
- y = Math.max(0, Math.min(y, window.innerHeight - guiElement.offsetHeight));
- // Use left/top for positioning when dragging
- guiElement.style.left = `${x}px`;
- guiElement.style.top = `${y}px`;
- guiElement.style.right = 'auto'; // Clear right/bottom styles
- guiElement.style.bottom = 'auto';
- });
- document.addEventListener('mouseup', () => {
- if (!state.isDragging) return;
- state.isDragging = false;
- guiElement.classList.remove('dragging');
- document.body.style.userSelect = ''; // Restore text selection
- saveSettings(); // Save the new position
- });
- }
- // --- Initialize ---
- function init() {
- // Add global styles for the GUI and notifications
- const style = document.createElement('style');
- style.innerHTML = `
- .xbox-macro-gui {
- /* Base styles defined in createGUI */
- }
- .xbox-macro-gui.locked {
- /* Add any locked-specific styles here if needed */
- }
- #xboxMacroNotificationArea {
- /* Styles defined in showNotification */
- }
- .xbox-macro-notification {
- /* Styles defined in showNotification */
- }
- `;
- document.head.appendChild(style);
- createGUI(); // Create the GUI element and its basic structure
- // Add global event listeners for macro triggers
- document.addEventListener('keydown', handleKeyDown, true); // Use capturing phase to catch events early
- document.addEventListener('keyup', handleKeyUp, true);
- // Note: Using 'true' for capturing might interfere with some page elements.
- // If issues arise, remove 'true' and rely on event propagation, but macro triggers might be missed
- // if another element stops propagation before the document.
- console.log(`[Xbox Macro] Initialized version ${CONFIG.VERSION}`);
- showNotification(`Xbox Macro v${CONFIG.VERSION} initialized. Press \` to toggle GUI.`, 'info');
- }
- // Run initialization when the window is fully loaded
- if (document.readyState === 'loading') {
- window.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement