Advertisement
Josiahiscool73

Xbox cloud gaming macros(no controller emulation)

Apr 30th, 2025 (edited)
50
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 101.26 KB | None | 0 0
  1. (() => {
  2. 'use strict';
  3.  
  4. // Prevent duplicates if script is run multiple times
  5. if (window.xboxMacroInitializedV3_2) { // Updated version flag
  6. console.warn("[Xbox Macro] Version 3.2.0 or similar already initialized. Aborting.");
  7. const existingGui = document.getElementById('xboxMacroGUI');
  8. if (existingGui && typeof alert === 'function') { // Check if alert is available
  9. alert("Xbox Macro GUI is already running. Please refresh the page if you want to reload it.");
  10. }
  11. return;
  12. }
  13. window.xboxMacroInitializedV3_2 = true;
  14.  
  15. // Configuration
  16. const CONFIG = {
  17. STORAGE_KEY: 'xboxCloudMacros_v3_2', // Updated storage key
  18. SETTINGS_STORAGE_KEY: 'xboxCloudMacroSettings_v3_2', // Updated settings key
  19. GAME_STREAM_ID: 'game-stream',
  20. DEFAULT_REPEAT_DELAY: 250,
  21. DEFAULT_ACTION_DELAY: 50, // Default delay for an action if not specified in a sequence
  22. DEFAULT_MOUSE_MOVEMENT_INTERNAL_DELAY: 100,
  23. DEFAULT_MOUSE_CLICK_DURATION: 50,
  24. GUI_POSITION: { top: '20px', right: '20px' },
  25. VERSION: '3.2.0', // Updated version
  26. NOTIFICATION_DURATION: 4000 // Duration for notifications in ms
  27. };
  28.  
  29. // State
  30. const state = {
  31. macros: {},
  32. isGuiVisible: true,
  33. isGuiLocked: false,
  34. isDragging: false,
  35. dragOffset: { x: 0, y: 0 },
  36. activeTab: 'create',
  37. gamepads: [], // Array to hold connected gamepads
  38. gamepadPollingInterval: null, // Interval ID for polling
  39. activeGamepadIndex: 0, // Index of the currently selected gamepad
  40. gamepadMappings: {} // { gamepadInput: macroTriggerKey } e.g., { 'button-0': 'space', 'axis-0-pos': 'd' }
  41. };
  42.  
  43. // Macro class (largely unchanged, added ID generation)
  44. class Macro {
  45. constructor(triggerKey, type, params, mode, description = '') {
  46. this.triggerKey = triggerKey.toLowerCase();
  47. this.type = type;
  48. this.params = params;
  49. this.mode = mode;
  50. this.description = description;
  51. this.isRunning = false;
  52. this.currentActionIndex = 0; // For sequences (key or other types in future)
  53. this.repeatTimeout = null;
  54. this.actionTimeout = null; // For individual actions within a sequence
  55. this.runCount = 0;
  56. this.createdAt = Date.now();
  57. this.id = `macro-${this.triggerKey}-${this.createdAt}`; // Unique ID
  58. }
  59.  
  60. serialize() {
  61. return {
  62. type: this.type,
  63. params: this.params,
  64. mode: this.mode,
  65. description: this.description,
  66. createdAt: this.createdAt // Include createdAt for consistent ID generation on load
  67. };
  68. }
  69.  
  70. start() {
  71. if (this.isRunning) return;
  72. this.isRunning = true;
  73. this.currentActionIndex = 0;
  74. this.runCount = 0;
  75. console.log(`[Xbox Macro] Started macro: ${this.triggerKey} (Type: ${this.type}, Mode: ${this.mode})`);
  76. this.executeNextAction();
  77. updateMacroStatus(this.triggerKey, true, this.runCount);
  78. }
  79.  
  80. stop() {
  81. if (!this.isRunning) return;
  82. this.isRunning = false;
  83. clearTimeout(this.repeatTimeout);
  84. clearTimeout(this.actionTimeout);
  85. this.currentActionIndex = 0;
  86. updateMacroStatus(this.triggerKey, false, this.runCount);
  87. console.log(`[Xbox Macro] Stopped macro: ${this.triggerKey}`);
  88. }
  89.  
  90. executeNextAction() {
  91. if (!this.isRunning) return;
  92.  
  93. let nextDelayForActionOrRepeat = this.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY;
  94.  
  95. switch (this.type) {
  96. case 'keySequence':
  97. if (this.params.actions && this.params.actions.length > 0) {
  98. if (this.currentActionIndex < this.params.actions.length) {
  99. const action = this.params.actions[this.currentActionIndex];
  100. simulateKeyPress(action.key);
  101.  
  102. nextDelayForActionOrRepeat = action.delayAfter || CONFIG.DEFAULT_ACTION_DELAY;
  103. this.currentActionIndex++;
  104.  
  105. if (this.currentActionIndex >= this.params.actions.length) { // End of sequence for this iteration
  106. this.runCount++;
  107. updateMacroStatus(this.triggerKey, true, this.runCount);
  108. this.currentActionIndex = 0; // Reset for next full sequence repeat
  109. nextDelayForActionOrRepeat = this.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY; // Use overall repeat delay
  110. this.actionTimeout = setTimeout(() => this.executeNextAction(), nextDelayForActionOrRepeat);
  111. } else {
  112. // Delay for the next action in the current sequence
  113. this.actionTimeout = setTimeout(() => this.executeNextAction(), nextDelayForActionOrRepeat);
  114. }
  115. return; // Return to avoid falling through to the main repeatTimeout logic yet
  116. }
  117. } else {
  118. console.warn(`[Xbox Macro] Action sequence for ${this.triggerKey} is empty or invalid.`);
  119. this.stop(); return;
  120. }
  121. break; // Should be handled by return above, but good practice
  122.  
  123. case 'mouseMovement':
  124. simulateMouseMovement(
  125. this.params.startX, this.params.startY,
  126. this.params.endX, this.params.endY,
  127. this.params.movementInternalDelay
  128. );
  129. this.runCount++;
  130. nextDelayForActionOrRepeat = this.params.repeatDelay;
  131. break;
  132.  
  133. case 'mouseClick':
  134. simulateMouseClick(
  135. this.params.x, this.params.y,
  136. this.params.button, this.params.clickDuration
  137. );
  138. this.runCount++;
  139. nextDelayForActionOrRepeat = this.params.repeatDelay;
  140. break;
  141.  
  142. case 'gamepadButtonPress': // New Macro Type
  143. if (typeof this.params.buttonIndex === 'number') {
  144. simulateGamepadButtonPress(state.activeGamepadIndex, this.params.buttonIndex, this.params.pressDuration);
  145. this.runCount++;
  146. nextDelayForActionOrRepeat = this.params.repeatDelay;
  147. } else {
  148. console.warn(`[Xbox Macro] Invalid button index for gamepad macro: ${this.triggerKey}`);
  149. this.stop(); return;
  150. }
  151. break;
  152.  
  153. case 'gamepadAxisMove': // New Macro Type
  154. if (typeof this.params.axisIndex === 'number' && typeof this.params.axisValue === 'number') {
  155. simulateGamepadAxisMove(state.activeGamepadIndex, this.params.axisIndex, this.params.axisValue);
  156. this.runCount++;
  157. nextDelayForActionOrRepeat = this.params.repeatDelay;
  158. } else {
  159. console.warn(`[Xbox Macro] Invalid axis parameters for gamepad macro: ${this.triggerKey}`);
  160. this.stop(); return;
  161. }
  162. break;
  163.  
  164.  
  165. default:
  166. console.error(`[Xbox Macro] Unknown macro type: ${this.type} for trigger: ${this.triggerKey}`);
  167. this.stop(); return;
  168. }
  169.  
  170. updateMacroStatus(this.triggerKey, true, this.runCount);
  171.  
  172. if (this.isRunning) {
  173. // For non-sequence types, or after a full sequence completes and needs to repeat
  174. this.repeatTimeout = setTimeout(() => this.executeNextAction(), nextDelayForActionOrRepeat);
  175. }
  176. }
  177.  
  178. toggle() {
  179. this.isRunning ? this.stop() : this.start();
  180. }
  181. }
  182.  
  183. // --- Simulation Functions (largely unchanged, added gamepad simulation) ---
  184. function simulateKeyPress(key) {
  185. const targetElement = document.getElementById(CONFIG.GAME_STREAM_ID) || document.body;
  186. const keyCode = key.length === 1 ? key.charCodeAt(0) : 0;
  187. const keyEventOptions = {
  188. key: key, code: key.length === 1 ? `Key${key.toUpperCase()}` : key,
  189. keyCode: keyCode, which: keyCode, bubbles: true, cancelable: true, composed: true, view: window
  190. };
  191. try {
  192. targetElement.dispatchEvent(new KeyboardEvent('keydown', keyEventOptions));
  193. setTimeout(() => {
  194. targetElement.dispatchEvent(new KeyboardEvent('keyup', keyEventOptions));
  195. }, 10 + Math.random() * 20);
  196. } catch (e) {
  197. console.error(`[Xbox Macro] Error dispatching key event for "${key}":`, e);
  198. showNotification(`Error simulating key "${key}". Check console.`, 'error');
  199. }
  200. }
  201.  
  202. function simulateMouseMovement(startX, startY, endX, endY, internalDelay) {
  203. const target = window;
  204. // console.log(`[XCloud Sim] Simulating mouse movement from (${startX}, ${startY}) to (${endX}, ${endY})`);
  205. target.dispatchEvent(new PointerEvent('pointermove', {
  206. bubbles: true, cancelable: true, view: window, clientX: startX, clientY: startY,
  207. movementX: 0, movementY: 0, pointerType: 'mouse', buttons: 0
  208. }));
  209. setTimeout(() => {
  210. target.dispatchEvent(new PointerEvent('pointermove', {
  211. bubbles: true, cancelable: true, view: window, clientX: endX, clientY: endY,
  212. movementX: endX - startX, movementY: endY - startY, pointerType: 'mouse', buttons: 0
  213. }));
  214. }, internalDelay);
  215. }
  216.  
  217. function simulateMouseClick(x, y, buttonName = 'left', duration = 50) {
  218. const target = window;
  219. let buttonCode = 0, pointerButtonProperty = 0;
  220. switch (buttonName.toLowerCase()) {
  221. case 'left': buttonCode = 1; pointerButtonProperty = 0; break;
  222. case 'right': buttonCode = 2; pointerButtonProperty = 2; break;
  223. case 'middle': buttonCode = 4; pointerButtonProperty = 1; break;
  224. default: buttonCode = 1; pointerButtonProperty = 0; break;
  225. }
  226. target.dispatchEvent(new PointerEvent('pointerdown', {
  227. bubbles: true, cancelable: true, view: window, clientX: x, clientY: y,
  228. pointerType: 'mouse', button: pointerButtonProperty, buttons: buttonCode
  229. }));
  230. setTimeout(() => {
  231. target.dispatchEvent(new PointerEvent('pointerup', {
  232. bubbles: true, cancelable: true, view: window, clientX: x, clientY: y,
  233. pointerType: 'mouse', button: pointerButtonProperty, buttons: 0
  234. }));
  235. }, duration);
  236. }
  237.  
  238. // Placeholder for gamepad simulation - Actual emulation is complex and depends on the target application
  239. // In a browser context, we can't *truly* inject gamepad input at the system level.
  240. // This would typically involve sending messages to the game stream element if it has an API,
  241. // or relying on the game's own input handling if it listens for events on the stream element.
  242. // For now, these functions are placeholders or could potentially dispatch custom events
  243. // if the target application is designed to listen for them.
  244. function simulateGamepadButtonPress(gamepadIndex, buttonIndex, duration = 50) {
  245. console.warn(`[Xbox Macro] Gamepad button simulation is a placeholder. Attempting to simulate button ${buttonIndex} on gamepad ${gamepadIndex}.`);
  246. // Potential implementation: Dispatch a custom event or find a way to interact with the game stream element's input handling.
  247. // Example (conceptual, may not work):
  248. // const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
  249. // if (gameStreamElement && gameStreamElement.dispatchEvent) {
  250. // gameStreamElement.dispatchEvent(new CustomEvent('gamepadbuttondown', { detail: { gamepadIndex, buttonIndex } }));
  251. // setTimeout(() => {
  252. // gameStreamElement.dispatchEvent(new CustomEvent('gamepadbuttonup', { detail: { gamepadIndex, buttonIndex } }));
  253. // }, duration);
  254. // }
  255. }
  256.  
  257. function simulateGamepadAxisMove(gamepadIndex, axisIndex, value) {
  258. console.warn(`[Xbox Macro] Gamepad axis simulation is a placeholder. Attempting to simulate axis ${axisIndex} to value ${value} on gamepad ${gamepadIndex}.`);
  259. // Potential implementation: Dispatch a custom event or find a way to interact with the game stream element's input handling.
  260. // Example (conceptual, may not work):
  261. // const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
  262. // if (gameStreamElement && gameStreamElement.dispatchEvent) {
  263. // gameStreamElement.dispatchEvent(new CustomEvent('gamepadaxismove', { detail: { gamepadIndex, axisIndex, value } }));
  264. // }
  265. }
  266.  
  267.  
  268. // --- Utility Functions ---
  269. function loadMacros() {
  270. const saved = localStorage.getItem(CONFIG.STORAGE_KEY);
  271. if (!saved) return;
  272. try {
  273. const data = JSON.parse(saved);
  274. for (const triggerKey in data) {
  275. const macroData = data[triggerKey];
  276. // Basic validation for critical properties
  277. if (macroData && typeof macroData.type === 'string' && typeof macroData.params === 'object' && typeof macroData.mode === 'string') {
  278. state.macros[triggerKey] = new Macro(
  279. triggerKey, macroData.type, macroData.params, macroData.mode, macroData.description || ''
  280. );
  281. // Ensure createdAt is loaded for ID consistency
  282. if (macroData.createdAt) state.macros[triggerKey].createdAt = macroData.createdAt;
  283. state.macros[triggerKey].id = `macro-${triggerKey}-${state.macros[triggerKey].createdAt || Date.now()}`;
  284. } else {
  285. console.warn(`[Xbox Macro] Skipping invalid macro data for key: ${triggerKey}`, macroData);
  286. }
  287. }
  288. console.log(`[Xbox Macro] Loaded ${Object.keys(state.macros).length} macros`);
  289. } catch (e) {
  290. console.error('[Xbox Macro] Error loading macros from localStorage:', e);
  291. localStorage.removeItem(CONFIG.STORAGE_KEY); // Clear potentially corrupt data
  292. }
  293. }
  294.  
  295. function saveMacros() {
  296. const serialized = {};
  297. for (const key in state.macros) {
  298. serialized[key] = state.macros[key].serialize();
  299. }
  300. localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(serialized));
  301. }
  302.  
  303. function saveSettings() {
  304. const guiElement = document.getElementById('xboxMacroGUI');
  305. const settings = {
  306. isGuiVisible: state.isGuiVisible,
  307. isGuiLocked: state.isGuiLocked,
  308. guiPosition: guiElement ? {
  309. top: guiElement.style.top, left: guiElement.style.left,
  310. right: guiElement.style.right, bottom: guiElement.style.bottom
  311. } : CONFIG.GUI_POSITION,
  312. activeTab: state.activeTab,
  313. gamepadMappings: state.gamepadMappings // Save gamepad mappings
  314. };
  315. localStorage.setItem(CONFIG.SETTINGS_STORAGE_KEY, JSON.stringify(settings));
  316. }
  317.  
  318. function loadSettings() {
  319. const saved = localStorage.getItem(CONFIG.SETTINGS_STORAGE_KEY);
  320. if (!saved) return;
  321. try {
  322. const settings = JSON.parse(saved);
  323. state.isGuiVisible = typeof settings.isGuiVisible === 'boolean' ? settings.isGuiVisible : true;
  324. state.isGuiLocked = typeof settings.isGuiLocked === 'boolean' ? settings.isGuiLocked : false;
  325. state.activeTab = settings.activeTab || 'create';
  326. state.gamepadMappings = settings.gamepadMappings || {}; // Load gamepad mappings
  327. const guiElement = document.getElementById('xboxMacroGUI');
  328. if (guiElement && settings.guiPosition) {
  329. // Apply position, prioritizing left/top if they exist
  330. guiElement.style.top = settings.guiPosition.top || '';
  331. guiElement.style.left = settings.guiPosition.left || '';
  332. guiElement.style.right = settings.guiPosition.right || '';
  333. guiElement.style.bottom = settings.guiPosition.bottom || '';
  334.  
  335. // Fallback to default if no position is set
  336. if (!guiElement.style.top && !guiElement.style.left && !guiElement.style.right && !guiElement.style.bottom) {
  337. guiElement.style.top = CONFIG.GUI_POSITION.top;
  338. guiElement.style.right = CONFIG.GUI_POSITION.right;
  339. }
  340. }
  341. if (guiElement) { updateGuiVisibility(guiElement); updateGuiLock(guiElement); }
  342. switchTab(state.activeTab, false); // Don't save settings again immediately after loading
  343. } catch (e) { console.error('[Xbox Macro] Error loading settings:', e); }
  344. }
  345.  
  346. function getInputValue(id, type = 'string', defaultValue = '') {
  347. const el = document.getElementById(id);
  348. if (!el) return defaultValue;
  349. let value = el.value.trim();
  350. if (type === 'number') {
  351. const num = parseFloat(value);
  352. return isNaN(num) ? (defaultValue !== '' ? parseFloat(defaultValue) : NaN) : num;
  353. }
  354. return value || defaultValue;
  355. }
  356.  
  357. function parseKeyActionsInput(inputString) {
  358. const actions = [];
  359. if (!inputString) return actions;
  360. const pairs = inputString.split(',');
  361. for (const pair of pairs) {
  362. const parts = pair.trim().split(':');
  363. const key = parts[0] ? parts[0].trim() : null;
  364. if (!key) continue; // Skip if key is empty
  365.  
  366. const delayAfter = parts[1] ? parseInt(parts[1].trim(), 10) : CONFIG.DEFAULT_ACTION_DELAY;
  367. if (isNaN(delayAfter) || delayAfter < 0) {
  368. showNotification(`Invalid delay for key "${key}". Using default ${CONFIG.DEFAULT_ACTION_DELAY}ms.`, 'error');
  369. actions.push({ key: key, delayAfter: CONFIG.DEFAULT_ACTION_DELAY });
  370. } else {
  371. actions.push({ key: key, delayAfter: delayAfter });
  372. }
  373. }
  374. return actions;
  375. }
  376.  
  377.  
  378. function createMacro() {
  379. const triggerKeyInput = getInputValue('macroTriggerKey');
  380. if (!triggerKeyInput) {
  381. showNotification('Error: Trigger key cannot be empty.', 'error');
  382. return false;
  383. }
  384. const triggerKey = triggerKeyInput.toLowerCase();
  385.  
  386. // Prevent using 'gamepad' as a trigger key as it's reserved
  387. if (triggerKey === 'gamepad') {
  388. showNotification('Error: "gamepad" is a reserved trigger key. Please choose another.', 'error');
  389. return false;
  390. }
  391.  
  392. const type = getInputValue('macroType');
  393. const mode = getInputValue('macroMode');
  394. const description = getInputValue('macroDescription');
  395.  
  396. let params = {};
  397. let isValid = true;
  398.  
  399. switch (type) {
  400. case 'keySequence':
  401. params.actions = parseKeyActionsInput(getInputValue('macroKeyActions'));
  402. params.repeatDelay = getInputValue('macroRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
  403. if (params.actions.length === 0) {
  404. showNotification('Error: Key actions cannot be empty for a sequence.', 'error');
  405. isValid = false;
  406. }
  407. break;
  408. case 'mouseMovement':
  409. params.startX = getInputValue('macroMouseStartX', 'number');
  410. params.startY = getInputValue('macroMouseStartY', 'number');
  411. params.endX = getInputValue('macroMouseEndX', 'number');
  412. params.endY = getInputValue('macroMouseEndY', 'number');
  413. params.movementInternalDelay = getInputValue('macroMouseMovementInternalDelay', 'number', CONFIG.DEFAULT_MOUSE_MOVEMENT_INTERNAL_DELAY);
  414. params.repeatDelay = getInputValue('macroMouseRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
  415. if ([params.startX, params.startY, params.endX, params.endY].some(isNaN)) {
  416. showNotification('Error: Mouse movement coordinates must be valid numbers.', 'error'); isValid = false;
  417. }
  418. break;
  419. case 'mouseClick':
  420. params.x = getInputValue('macroMouseClickX', 'number');
  421. params.y = getInputValue('macroMouseClickY', 'number');
  422. params.button = getInputValue('macroMouseButton');
  423. params.clickDuration = getInputValue('macroMouseClickDuration', 'number', CONFIG.DEFAULT_MOUSE_CLICK_DURATION);
  424. params.repeatDelay = getInputValue('macroMouseClickRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
  425. if (isNaN(params.x) || isNaN(params.y)) {
  426. showNotification('Error: Mouse click coordinates must be valid numbers.', 'error'); isValid = false;
  427. }
  428. break;
  429. // Add cases for new gamepad macro types if needed for creation form
  430. // case 'gamepadButtonPress':
  431. // params.buttonIndex = getInputValue('macroGamepadButtonIndex', 'number');
  432. // params.pressDuration = getInputValue('macroGamepadButtonDuration', 'number', 50);
  433. // params.repeatDelay = getInputValue('macroGamepadButtonRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
  434. // if (isNaN(params.buttonIndex)) { showNotification('Error: Gamepad button index must be a number.', 'error'); isValid = false; }
  435. // break;
  436. // case 'gamepadAxisMove':
  437. // params.axisIndex = getInputValue('macroGamepadAxisIndex', 'number');
  438. // params.axisValue = getInputValue('macroGamepadAxisValue', 'number'); // Value between -1 and 1
  439. // params.repeatDelay = getInputValue('macroGamepadAxisRepeatDelay', 'number', CONFIG.DEFAULT_REPEAT_DELAY);
  440. // if (isNaN(params.axisIndex) || isNaN(params.axisValue) || params.axisValue < -1 || params.axisValue > 1) {
  441. // showNotification('Error: Gamepad axis index and value (-1 to 1) must be valid numbers.', 'error'); isValid = false;
  442. // }
  443. // break;
  444. default: showNotification('Error: Invalid macro type selected.', 'error'); return false;
  445. }
  446.  
  447. if (!isValid) return false;
  448.  
  449. const isUpdate = triggerKey in state.macros;
  450. if (isUpdate && state.macros[triggerKey].isRunning) state.macros[triggerKey].stop();
  451. const verb = isUpdate ? 'Updated' : 'Created';
  452.  
  453. state.macros[triggerKey] = new Macro(triggerKey, type, params, mode, description);
  454. saveMacros();
  455. refreshGUI();
  456. showNotification(`${verb} macro for key: ${triggerKey}`, 'success');
  457. document.getElementById('macroForm').reset();
  458. updateMacroTypeSpecificFields();
  459. document.getElementById('macroTriggerKey').focus(); // Focus trigger key for next macro
  460. return true;
  461. }
  462.  
  463. function deleteMacro(triggerKey) {
  464. triggerKey = triggerKey.toLowerCase();
  465. if (state.macros[triggerKey]) {
  466. state.macros[triggerKey].stop();
  467. delete state.macros[triggerKey];
  468. // Also remove any gamepad mappings associated with this macro
  469. for (const gamepadInput in state.gamepadMappings) {
  470. if (state.gamepadMappings[gamepadInput] === triggerKey) {
  471. delete state.gamepadMappings[gamepadInput];
  472. }
  473. }
  474. saveMacros(); saveSettings(); // Save both macros and settings (for mappings)
  475. refreshGUI(); refreshGamepadMappingUI(); // Refresh both GUIs
  476. showNotification(`Deleted macro for key: ${triggerKey}`, 'info');
  477. }
  478. }
  479.  
  480. function deleteAllMacros() {
  481. if (confirm('Are you sure you want to delete ALL macros? This cannot be undone.')) {
  482. stopAllMacros();
  483. state.macros = {};
  484. state.gamepadMappings = {}; // Also clear mappings
  485. saveMacros();
  486. saveSettings(); // Save both
  487. refreshGUI();
  488. refreshGamepadMappingUI(); // Refresh both
  489. showNotification('All macros and gamepad mappings have been deleted.', 'info');
  490. }
  491. }
  492.  
  493. function stopAllMacros() {
  494. Object.values(state.macros).forEach(macro => macro.stop());
  495. showNotification('All macros stopped', 'info');
  496. refreshGUI();
  497. }
  498.  
  499. function exportMacros() {
  500. const serialized = {};
  501. for (const key in state.macros) serialized[key] = state.macros[key].serialize();
  502. const dataStr = JSON.stringify(serialized, null, 2);
  503. const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
  504. const exportName = `xbox-cloud-macros-v${CONFIG.VERSION}-${new Date().toISOString().slice(0, 10)}.json`;
  505. const linkElement = document.createElement('a');
  506. linkElement.setAttribute('href', dataUri);
  507. linkElement.setAttribute('download', exportName);
  508. linkElement.click();
  509. showNotification('Macros exported successfully', 'success');
  510. }
  511.  
  512. function importMacros() {
  513. const input = document.createElement('input');
  514. input.type = 'file'; input.accept = 'application/json';
  515. input.onchange = e => {
  516. const file = e.target.files[0]; if (!file) return;
  517. const reader = new FileReader();
  518. reader.onload = event => {
  519. try {
  520. const importedData = JSON.parse(event.target.result);
  521. let importCount = 0, overwriteCount = 0;
  522. for (const key in importedData) {
  523. const macroData = importedData[key];
  524. if (typeof macroData === 'object' && macroData.type && macroData.params && macroData.mode) {
  525. const lowerKey = key.toLowerCase();
  526. if (state.macros[lowerKey]) {
  527. overwriteCount++;
  528. if (state.macros[lowerKey].isRunning) state.macros[lowerKey].stop();
  529. }
  530. state.macros[lowerKey] = new Macro(
  531. lowerKey, macroData.type, macroData.params, macroData.mode, macroData.description || ''
  532. );
  533. if (macroData.createdAt) state.macros[lowerKey].createdAt = macroData.createdAt;
  534. state.macros[lowerKey].id = `macro-${lowerKey}-${state.macros[lowerKey].createdAt || Date.now()}`;
  535. importCount++;
  536. }
  537. }
  538. saveMacros(); refreshGUI();
  539. showNotification(`Imported ${importCount} macros. ${overwriteCount} existing macros overwritten.`, 'success');
  540. } catch (err) {
  541. console.error('[Xbox Macro] Import error:', err);
  542. showNotification('Error importing macros: Invalid file format or content.', 'error');
  543. }
  544. };
  545. reader.readAsText(file);
  546. };
  547. input.click();
  548. }
  549.  
  550. function showNotification(message, type = 'info') {
  551. let notificationArea = document.getElementById('xboxMacroNotificationArea');
  552. if (!notificationArea) {
  553. notificationArea = document.createElement('div');
  554. notificationArea.id = 'xboxMacroNotificationArea';
  555. document.body.appendChild(notificationArea);
  556. }
  557. const notification = document.createElement('div');
  558. notification.className = `xbox-macro-notification ${type}`;
  559. notification.textContent = message;
  560. notificationArea.prepend(notification); // Add to the top
  561. setTimeout(() => { notification.style.transform = 'translateX(0)'; notification.style.opacity = '1'; }, 10); // Animate in
  562. setTimeout(() => {
  563. notification.style.transform = 'translateX(120%)'; notification.style.opacity = '0'; // Animate out
  564. setTimeout(() => notification.remove(), 500); // Remove after animation
  565. }, CONFIG.NOTIFICATION_DURATION + message.length * 10); // Adjust duration based on message length
  566. }
  567.  
  568. function updateMacroStatus(triggerKey, isRunning, runCount = 0) {
  569. const macro = state.macros[triggerKey]; if (!macro) return;
  570. const statusEl = document.querySelector(`.macro-item[data-trigger="${triggerKey}"] .macro-status`);
  571. if (statusEl) {
  572. statusEl.className = `macro-status ${isRunning ? 'active' : 'inactive'}`;
  573. statusEl.textContent = isRunning ? `● Running (Count: ${runCount})` : '○ Stopped';
  574. }
  575. }
  576.  
  577. function formatActionsForEdit(actions) {
  578. if (!actions || !Array.isArray(actions)) return '';
  579. return actions.map(action => `${action.key}:${action.delayAfter}`).join(', ');
  580. }
  581.  
  582. function editMacro(triggerKey) {
  583. const macro = state.macros[triggerKey]; if (!macro) return;
  584. switchTab('create');
  585. document.getElementById('macroTriggerKey').value = macro.triggerKey;
  586. document.getElementById('macroType').value = macro.type;
  587. updateMacroTypeSpecificFields(); // Update visibility first
  588. document.getElementById('macroMode').value = macro.mode;
  589. document.getElementById('macroDescription').value = macro.description;
  590.  
  591. switch (macro.type) {
  592. case 'keySequence':
  593. document.getElementById('macroKeyActions').value = formatActionsForEdit(macro.params.actions);
  594. document.getElementById('macroRepeatDelay').value = macro.params.repeatDelay;
  595. break;
  596. case 'mouseMovement':
  597. document.getElementById('macroMouseStartX').value = macro.params.startX;
  598. document.getElementById('macroMouseStartY').value = macro.params.startY;
  599. document.getElementById('macroMouseEndX').value = macro.params.endX;
  600. document.getElementById('macroMouseEndY').value = macro.params.endY;
  601. document.getElementById('macroMouseMovementInternalDelay').value = macro.params.movementInternalDelay;
  602. document.getElementById('macroMouseRepeatDelay').value = macro.params.repeatDelay;
  603. break;
  604. case 'mouseClick':
  605. document.getElementById('macroMouseClickX').value = macro.params.x;
  606. document.getElementById('macroMouseClickY').value = macro.params.y;
  607. document.getElementById('macroMouseButton').value = macro.params.button;
  608. document.getElementById('macroMouseClickDuration').value = macro.params.clickDuration;
  609. document.getElementById('macroMouseClickRepeatDelay').value = macro.params.repeatDelay;
  610. break;
  611. // Add cases for new gamepad macro types if implementing creation form fields
  612. // case 'gamepadButtonPress':
  613. // document.getElementById('macroGamepadButtonIndex').value = macro.params.buttonIndex;
  614. // document.getElementById('macroGamepadButtonDuration').value = macro.params.pressDuration;
  615. // document.getElementById('macroGamepadButtonRepeatDelay').value = macro.params.repeatDelay;
  616. // break;
  617. // case 'gamepadAxisMove':
  618. // document.getElementById('macroGamepadAxisIndex').value = macro.params.axisIndex;
  619. // document.getElementById('macroGamepadAxisValue').value = macro.params.axisValue;
  620. // document.getElementById('macroGamepadAxisRepeatDelay').value = macro.params.repeatDelay;
  621. // break;
  622. }
  623. document.getElementById('addMacroBtn').textContent = 'Update Macro';
  624. }
  625.  
  626. // --- GUI Management ---
  627. function toggleGuiVisibility(guiElement) {
  628. state.isGuiVisible = !state.isGuiVisible;
  629. updateGuiVisibility(guiElement);
  630. saveSettings();
  631. }
  632.  
  633. function updateGuiVisibility(guiElement) {
  634. if (!guiElement) guiElement = document.getElementById('xboxMacroGUI');
  635. if (guiElement) guiElement.style.display = state.isGuiVisible ? 'flex' : 'none';
  636.  
  637. const minimizeBtn = document.getElementById('minimizeGuiBtn');
  638. if (minimizeBtn && guiElement) { // Ensure guiElement is defined
  639. const content = guiElement.querySelector('.gui-content');
  640. if (content) { // Ensure content is defined
  641. // If the main GUI is hidden, content should also be considered hidden for minimize button state
  642. const isEffectivelyVisible = state.isGuiVisible && content.style.display !== 'none';
  643. minimizeBtn.textContent = isEffectivelyVisible ? '▼' : '▲';
  644. minimizeBtn.title = isEffectivelyVisible ? 'Minimize panel content' : 'Expand panel content';
  645. }
  646. }
  647. }
  648.  
  649. function minimizePanelContent(guiElement) {
  650. const content = guiElement.querySelector('.gui-content');
  651. const minimizeBtn = document.getElementById('minimizeGuiBtn');
  652. if (content && minimizeBtn) {
  653. const isContentVisible = content.style.display !== 'none';
  654. content.style.display = isContentVisible ? 'none' : 'block';
  655. minimizeBtn.textContent = isContentVisible ? '▲' : '▼';
  656. minimizeBtn.title = isContentVisible ? 'Expand panel content' : 'Minimize panel content';
  657. // This minimized state is not saved in global settings, only overall panel visibility
  658. }
  659. }
  660.  
  661. function toggleGuiLock(guiElement) {
  662. state.isGuiLocked = !state.isGuiLocked;
  663. updateGuiLock(guiElement);
  664. saveSettings();
  665. }
  666.  
  667. function updateGuiLock(guiElement) {
  668. if (!guiElement) guiElement = document.getElementById('xboxMacroGUI');
  669. const lockButton = document.getElementById('lockGuiBtn');
  670. if (lockButton && guiElement) {
  671. lockButton.textContent = state.isGuiLocked ? '🔒' : '🔓';
  672. lockButton.title = state.isGuiLocked ? 'Unlock panel position' : 'Lock panel position';
  673. guiElement.classList.toggle('locked', state.isGuiLocked);
  674. }
  675. }
  676.  
  677. function updateMacroTypeSpecificFields() {
  678. const type = document.getElementById('macroType').value;
  679. // Hide all type-specific fields first
  680. document.getElementById('keySequenceFields').style.display = 'none';
  681. document.getElementById('mouseMovementFields').style.display = 'none';
  682. document.getElementById('mouseClickFields').style.display = 'none';
  683. // document.getElementById('gamepadButtonPressFields').style.display = 'none'; // Uncomment if adding form fields
  684. // document.getElementById('gamepadAxisMoveFields').style.display = 'none'; // Uncomment if adding form fields
  685.  
  686.  
  687. // Show fields for the selected type
  688. const fieldsToShow = {
  689. 'keySequence': 'keySequenceFields',
  690. 'mouseMovement': 'mouseMovementFields',
  691. 'mouseClick': 'mouseClickFields',
  692. // 'gamepadButtonPress': 'gamepadButtonPressFields', // Uncomment if adding form fields
  693. // 'gamepadAxisMove': 'gamepadAxisMoveFields' // Uncomment if adding form fields
  694. };
  695.  
  696. const targetFieldsId = fieldsToShow[type];
  697. if (targetFieldsId) {
  698. const targetElement = document.getElementById(targetFieldsId);
  699. if (targetElement) {
  700. targetElement.style.display = 'block';
  701. }
  702. }
  703.  
  704. // Reset or set default values for relevant repeat delay fields when type changes
  705. const repeatDelayFields = {
  706. keySequence: 'macroRepeatDelay',
  707. mouseMovement: 'macroMouseRepeatDelay',
  708. mouseClick: 'macroMouseClickRepeatDelay',
  709. // gamepadButtonPress: 'macroGamepadButtonRepeatDelay', // Uncomment if adding form fields
  710. // gamepadAxisMove: 'macroGamepadAxisRepeatDelay' // Uncomment if adding form fields
  711. };
  712. for (const fieldType in repeatDelayFields) {
  713. const inputEl = document.getElementById(repeatDelayFields[fieldType]);
  714. if (inputEl) { // Ensure element exists
  715. if (type === fieldType && !inputEl.value) { // If it's the current type and empty, set default
  716. inputEl.value = CONFIG.DEFAULT_REPEAT_DELAY;
  717. }
  718. }
  719. }
  720. }
  721.  
  722. function switchTab(tabId, save = true) {
  723. document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
  724. document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  725. const newTabContent = document.getElementById(tabId + 'Tab');
  726. const newTab = document.querySelector(`.tab[data-tab="${tabId}"]`);
  727. if (newTabContent) newTabContent.classList.add('active');
  728. if (newTab) newTab.classList.add('active');
  729. state.activeTab = tabId;
  730. if (save) saveSettings();
  731.  
  732. // Special handling for the gamepad tab
  733. if (tabId === 'gamepad') {
  734. initializeGamepadPolling();
  735. refreshGamepadMappingUI();
  736. } else {
  737. stopGamepadPolling();
  738. }
  739. }
  740.  
  741. function refreshGUI() {
  742. const macroListElement = document.getElementById('macroList');
  743. if (!macroListElement) return;
  744.  
  745. macroListElement.innerHTML = ''; // Clear current list
  746.  
  747. // Sort macros by creation date, newest first
  748. const sortedMacros = Object.values(state.macros).sort((a, b) => b.createdAt - a.createdAt);
  749.  
  750. if (sortedMacros.length === 0) {
  751. macroListElement.innerHTML = '<p class="no-macros">No macros created yet.</p>';
  752. return;
  753. }
  754.  
  755. sortedMacros.forEach(macro => {
  756. const macroItem = document.createElement('div');
  757. macroItem.className = 'macro-item';
  758. macroItem.dataset.trigger = macro.triggerKey;
  759. macroItem.id = macro.id; // Set the unique ID
  760.  
  761. const statusClass = macro.isRunning ? 'active' : 'inactive';
  762. const statusText = macro.isRunning ? `● Running (Count: ${macro.runCount})` : '○ Stopped';
  763.  
  764. macroItem.innerHTML = `
  765. <div class="macro-header">
  766. <span class="macro-trigger">${macro.triggerKey.toUpperCase()}</span>
  767. <span class="macro-type">(${macro.type.replace(/([A-Z])/g, ' $1').trim()})</span>
  768. <span class="macro-mode">[${macro.mode}]</span>
  769. <span class="macro-status ${statusClass}">${statusText}</span>
  770. </div>
  771. <div class="macro-details">
  772. <p>${macro.description || 'No description'}</p>
  773. <p>Repeat Delay: ${macro.type === 'keySequence' ? (macro.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY) : (macro.params.repeatDelay || CONFIG.DEFAULT_REPEAT_DELAY)}ms</p>
  774. ${macro.type === 'keySequence' ? `<p>Actions: ${formatActionsForEdit(macro.params.actions)}</p>` : ''}
  775. ${macro.type === 'mouseMovement' ? `<p>Move from (${macro.params.startX}, ${macro.params.startY}) to (${macro.params.endX}, ${macro.params.endY})</p>` : ''}
  776. ${macro.type === 'mouseClick' ? `<p>Click ${macro.params.button} at (${macro.params.x}, ${macro.params.y})</p>` : ''}
  777. </div>
  778. <div class="macro-actions">
  779. <button class="edit-macro-btn" data-trigger="${macro.triggerKey}" title="Edit Macro">✏️</button>
  780. <button class="delete-macro-btn" data-trigger="${macro.triggerKey}" title="Delete Macro">🗑️</button>
  781. </div>
  782. `;
  783. macroListElement.appendChild(macroItem);
  784. });
  785.  
  786. // Add event listeners for edit and delete buttons
  787. macroListElement.querySelectorAll('.edit-macro-btn').forEach(button => {
  788. button.addEventListener('click', (e) => {
  789. e.stopPropagation(); // Prevent triggering macro if it's bound to the same key
  790. editMacro(e.target.dataset.trigger);
  791. });
  792. });
  793. macroListElement.querySelectorAll('.delete-macro-btn').forEach(button => {
  794. button.addEventListener('click', (e) => {
  795. e.stopPropagation(); // Prevent triggering macro
  796. deleteMacro(e.target.dataset.trigger);
  797. });
  798. });
  799. }
  800.  
  801. // --- Gamepad Handling ---
  802.  
  803. function initializeGamepadAPI() {
  804. if (!navigator.getGamepads) {
  805. console.warn("[Xbox Macro] Gamepad API not supported in this browser.");
  806. const gamepadTab = document.querySelector('.tab[data-tab="gamepad"]');
  807. if (gamepadTab) gamepadTab.style.display = 'none'; // Hide gamepad tab if not supported
  808. const gamepadTabContent = document.getElementById('gamepadTab');
  809. if (gamepadTabContent) gamepadTabContent.innerHTML = '<p>Gamepad API not supported in this browser.</p>';
  810. return;
  811. }
  812.  
  813. window.addEventListener("gamepadconnected", (e) => {
  814. console.log("[Xbox Macro] Gamepad connected:", e.gamepad);
  815. // Add the gamepad to our state, ensuring no duplicates based on index
  816. state.gamepads[e.gamepad.index] = e.gamepad;
  817. showNotification(`Gamepad ${e.gamepad.index + 1} connected: ${e.gamepad.id}`, 'info');
  818. refreshGamepadList();
  819. // If this is the first gamepad or the only one, select it
  820. if (state.gamepads.filter(g => g !== null).length === 1) {
  821. selectGamepad(e.gamepad.index);
  822. }
  823. if (state.activeTab === 'gamepad') {
  824. initializeGamepadPolling(); // Start polling if on gamepad tab
  825. }
  826. });
  827.  
  828. window.addEventListener("gamepaddisconnected", (e) => {
  829. console.log("[Xbox Macro] Gamepad disconnected:", e.gamepad);
  830. // Remove the gamepad from our state
  831. if (state.gamepads[e.gamepad.index]) {
  832. state.gamepads[e.gamepad.index] = null;
  833. showNotification(`Gamepad ${e.gamepad.index + 1} disconnected: ${e.gamepad.id}`, 'info');
  834. refreshGamepadList();
  835. // If the disconnected gamepad was the active one, try to select another
  836. if (state.activeGamepadIndex === e.gamepad.index) {
  837. const remainingGamepads = state.gamepads.filter(g => g !== null);
  838. if (remainingGamepads.length > 0) {
  839. selectGamepad(remainingGamepads[0].index);
  840. } else {
  841. selectGamepad(0); // Select a non-existent index to indicate no gamepad selected
  842. }
  843. }
  844. }
  845. if (state.activeTab === 'gamepad' && state.gamepads.filter(g => g !== null).length === 0) {
  846. stopGamepadPolling(); // Stop polling if no gamepads remain and on gamepad tab
  847. }
  848. });
  849.  
  850. // Check for already connected gamepads on load
  851. const gamepads = navigator.getGamepads();
  852. for (let i = 0; i < gamepads.length; i++) {
  853. if (gamepads[i]) {
  854. state.gamepads[gamepads[i].index] = gamepads[i];
  855. console.log("[Xbox Macro] Found already connected gamepad:", gamepads[i]);
  856. }
  857. }
  858. refreshGamepadList();
  859. // Select the first available gamepad if any exist
  860. const firstGamepad = state.gamepads.find(g => g !== null);
  861. if (firstGamepad) {
  862. selectGamepad(firstGamepad.index);
  863. } else {
  864. selectGamepad(0); // Select a non-existent index if none found
  865. }
  866. }
  867.  
  868. function refreshGamepadList() {
  869. const gamepadSelect = document.getElementById('gamepadSelect');
  870. if (!gamepadSelect) return;
  871.  
  872. gamepadSelect.innerHTML = ''; // Clear existing options
  873.  
  874. const availableGamepads = state.gamepads.filter(g => g !== null);
  875.  
  876. if (availableGamepads.length === 0) {
  877. const option = document.createElement('option');
  878. option.value = '-1'; // Use -1 to indicate no gamepad selected
  879. option.textContent = 'No gamepads connected';
  880. gamepadSelect.appendChild(option);
  881. selectGamepad(-1); // Ensure state reflects no gamepad selected
  882. return;
  883. }
  884.  
  885. availableGamepads.forEach(gamepad => {
  886. const option = document.createElement('option');
  887. option.value = gamepad.index;
  888. option.textContent = `Gamepad ${gamepad.index + 1}: ${gamepad.id}`;
  889. gamepadSelect.appendChild(option);
  890. });
  891.  
  892. // Select the currently active gamepad in the dropdown
  893. gamepadSelect.value = state.activeGamepadIndex;
  894. }
  895.  
  896. function selectGamepad(index) {
  897. state.activeGamepadIndex = parseInt(index, 10);
  898. console.log(`[Xbox Macro] Selected gamepad index: ${state.activeGamepadIndex}`);
  899. refreshGamepadMappingUI(); // Refresh the mapping UI for the newly selected gamepad
  900. }
  901.  
  902.  
  903. let previousGamepadState = {}; // Store the state of buttons and axes from the previous frame
  904.  
  905. function gamepadPollingLoop() {
  906. // Request the latest gamepad state
  907. const gamepads = navigator.getGamepads();
  908. const activeGamepad = gamepads[state.activeGamepadIndex];
  909.  
  910. if (!activeGamepad) {
  911. // If the active gamepad is no longer available, stop polling or switch
  912. console.warn(`[Xbox Macro] Active gamepad ${state.activeGamepadIndex} not found during polling.`);
  913. stopGamepadPolling();
  914. refreshGamepadList(); // Update the list to reflect disconnected gamepad
  915. return;
  916. }
  917.  
  918. // Get the SVG controller elements
  919. const svgController = document.getElementById('xboxControllerSVG');
  920. if (!svgController) {
  921. // If the SVG isn't rendered (e.g., not on the gamepad tab), stop polling
  922. stopGamepadPolling();
  923. return;
  924. }
  925.  
  926. // Process Buttons
  927. activeGamepad.buttons.forEach((button, index) => {
  928. const buttonElement = svgController.querySelector(`.button[data-button-index="${index}"]`);
  929. const isPressed = button.pressed;
  930. const wasPressed = previousGamepadState.buttons ? previousGamepadState.buttons[index]?.pressed : false;
  931.  
  932. if (buttonElement) {
  933. // Visual feedback: Add/remove an 'active' class based on pressed state
  934. buttonElement.classList.toggle('active', isPressed);
  935. }
  936.  
  937. // Check for button press (transition from not pressed to pressed)
  938. if (isPressed && !wasPressed) {
  939. console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Button ${index} Pressed`);
  940. // Trigger macro if mapped
  941. const mappingKey = `button-${index}`;
  942. if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]]) {
  943. const macro = state.macros[state.gamepadMappings[mappingKey]];
  944. if (macro.mode === 'toggle') macro.toggle();
  945. else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
  946. }
  947. // Update mapping UI if currently mapping a button
  948. if (state.isMapping && state.mappingType === 'button') {
  949. setMappingInput(`button-${index}`);
  950. }
  951. }
  952. // Check for button release (transition from pressed to not pressed)
  953. else if (!isPressed && wasPressed) {
  954. console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Button ${index} Released`);
  955. // Stop 'hold' macro if mapped
  956. const mappingKey = `button-${index}`;
  957. if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]] && state.macros[state.gamepadMappings[mappingKey]].mode === 'hold') {
  958. state.macros[state.gamepadMappings[mappingKey]].stop();
  959. }
  960. }
  961. });
  962.  
  963. // Process Axes
  964. activeGamepad.axes.forEach((axisValue, index) => {
  965. // Axes have a range typically from -1 to 1
  966. // We might want to trigger events based on a threshold
  967. const threshold = 0.5; // Define a threshold for considering an axis "active"
  968. const previousAxisValue = previousGamepadState.axes ? previousGamepadState.axes[index] : 0;
  969.  
  970. // Visual feedback for analog sticks/triggers
  971. const axisElement = svgController.querySelector(`.axis[data-axis-index="${index}"]`);
  972. if (axisElement) {
  973. // Simple visual: change color based on absolute value
  974. const intensity = Math.abs(axisValue);
  975. axisElement.style.fill = `rgba(0, 255, 0, ${intensity})`; // Green based on intensity
  976. }
  977.  
  978. // Check for positive axis movement (e.g., right stick right, right trigger pull)
  979. if (axisValue > threshold && previousAxisValue <= threshold) {
  980. console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Axis ${index} Positive (> ${threshold})`);
  981. // Trigger macro if mapped to positive direction
  982. const mappingKey = `axis-${index}-pos`;
  983. if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]]) {
  984. const macro = state.macros[state.gamepadMappings[mappingKey]];
  985. if (macro.mode === 'toggle') macro.toggle();
  986. else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
  987. }
  988. // Update mapping UI if currently mapping an axis
  989. if (state.isMapping && state.mappingType === 'axis') {
  990. setMappingInput(`axis-${index}-pos`);
  991. }
  992. }
  993. // Check for negative axis movement (e.g., right stick left, left trigger pull)
  994. else if (axisValue < -threshold && previousAxisValue >= -threshold) {
  995. console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Axis ${index} Negative (< ${-threshold})`);
  996. // Trigger macro if mapped to negative direction
  997. const mappingKey = `axis-${index}-neg`;
  998. if (state.gamepadMappings[mappingKey] && state.macros[state.gamepadMappings[mappingKey]]) {
  999. const macro = state.macros[state.gamepadMappings[mappingKey]];
  1000. if (macro.mode === 'toggle') macro.toggle();
  1001. else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
  1002. }
  1003. // Update mapping UI if currently mapping an axis
  1004. if (state.isMapping && state.mappingType === 'axis') {
  1005. setMappingInput(`axis-${index}-neg`);
  1006. }
  1007. }
  1008.  
  1009. // Check for axis returning to center (for 'hold' macros)
  1010. if (Math.abs(axisValue) <= threshold && Math.abs(previousAxisValue) > threshold) {
  1011. console.log(`[Xbox Macro] Gamepad ${activeGamepad.index} Axis ${index} Centered`);
  1012. // Stop 'hold' macros mapped to either direction
  1013. const mappingKeyPos = `axis-${index}-pos`;
  1014. if (state.gamepadMappings[mappingKeyPos] && state.macros[state.gamepadMappings[mappingKeyPos]] && state.macros[state.gamepadMappings[mappingKeyPos]].mode === 'hold') {
  1015. state.macros[state.gamepadMappings[mappingKeyPos]].stop();
  1016. }
  1017. const mappingKeyNeg = `axis-${index}-neg`;
  1018. if (state.gamepadMappings[mappingKeyNeg] && state.macros[state.gamepadMappings[mappingKeyNeg]] && state.macros[state.gamepadMappings[mappingKeyNeg]].mode === 'hold') {
  1019. state.macros[state.gamepadMappings[mappingKeyNeg]].stop();
  1020. }
  1021. }
  1022. });
  1023.  
  1024.  
  1025. // Store current state for the next frame
  1026. previousGamepadState = {
  1027. buttons: activeGamepad.buttons.map(b => ({ pressed: b.pressed, value: b.value })),
  1028. axes: [...activeGamepad.axes] // Create a copy
  1029. };
  1030.  
  1031. // Continue the loop
  1032. state.gamepadPollingInterval = requestAnimationFrame(gamepadPollingLoop);
  1033. }
  1034.  
  1035. function initializeGamepadPolling() {
  1036. if (state.gamepadPollingInterval === null && state.gamepads.filter(g => g !== null).length > 0) {
  1037. console.log("[Xbox Macro] Starting gamepad polling.");
  1038. // Reset previous state when starting polling
  1039. previousGamepadState = {};
  1040. gamepadPollingLoop(); // Start the animation frame loop
  1041. }
  1042. }
  1043.  
  1044. function stopGamepadPolling() {
  1045. if (state.gamepadPollingInterval !== null) {
  1046. console.log("[Xbox Macro] Stopping gamepad polling.");
  1047. cancelAnimationFrame(state.gamepadPollingInterval);
  1048. state.gamepadPollingInterval = null;
  1049. // Clear visual feedback on SVG when polling stops
  1050. const svgController = document.getElementById('xboxControllerSVG');
  1051. if (svgController) {
  1052. svgController.querySelectorAll('.button.active').forEach(el => el.classList.remove('active'));
  1053. svgController.querySelectorAll('.axis').forEach(el => el.style.fill = 'transparent'); // Or default color
  1054. }
  1055. }
  1056. }
  1057.  
  1058. function refreshGamepadMappingUI() {
  1059. const mappingListElement = document.getElementById('gamepadMappingList');
  1060. const gamepadInfoElement = document.getElementById('selectedGamepadInfo');
  1061. const svgContainer = document.getElementById('xboxControllerSVGContainer');
  1062. const mapButtonPrompt = document.getElementById('mapButtonPrompt');
  1063.  
  1064. if (!mappingListElement || !gamepadInfoElement || !svgContainer || !mapButtonPrompt) return;
  1065.  
  1066. const activeGamepad = state.gamepads[state.activeGamepadIndex];
  1067.  
  1068. if (!activeGamepad) {
  1069. gamepadInfoElement.textContent = 'No gamepad selected or connected.';
  1070. mappingListElement.innerHTML = '<p>Connect a gamepad and select it above to set up mappings.</p>';
  1071. svgContainer.style.display = 'none';
  1072. mapButtonPrompt.style.display = 'none';
  1073. return;
  1074. }
  1075.  
  1076. svgContainer.style.display = 'block';
  1077. mapButtonPrompt.style.display = 'block';
  1078. gamepadInfoElement.textContent = `Selected: Gamepad ${activeGamepad.index + 1} - ${activeGamepad.id}`;
  1079.  
  1080. mappingListElement.innerHTML = ''; // Clear current mappings
  1081.  
  1082. // Add a button to start mapping a new input
  1083. const addMappingBtn = document.createElement('button');
  1084. addMappingBtn.textContent = 'Map New Gamepad Input';
  1085. addMappingBtn.className = 'add-mapping-btn';
  1086. addMappingBtn.addEventListener('click', startMapping);
  1087. mappingListElement.appendChild(addMappingBtn);
  1088.  
  1089.  
  1090. // Display current mappings
  1091. const currentMappingsHeader = document.createElement('h4');
  1092. currentMappingsHeader.textContent = 'Current Mappings:';
  1093. mappingListElement.appendChild(currentMappingsHeader);
  1094.  
  1095. const mappingsExist = Object.keys(state.gamepadMappings).length > 0;
  1096. if (!mappingsExist) {
  1097. const noMappings = document.createElement('p');
  1098. noMappings.textContent = 'No mappings set yet.';
  1099. mappingListElement.appendChild(noMappings);
  1100. } else {
  1101. const mappingList = document.createElement('ul');
  1102. mappingList.className = 'current-mappings-list';
  1103. for (const gamepadInput in state.gamepadMappings) {
  1104. const macroTriggerKey = state.gamepadMappings[gamepadInput];
  1105. const macro = state.macros[macroTriggerKey];
  1106. if (macro) {
  1107. const listItem = document.createElement('li');
  1108. listItem.className = 'mapping-item';
  1109. listItem.innerHTML = `
  1110. <span class="gamepad-input">${formatGamepadInput(gamepadInput)}</span>
  1111. <span> maps to </span>
  1112. <span class="macro-trigger-key">${macroTriggerKey.toUpperCase()} (${macro.type.replace(/([A-Z])/g, ' $1').trim()})</span>
  1113. <button class="remove-mapping-btn" data-input="${gamepadInput}" title="Remove Mapping">❌</button>
  1114. `;
  1115. mappingList.appendChild(listItem);
  1116. } else {
  1117. // Clean up orphaned mappings if the macro no longer exists
  1118. delete state.gamepadMappings[gamepadInput];
  1119. saveSettings(); // Save updated mappings
  1120. refreshGamepadMappingUI(); // Refresh UI after cleanup
  1121. return; // Exit to prevent further processing of stale data
  1122. }
  1123. }
  1124. mappingListElement.appendChild(mappingList);
  1125.  
  1126. // Add event listeners for remove buttons
  1127. mappingListElement.querySelectorAll('.remove-mapping-btn').forEach(button => {
  1128. button.addEventListener('click', (e) => {
  1129. const inputToRemove = e.target.dataset.input;
  1130. removeMapping(inputToRemove);
  1131. });
  1132. });
  1133. }
  1134.  
  1135. // Update the macro dropdown for mapping selection
  1136. const macroSelect = document.getElementById('macroToMapSelect');
  1137. if (macroSelect) {
  1138. macroSelect.innerHTML = '<option value="">-- Select Macro --</option>';
  1139. Object.values(state.macros).sort((a, b) => a.triggerKey.localeCompare(b.triggerKey)).forEach(macro => {
  1140. const option = document.createElement('option');
  1141. option.value = macro.triggerKey;
  1142. option.textContent = `${macro.triggerKey.toUpperCase()} (${macro.type.replace(/([A-Z])/g, ' $1').trim()})`;
  1143. macroSelect.appendChild(option);
  1144. });
  1145. }
  1146. }
  1147.  
  1148. function formatGamepadInput(inputKey) {
  1149. const parts = inputKey.split('-');
  1150. if (parts[0] === 'button') {
  1151. return `Button ${parts[1]}`;
  1152. } else if (parts[0] === 'axis') {
  1153. const direction = parts[2] === 'pos' ? 'Positive' : 'Negative';
  1154. return `Axis ${parts[1]} (${direction})`;
  1155. }
  1156. return inputKey; // Fallback
  1157. }
  1158.  
  1159. // Mapping State
  1160. state.isMapping = false;
  1161. state.mappingInputKey = null; // e.g., 'button-0', 'axis-1-pos'
  1162. state.mappingType = null; // 'button' or 'axis'
  1163.  
  1164. function startMapping() {
  1165. if (state.isMapping) {
  1166. showNotification('Already in mapping mode. Press a gamepad input or Cancel.', 'warning');
  1167. return;
  1168. }
  1169. const macroToMapSelect = document.getElementById('macroToMapSelect');
  1170. const selectedMacroTrigger = macroToMapSelect ? macroToMapSelect.value : '';
  1171.  
  1172. if (!selectedMacroTrigger) {
  1173. showNotification('Please select a macro to map.', 'error');
  1174. return;
  1175. }
  1176.  
  1177. state.isMapping = true;
  1178. state.mappingInputKey = null; // Reset input key
  1179. state.mappingType = null; // Reset type
  1180.  
  1181. const mapButtonPrompt = document.getElementById('mapButtonPrompt');
  1182. if (mapButtonPrompt) {
  1183. mapButtonPrompt.innerHTML = `
  1184. <p>Press a button or move an axis on the gamepad...</p>
  1185. <button id="cancelMappingBtn">Cancel</button>
  1186. `;
  1187. document.getElementById('cancelMappingBtn').addEventListener('click', cancelMapping);
  1188. }
  1189. showNotification('Mapping mode started. Press a gamepad input.', 'info');
  1190. }
  1191.  
  1192. function setMappingInput(inputKey) {
  1193. if (!state.isMapping) return;
  1194.  
  1195. state.mappingInputKey = inputKey;
  1196. state.mappingType = inputKey.startsWith('button') ? 'button' : 'axis';
  1197.  
  1198. const mapButtonPrompt = document.getElementById('mapButtonPrompt');
  1199. if (mapButtonPrompt) {
  1200. mapButtonPrompt.innerHTML = `
  1201. <p>Mapping <span class="gamepad-input">${formatGamepadInput(inputKey)}</span> to:</p>
  1202. <select id="macroToMapSelect"></select>
  1203. <button id="confirmMappingBtn">Confirm Mapping</button>
  1204. <button id="cancelMappingBtn">Cancel</button>
  1205. `;
  1206. // Re-populate the macro select dropdown
  1207. const macroSelect = document.getElementById('macroToMapSelect');
  1208. if (macroSelect) {
  1209. macroSelect.innerHTML = '<option value="">-- Select Macro --</option>';
  1210. Object.values(state.macros).sort((a, b) => a.triggerKey.localeCompare(b.triggerKey)).forEach(macro => {
  1211. const option = document.createElement('option');
  1212. option.value = macro.triggerKey;
  1213. option.textContent = `${macro.triggerKey.toUpperCase()} (${macro.type.replace(/([A-Z])/g, ' $1').trim()})`;
  1214. macroSelect.appendChild(option);
  1215. });
  1216. // Pre-select the currently mapped macro if it exists
  1217. if (state.gamepadMappings[inputKey]) {
  1218. macroSelect.value = state.gamepadMappings[inputKey];
  1219. }
  1220. }
  1221.  
  1222. document.getElementById('confirmMappingBtn').addEventListener('click', confirmMapping);
  1223. document.getElementById('cancelMappingBtn').addEventListener('click', cancelMapping);
  1224. }
  1225. showNotification(`Gamepad input detected: ${formatGamepadInput(inputKey)}. Select a macro to map.`, 'info');
  1226. }
  1227.  
  1228. function confirmMapping() {
  1229. if (!state.isMapping || !state.mappingInputKey) {
  1230. showNotification('Mapping process not active.', 'error');
  1231. return;
  1232. }
  1233.  
  1234. const macroToMapSelect = document.getElementById('macroToMapSelect');
  1235. const selectedMacroTrigger = macroToMapSelect ? macroToMapSelect.value : '';
  1236.  
  1237. if (!selectedMacroTrigger) {
  1238. showNotification('Please select a macro to map.', 'error');
  1239. return;
  1240. }
  1241.  
  1242. // Check if this macro is already mapped to another gamepad input
  1243. const existingInput = Object.keys(state.gamepadMappings).find(input => state.gamepadMappings[input] === selectedMacroTrigger);
  1244. if (existingInput && existingInput !== state.mappingInputKey) {
  1245. if (!confirm(`Macro "${selectedMacroTrigger.toUpperCase()}" is already mapped to ${formatGamepadInput(existingInput)}. Do you want to overwrite?`)) {
  1246. cancelMapping();
  1247. return;
  1248. }
  1249. // Remove the old mapping
  1250. delete state.gamepadMappings[existingInput];
  1251. }
  1252.  
  1253.  
  1254. state.gamepadMappings[state.mappingInputKey] = selectedMacroTrigger;
  1255. saveSettings();
  1256. showNotification(`Mapped ${formatGamepadInput(state.mappingInputKey)} to macro "${selectedMacroTrigger.toUpperCase()}".`, 'success');
  1257. cancelMapping(); // Exit mapping mode
  1258. refreshGamepadMappingUI(); // Update the displayed mappings
  1259. }
  1260.  
  1261. function cancelMapping() {
  1262. state.isMapping = false;
  1263. state.mappingInputKey = null;
  1264. state.mappingType = null;
  1265. const mapButtonPrompt = document.getElementById('mapButtonPrompt');
  1266. if (mapButtonPrompt) {
  1267. mapButtonPrompt.innerHTML = '<p>Press "Map New Gamepad Input" to start mapping.</p>';
  1268. }
  1269. showNotification('Mapping cancelled.', 'info');
  1270. refreshGamepadMappingUI(); // Refresh to show the "Map New Input" button again
  1271. }
  1272.  
  1273. function removeMapping(inputKey) {
  1274. if (state.gamepadMappings[inputKey]) {
  1275. const macroTrigger = state.gamepadMappings[inputKey];
  1276. delete state.gamepadMappings[inputKey];
  1277. saveSettings();
  1278. showNotification(`Removed mapping for ${formatGamepadInput(inputKey)} (was mapped to "${macroTrigger.toUpperCase()}").`, 'info');
  1279. refreshGamepadMappingUI();
  1280. }
  1281. }
  1282.  
  1283.  
  1284. // --- Initial Setup ---
  1285. function createGUI() {
  1286. if (document.getElementById('xboxMacroGUI')) {
  1287. console.warn("[Xbox Macro] GUI element already exists.");
  1288. return;
  1289. }
  1290.  
  1291. const guiElement = document.createElement('div');
  1292. guiElement.id = 'xboxMacroGUI';
  1293. guiElement.className = 'xbox-macro-gui';
  1294. guiElement.style.position = 'fixed';
  1295. guiElement.style.zIndex = '9999'; // Ensure it's on top
  1296. guiElement.style.display = 'flex'; // Use flexbox for layout
  1297. guiElement.style.flexDirection = 'column';
  1298. guiElement.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'; // Darker, slightly transparent
  1299. guiElement.style.color = '#eee';
  1300. guiElement.style.fontFamily = 'sans-serif';
  1301. guiElement.style.fontSize = '14px';
  1302. guiElement.style.borderRadius = '8px';
  1303. guiElement.style.overflow = 'hidden'; // Hide overflow for rounded corners
  1304. guiElement.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
  1305. guiElement.style.width = '350px'; // Fixed width for now
  1306. guiElement.style.maxHeight = '90vh'; // Max height to prevent overflow
  1307.  
  1308. // Apply initial position from config (will be overwritten by loadSettings)
  1309. guiElement.style.top = CONFIG.GUI_POSITION.top;
  1310. guiElement.style.right = CONFIG.GUI_POSITION.right;
  1311.  
  1312. guiElement.innerHTML = `
  1313. <style>
  1314. .xbox-macro-gui {
  1315. /* Styles defined above */
  1316. transition: all 0.2s ease-in-out; /* Smooth transitions for position/size */
  1317. }
  1318. .xbox-macro-gui.dragging {
  1319. cursor: grabbing !important;
  1320. opacity: 0.9;
  1321. }
  1322. .xbox-macro-gui.locked .gui-header {
  1323. cursor: default !important; /* No drag cursor when locked */
  1324. }
  1325. .gui-header {
  1326. background-color: rgba(50, 50, 50, 0.95);
  1327. padding: 10px;
  1328. cursor: grab; /* Indicate draggable */
  1329. display: flex;
  1330. justify-content: space-between;
  1331. align-items: center;
  1332. border-bottom: 1px solid #444;
  1333. }
  1334. .gui-header h3 {
  1335. margin: 0;
  1336. font-size: 16px;
  1337. color: #fff;
  1338. }
  1339. .header-buttons button {
  1340. background: none;
  1341. border: none;
  1342. color: #eee;
  1343. font-size: 18px;
  1344. cursor: pointer;
  1345. margin-left: 5px;
  1346. padding: 2px;
  1347. transition: color 0.2s ease;
  1348. }
  1349. .header-buttons button:hover {
  1350. color: #fff;
  1351. }
  1352.  
  1353. .gui-content {
  1354. padding: 15px;
  1355. flex-grow: 1; /* Allow content to fill space */
  1356. overflow-y: auto; /* Enable scrolling for content */
  1357. }
  1358.  
  1359. .tabs {
  1360. display: flex;
  1361. margin-bottom: 15px;
  1362. border-bottom: 1px solid #444;
  1363. }
  1364. .tab {
  1365. padding: 8px 15px;
  1366. cursor: pointer;
  1367. border: none;
  1368. background-color: transparent;
  1369. color: #bbb;
  1370. font-size: 14px;
  1371. transition: color 0.2s ease, border-bottom-color 0.2s ease;
  1372. border-bottom: 2px solid transparent;
  1373. }
  1374. .tab:hover {
  1375. color: #fff;
  1376. }
  1377. .tab.active {
  1378. color: #fff;
  1379. border-bottom-color: #0078d4; /* Xbox blue */
  1380. }
  1381.  
  1382. .tab-content {
  1383. display: none;
  1384. }
  1385. .tab-content.active {
  1386. display: block;
  1387. }
  1388.  
  1389. /* Create Tab Styles */
  1390. #createTab label {
  1391. display: block;
  1392. margin-bottom: 5px;
  1393. font-weight: bold;
  1394. color: #ccc;
  1395. }
  1396. #createTab input,
  1397. #createTab select,
  1398. #createTab textarea {
  1399. width: calc(100% - 18px); /* Adjust for padding/border */
  1400. padding: 8px;
  1401. margin-bottom: 10px;
  1402. border: 1px solid #555;
  1403. border-radius: 4px;
  1404. background-color: #333;
  1405. color: #eee;
  1406. font-size: 13px;
  1407. }
  1408. #createTab textarea {
  1409. resize: vertical;
  1410. min-height: 60px;
  1411. }
  1412. #createTab button {
  1413. background-color: #0078d4; /* Xbox blue */
  1414. color: white;
  1415. border: none;
  1416. padding: 10px 15px;
  1417. border-radius: 4px;
  1418. cursor: pointer;
  1419. font-size: 14px;
  1420. transition: background-color 0.2s ease;
  1421. }
  1422. #createTab button:hover {
  1423. background-color: #005a9e; /* Darker blue */
  1424. }
  1425. .macro-type-fields > div {
  1426. border: 1px dashed #555;
  1427. padding: 10px;
  1428. margin-bottom: 10px;
  1429. border-radius: 4px;
  1430. }
  1431.  
  1432. /* List Tab Styles */
  1433. #listTab .macro-item {
  1434. background-color: #282828;
  1435. border: 1px solid #444;
  1436. border-radius: 4px;
  1437. padding: 10px;
  1438. margin-bottom: 10px;
  1439. word-break: break-word; /* Prevent long text overflow */
  1440. }
  1441. .macro-item .macro-header {
  1442. display: flex;
  1443. align-items: center;
  1444. margin-bottom: 5px;
  1445. cursor: pointer; /* Indicate clickable to expand */
  1446. }
  1447. .macro-item .macro-header:hover {
  1448. text-decoration: underline;
  1449. }
  1450. .macro-item .macro-trigger {
  1451. font-weight: bold;
  1452. color: #0078d4;
  1453. margin-right: 5px;
  1454. }
  1455. .macro-item .macro-type,
  1456. .macro-item .macro-mode {
  1457. font-size: 11px;
  1458. color: #aaa;
  1459. margin-right: 5px;
  1460. }
  1461. .macro-item .macro-status {
  1462. font-size: 11px;
  1463. margin-left: auto; /* Push to the right */
  1464. }
  1465. .macro-status.active { color: #4CAF50; /* Green */ }
  1466. .macro-status.inactive { color: #f44336; /* Red */ }
  1467.  
  1468. .macro-item .macro-details {
  1469. font-size: 12px;
  1470. color: #bbb;
  1471. margin-left: 10px;
  1472. border-left: 2px solid #555;
  1473. padding-left: 10px;
  1474. display: none; /* Hidden by default */
  1475. }
  1476. .macro-item.expanded .macro-details {
  1477. display: block; /* Show when expanded */
  1478. }
  1479. .macro-item .macro-details p {
  1480. margin: 3px 0;
  1481. }
  1482. .macro-item .macro-actions {
  1483. margin-top: 10px;
  1484. text-align: right;
  1485. }
  1486. .macro-item .macro-actions button {
  1487. background: none;
  1488. border: none;
  1489. color: #bbb;
  1490. font-size: 14px;
  1491. cursor: pointer;
  1492. margin-left: 5px;
  1493. transition: color 0.2s ease;
  1494. }
  1495. .macro-item .macro-actions button:hover {
  1496. color: #fff;
  1497. }
  1498. .no-macros {
  1499. text-align: center;
  1500. color: #aaa;
  1501. }
  1502.  
  1503. /* Settings Tab Styles */
  1504. #settingsTab button {
  1505. background-color: #555;
  1506. color: white;
  1507. border: none;
  1508. padding: 8px 12px;
  1509. border-radius: 4px;
  1510. cursor: pointer;
  1511. font-size: 13px;
  1512. margin-right: 5px;
  1513. transition: background-color 0.2s ease;
  1514. }
  1515. #settingsTab button:hover {
  1516. background-color: #777;
  1517. }
  1518. #settingsTab button:last-child {
  1519. margin-right: 0;
  1520. }
  1521. #settingsTab .danger-button {
  1522. background-color: #f44336;
  1523. }
  1524. #settingsTab .danger-button:hover {
  1525. background-color: #d32f2f;
  1526. }
  1527.  
  1528. /* Notifications Area */
  1529. #xboxMacroNotificationArea {
  1530. position: fixed;
  1531. top: 10px;
  1532. right: 10px;
  1533. z-index: 10000; /* Above the GUI */
  1534. display: flex;
  1535. flex-direction: column;
  1536. align-items: flex-end;
  1537. }
  1538. .xbox-macro-notification {
  1539. background-color: #333;
  1540. color: #eee;
  1541. padding: 10px 15px;
  1542. margin-bottom: 8px;
  1543. border-radius: 4px;
  1544. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  1545. max-width: 300px;
  1546. word-break: break-word;
  1547. opacity: 0;
  1548. transform: translateX(120%); /* Start off-screen */
  1549. transition: transform 0.4s ease-out, opacity 0.4s ease-out;
  1550. }
  1551. .xbox-macro-notification.info { border-left: 4px solid #2196F3; /* Blue */ }
  1552. .xbox-macro-notification.success { border-left: 4px solid #4CAF50; /* Green */ }
  1553. .xbox-macro-notification.warning { border-left: 4px solid #FF9800; /* Orange */ }
  1554. .xbox-macro-notification.error { border-left: 4px solid #f44336; /* Red */ }
  1555.  
  1556. /* Gamepad Tab Styles */
  1557. #gamepadTab .gamepad-select-container {
  1558. margin-bottom: 15px;
  1559. padding-bottom: 15px;
  1560. border-bottom: 1px solid #444;
  1561. }
  1562. #gamepadTab label {
  1563. display: block;
  1564. margin-bottom: 5px;
  1565. font-weight: bold;
  1566. color: #ccc;
  1567. }
  1568. #gamepadTab select {
  1569. width: 100%;
  1570. padding: 8px;
  1571. border: 1px solid #555;
  1572. border-radius: 4px;
  1573. background-color: #333;
  1574. color: #eee;
  1575. font-size: 13px;
  1576. }
  1577. #selectedGamepadInfo {
  1578. margin-top: 10px;
  1579. font-size: 12px;
  1580. color: #aaa;
  1581. }
  1582. #xboxControllerSVGContainer {
  1583. width: 100%;
  1584. max-width: 300px; /* Max width for the SVG */
  1585. margin: 15px auto; /* Center the SVG */
  1586. background-color: #1a1a1a; /* Dark background for SVG area */
  1587. border-radius: 8px;
  1588. padding: 10px;
  1589. }
  1590. #xboxControllerSVG .button,
  1591. #xboxControllerSVG .axis {
  1592. fill: #555; /* Default color */
  1593. transition: fill 0.05s ease; /* Quick visual feedback */
  1594. }
  1595. #xboxControllerSVG .button.active {
  1596. fill: #0078d4; /* Xbox Blue when active */
  1597. }
  1598. #xboxControllerSVG .axis {
  1599. fill: transparent; /* Axes are transparent by default, filled by JS */
  1600. }
  1601. #xboxControllerSVG .outline {
  1602. stroke: #888;
  1603. stroke-width: 2px;
  1604. fill: #333;
  1605. }
  1606. #xboxControllerSVG text {
  1607. font-family: sans-serif;
  1608. font-size: 10px;
  1609. fill: #ccc;
  1610. text-anchor: middle;
  1611. pointer-events: none; /* Don't interfere with clicks */
  1612. }
  1613.  
  1614. #mapButtonPrompt {
  1615. text-align: center;
  1616. margin-bottom: 15px;
  1617. padding-bottom: 15px;
  1618. border-bottom: 1px solid #444;
  1619. }
  1620. #mapButtonPrompt button {
  1621. background-color: #0078d4;
  1622. color: white;
  1623. border: none;
  1624. padding: 8px 12px;
  1625. border-radius: 4px;
  1626. cursor: pointer;
  1627. font-size: 13px;
  1628. margin-top: 10px;
  1629. transition: background-color 0.2s ease;
  1630. }
  1631. #mapButtonPrompt button:hover {
  1632. background-color: #005a9e;
  1633. }
  1634. #mapButtonPrompt .gamepad-input {
  1635. font-weight: bold;
  1636. color: #0078d4;
  1637. }
  1638.  
  1639. #gamepadMappingList .add-mapping-btn {
  1640. background-color: #4CAF50; /* Green */
  1641. color: white;
  1642. border: none;
  1643. padding: 8px 12px;
  1644. border-radius: 4px;
  1645. cursor: pointer;
  1646. font-size: 13px;
  1647. margin-bottom: 15px;
  1648. transition: background-color 0.2s ease;
  1649. }
  1650. #gamepadMappingList .add-mapping-btn:hover {
  1651. background-color: #388E3C; /* Darker Green */
  1652. }
  1653.  
  1654. #gamepadMappingList .current-mappings-list {
  1655. list-style: none;
  1656. padding: 0;
  1657. margin: 0;
  1658. }
  1659. #gamepadMappingList .mapping-item {
  1660. background-color: #282828;
  1661. border: 1px solid #444;
  1662. border-radius: 4px;
  1663. padding: 8px;
  1664. margin-bottom: 8px;
  1665. display: flex;
  1666. justify-content: space-between;
  1667. align-items: center;
  1668. font-size: 13px;
  1669. }
  1670. #gamepadMappingList .mapping-item .gamepad-input {
  1671. font-weight: bold;
  1672. color: #0078d4;
  1673. flex-grow: 1; /* Allow input text to take space */
  1674. margin-right: 10px;
  1675. }
  1676. #gamepadMappingList .mapping-item .macro-trigger-key {
  1677. font-weight: bold;
  1678. color: #ccc;
  1679. }
  1680.  
  1681. #gamepadMappingList .mapping-item .remove-mapping-btn {
  1682. background: none;
  1683. border: none;
  1684. color: #f44336; /* Red */
  1685. font-size: 14px;
  1686. cursor: pointer;
  1687. margin-left: 10px;
  1688. transition: color 0.2s ease;
  1689. }
  1690. #gamepadMappingList .mapping-item .remove-mapping-btn:hover {
  1691. color: #d32f2f; /* Darker Red */
  1692. }
  1693. #gamepadMappingList p {
  1694. text-align: center;
  1695. color: #aaa;
  1696. }
  1697. </style>
  1698.  
  1699. <div class="gui-header">
  1700. <h3>Xbox Macro v${CONFIG.VERSION}</h3>
  1701. <div class="header-buttons">
  1702. <button id="lockGuiBtn" title="Lock panel position">🔓</button>
  1703. <button id="minimizeGuiBtn" title="Minimize panel content">▼</button>
  1704. <button id="closeGuiBtn" title="Hide panel">✖</button>
  1705. </div>
  1706. </div>
  1707. <div class="gui-content">
  1708. <div class="tabs">
  1709. <button class="tab active" data-tab="create">Create</button>
  1710. <button class="tab" data-tab="list">List</button>
  1711. <button class="tab" data-tab="gamepad">Gamepad</button>
  1712. <button class="tab" data-tab="settings">Settings</button>
  1713. </div>
  1714.  
  1715. <div id="createTab" class="tab-content active">
  1716. <form id="macroForm">
  1717. <div>
  1718. <label for="macroTriggerKey">Trigger Key:</label>
  1719. <input type="text" id="macroTriggerKey" required placeholder="e.g., f, space, ArrowUp">
  1720. </div>
  1721. <div>
  1722. <label for="macroType">Macro Type:</label>
  1723. <select id="macroType">
  1724. <option value="keySequence">Key Sequence</option>
  1725. <option value="mouseMovement">Mouse Movement</option>
  1726. <option value="mouseClick">Mouse Click</option>
  1727. </select>
  1728. </div>
  1729. <div>
  1730. <label for="macroMode">Mode:</label>
  1731. <select id="macroMode">
  1732. <option value="toggle">Toggle (Press to Start/Stop)</option>
  1733. <option value="hold">Hold (Hold to Run, Release to Stop)</option>
  1734. </select>
  1735. </div>
  1736. <div>
  1737. <label for="macroDescription">Description (Optional):</label>
  1738. <textarea id="macroDescription" placeholder="e.g., Auto-loot, Sprint toggle"></textarea>
  1739. </div>
  1740.  
  1741. <div class="macro-type-fields">
  1742. <div id="keySequenceFields">
  1743. <label for="macroKeyActions">Key Actions (key:delay, ...):</label>
  1744. <input type="text" id="macroKeyActions" placeholder="e.g., w:50, space:100, w:50">
  1745. <label for="macroRepeatDelay">Repeat Delay (ms):</label>
  1746. <input type="number" id="macroRepeatDelay" value="${CONFIG.DEFAULT_REPEAT_DELAY}" min="0">
  1747. </div>
  1748. <div id="mouseMovementFields">
  1749. <label>Start Position (X, Y):</label>
  1750. <input type="number" id="macroMouseStartX" placeholder="e.g., 500">
  1751. <input type="number" id="macroMouseStartY" placeholder="e.g., 300">
  1752. <label>End Position (X, Y):</label>
  1753. <input type="number" id="macroMouseEndX" placeholder="e.g., 800">
  1754. <input type="number" id="macroMouseEndY" placeholder="e.g., 600">
  1755. <label for="macroMouseMovementInternalDelay">Movement Step Delay (ms):</label>
  1756. <input type="number" id="macroMouseMovementInternalDelay" value="${CONFIG.DEFAULT_MOUSE_MOVEMENT_INTERNAL_DELAY}" min="0">
  1757. <label for="macroMouseRepeatDelay">Repeat Delay (ms):</label>
  1758. <input type="number" id="macroMouseRepeatDelay" value="${CONFIG.DEFAULT_REPEAT_DELAY}" min="0">
  1759. </div>
  1760. <div id="mouseClickFields">
  1761. <label>Click Position (X, Y):</label>
  1762. <input type="number" id="macroMouseClickX" placeholder="e.g., 700">
  1763. <input type="number" id="macroMouseClickY" placeholder="e.g., 450">
  1764. <label for="macroMouseButton">Mouse Button:</label>
  1765. <select id="macroMouseButton">
  1766. <option value="left">Left</option>
  1767. <option value="right">Right</option>
  1768. <option value="middle">Middle</option>
  1769. </select>
  1770. <label for="macroMouseClickDuration">Click Duration (ms):</label>
  1771. <input type="number" id="macroMouseClickDuration" value="${CONFIG.DEFAULT_MOUSE_CLICK_DURATION}" min="0">
  1772. <label for="macroMouseClickRepeatDelay">Repeat Delay (ms):</label>
  1773. <input type="number" id="macroMouseClickRepeatDelay" value="${CONFIG.DEFAULT_REPEAT_DELAY}" min="0">
  1774. </div>
  1775. </div>
  1776.  
  1777. <button type="submit" id="addMacroBtn">Add Macro</button>
  1778. </form>
  1779. </div>
  1780.  
  1781. <div id="listTab" class="tab-content">
  1782. <div id="macroList">
  1783. </div>
  1784. </div>
  1785.  
  1786. <div id="gamepadTab" class="tab-content">
  1787. <div class="gamepad-select-container">
  1788. <label for="gamepadSelect">Select Gamepad:</label>
  1789. <select id="gamepadSelect"></select>
  1790. <div id="selectedGamepadInfo"></div>
  1791. </div>
  1792.  
  1793. <div id="xboxControllerSVGContainer">
  1794. <svg id="xboxControllerSVG" viewBox="0 0 500 300" xmlns="http://www.w3.org/2000/svg">
  1795. <rect x="50" y="50" width="400" height="200" rx="20" ry="20" class="outline"/>
  1796.  
  1797. <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>
  1798. <text x="410" y="95" dy="3">B</text>
  1799. <text x="350" y="95" dy="3">X</text>
  1800. <text x="320" y="125" dy="3">Y</text>
  1801. <text x="100" y="55" dy="3">LB</text>
  1802. <text x="400" y="55" dy="3">RB</text>
  1803. <text x="100" y="35" dy="3">LT</text>
  1804. <text x="400" y="35" dy="3">RT</text>
  1805. <text x="220" y="155" dy="3">VIEW</text>
  1806. <text x="280" y="155" dy="3">MENU</text>
  1807. <text x="250" y="95" dy="3">XBOX</text>
  1808.  
  1809. </svg>
  1810. </div>
  1811.  
  1812. <div id="mapButtonPrompt">
  1813. <p>Press "Map New Gamepad Input" to start mapping.</p>
  1814. </div>
  1815.  
  1816. <div id="gamepadMappingList">
  1817. <div class="mapping-controls">
  1818. <label for="macroToMapSelect">Map to Macro:</label>
  1819. <select id="macroToMapSelect"></select>
  1820. </div>
  1821. </div>
  1822. </div>
  1823.  
  1824. <div id="settingsTab" class="tab-content">
  1825. <h4>Data Management</h4>
  1826. <button id="exportMacrosBtn">Export Macros</button>
  1827. <button id="importMacrosBtn">Import Macros</button>
  1828. <button id="deleteAllMacrosBtn" class="danger-button">Delete All Macros & Mappings</button>
  1829. </div>
  1830. </div>
  1831. `;
  1832. document.body.appendChild(guiElement);
  1833.  
  1834. // Add event listeners for GUI controls
  1835. const closeBtn = document.getElementById('closeGuiBtn');
  1836. if (closeBtn) closeBtn.addEventListener('click', () => toggleGuiVisibility(guiElement));
  1837.  
  1838. const minimizeBtn = document.getElementById('minimizeGuiBtn');
  1839. if (minimizeBtn) minimizeBtn.addEventListener('click', () => minimizePanelContent(guiElement));
  1840.  
  1841. const lockBtn = document.getElementById('lockGuiBtn');
  1842. if (lockBtn) lockBtn.addEventListener('click', () => toggleGuiLock(guiElement));
  1843.  
  1844.  
  1845. // Add event listeners for tabs
  1846. document.querySelectorAll('.tab').forEach(tab => {
  1847. tab.addEventListener('click', () => switchTab(tab.dataset.tab));
  1848. });
  1849.  
  1850. // Add event listener for the macro form submission
  1851. const macroForm = document.getElementById('macroForm');
  1852. if (macroForm) macroForm.addEventListener('submit', (e) => {
  1853. e.preventDefault();
  1854. createMacro();
  1855. });
  1856.  
  1857. // Add event listener for macro type change to update fields
  1858. const macroTypeSelect = document.getElementById('macroType');
  1859. if (macroTypeSelect) macroTypeSelect.addEventListener('change', updateMacroTypeSpecificFields);
  1860.  
  1861. // Add event listeners for settings buttons
  1862. const exportBtn = document.getElementById('exportMacrosBtn');
  1863. if (exportBtn) exportBtn.addEventListener('click', exportMacros);
  1864. const importBtn = document.getElementById('importMacrosBtn');
  1865. if (importBtn) importBtn.addEventListener('click', importMacros);
  1866. const deleteAllBtn = document.getElementById('deleteAllMacrosBtn');
  1867. if (deleteAllBtn) deleteAllBtn.addEventListener('click', deleteAllMacros);
  1868.  
  1869. // Add event listener for gamepad selection change
  1870. const gamepadSelect = document.getElementById('gamepadSelect');
  1871. if (gamepadSelect) {
  1872. gamepadSelect.addEventListener('change', (e) => {
  1873. selectGamepad(e.target.value);
  1874. // Restart polling if a gamepad is selected and we are on the gamepad tab
  1875. if (state.activeTab === 'gamepad' && parseInt(e.target.value, 10) !== -1) {
  1876. initializeGamepadPolling();
  1877. } else {
  1878. stopGamepadPolling();
  1879. }
  1880. });
  1881. }
  1882.  
  1883.  
  1884. // Initial calls
  1885. loadSettings(); // Load settings first to get GUI position and active tab
  1886. loadMacros();
  1887. refreshGUI(); // Populate the macro list
  1888. updateMacroTypeSpecificFields(); // Set initial visibility for macro type fields
  1889. initializeGamepadAPI(); // Setup gamepad event listeners
  1890. setupDragHandlers(guiElement); // Setup drag functionality
  1891. updateGuiVisibility(guiElement); // Apply visibility from settings
  1892. updateGuiLock(guiElement); // Apply lock state from settings
  1893. // Gamepad polling is started when the gamepad tab is activated
  1894. }
  1895.  
  1896. // --- Event Handlers ---
  1897. function handleKeyDown(e) {
  1898. // Ignore if typing in an input field
  1899. if (document.activeElement && ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
  1900. if (e.key === "Escape") document.activeElement.blur(); // Allow escape to exit input
  1901. return;
  1902. }
  1903. // Ignore if GUI is locked and key is used by GUI controls (e.g., Escape to close)
  1904. if (state.isGuiLocked && ['escape', '`', '~'].includes(e.key.toLowerCase())) {
  1905. // Allow these specific keys to still control GUI visibility/lock even when locked
  1906. } else if (state.isGuiLocked && document.getElementById('xboxMacroGUI').contains(e.target)) {
  1907. // If GUI is locked and event originated within the GUI, don't process as macro trigger
  1908. return;
  1909. }
  1910.  
  1911.  
  1912. const key = e.key.toLowerCase();
  1913. const macro = state.macros[key];
  1914.  
  1915. // Check for GUI toggle key (e.g., ` or ~)
  1916. if (key === '`' || key === '~') {
  1917. const guiElement = document.getElementById('xboxMacroGUI');
  1918. if (guiElement) {
  1919. toggleGuiVisibility(guiElement);
  1920. e.preventDefault(); // Prevent the character from appearing in inputs if focused
  1921. e.stopPropagation();
  1922. }
  1923. return; // Don't process as a macro trigger
  1924. }
  1925.  
  1926. // Check for GUI lock toggle (e.g., Ctrl + `)
  1927. if (e.ctrlKey && (key === '`' || key === '~')) {
  1928. const guiElement = document.getElementById('xboxMacroGUI');
  1929. if (guiElement) {
  1930. toggleGuiLock(guiElement);
  1931. e.preventDefault();
  1932. e.stopPropagation();
  1933. }
  1934. return; // Don't process as a macro trigger
  1935. }
  1936.  
  1937. // Check for macro trigger
  1938. if (!macro) return; // Not a macro trigger key
  1939.  
  1940. // If GUI is visible and not locked, and the event target is NOT the game stream,
  1941. // prevent macro execution to avoid accidental triggers while interacting with GUI.
  1942. const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
  1943. const isTargetGameStream = gameStreamElement && gameStreamElement.contains(e.target);
  1944.  
  1945. if (state.isGuiVisible && !state.isGuiLocked && !isTargetGameStream) {
  1946. // If GUI is visible and unlocked, assume user is interacting with the page/GUI, not the game.
  1947. // Only allow macro execution if the event target is specifically the game stream element.
  1948. // This prevents macros triggering while typing in other inputs on the page.
  1949. console.log("[Xbox Macro] Ignoring macro trigger while GUI is visible and unlocked (event target not game stream).");
  1950. return;
  1951. }
  1952.  
  1953.  
  1954. e.preventDefault(); // Prevent default browser action (e.g., scrolling for arrow keys)
  1955. e.stopPropagation(); // Stop event from propagating further
  1956.  
  1957. if (macro.mode === 'toggle') macro.toggle();
  1958. else if (macro.mode === 'hold' && !macro.isRunning) macro.start();
  1959. }
  1960.  
  1961. function handleKeyUp(e) {
  1962. // Ignore if typing in an input field
  1963. if (document.activeElement && ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) return;
  1964.  
  1965. // Ignore if GUI is locked and event originated within the GUI
  1966. if (state.isGuiLocked && document.getElementById('xboxMacroGUI').contains(e.target)) {
  1967. return;
  1968. }
  1969.  
  1970. const key = e.key.toLowerCase();
  1971. const macro = state.macros[key];
  1972.  
  1973. // If GUI is visible and not locked, and the event target is NOT the game stream, ignore.
  1974. const gameStreamElement = document.getElementById(CONFIG.GAME_STREAM_ID);
  1975. const isTargetGameStream = gameStreamElement && gameStreamElement.contains(e.target);
  1976. if (state.isGuiVisible && !state.isGuiLocked && !isTargetGameStream) {
  1977. return;
  1978. }
  1979.  
  1980. if (!macro || macro.mode !== 'hold') return; // Not a hold macro trigger key
  1981.  
  1982. e.preventDefault();
  1983. e.stopPropagation();
  1984. macro.stop();
  1985. }
  1986.  
  1987. function setupDragHandlers(guiElement) {
  1988. const header = guiElement.querySelector('.gui-header'); if (!header) return;
  1989. header.addEventListener('mousedown', e => {
  1990. // Only allow drag if GUI is not locked and the click is directly on the header, not a button
  1991. if (state.isGuiLocked || e.target !== header) return;
  1992.  
  1993. state.isDragging = true;
  1994. const rect = guiElement.getBoundingClientRect();
  1995. state.dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
  1996. guiElement.classList.add('dragging');
  1997. document.body.style.userSelect = 'none'; // Prevent text selection while dragging
  1998. });
  1999.  
  2000. document.addEventListener('mousemove', e => {
  2001. if (!state.isDragging) return;
  2002. e.preventDefault(); // Prevent default drag behavior
  2003. e.stopPropagation();
  2004.  
  2005. let x = e.clientX - state.dragOffset.x;
  2006. let y = e.clientY - state.dragOffset.y;
  2007.  
  2008. // Constrain the GUI to the viewport
  2009. x = Math.max(0, Math.min(x, window.innerWidth - guiElement.offsetWidth));
  2010. y = Math.max(0, Math.min(y, window.innerHeight - guiElement.offsetHeight));
  2011.  
  2012. // Use left/top for positioning when dragging
  2013. guiElement.style.left = `${x}px`;
  2014. guiElement.style.top = `${y}px`;
  2015. guiElement.style.right = 'auto'; // Clear right/bottom styles
  2016. guiElement.style.bottom = 'auto';
  2017. });
  2018.  
  2019. document.addEventListener('mouseup', () => {
  2020. if (!state.isDragging) return;
  2021. state.isDragging = false;
  2022. guiElement.classList.remove('dragging');
  2023. document.body.style.userSelect = ''; // Restore text selection
  2024. saveSettings(); // Save the new position
  2025. });
  2026. }
  2027.  
  2028.  
  2029. // --- Initialize ---
  2030. function init() {
  2031. // Add global styles for the GUI and notifications
  2032. const style = document.createElement('style');
  2033. style.innerHTML = `
  2034. .xbox-macro-gui {
  2035. /* Base styles defined in createGUI */
  2036. }
  2037. .xbox-macro-gui.locked {
  2038. /* Add any locked-specific styles here if needed */
  2039. }
  2040. #xboxMacroNotificationArea {
  2041. /* Styles defined in showNotification */
  2042. }
  2043. .xbox-macro-notification {
  2044. /* Styles defined in showNotification */
  2045. }
  2046. `;
  2047. document.head.appendChild(style);
  2048.  
  2049. createGUI(); // Create the GUI element and its basic structure
  2050.  
  2051. // Add global event listeners for macro triggers
  2052. document.addEventListener('keydown', handleKeyDown, true); // Use capturing phase to catch events early
  2053. document.addEventListener('keyup', handleKeyUp, true);
  2054. // Note: Using 'true' for capturing might interfere with some page elements.
  2055. // If issues arise, remove 'true' and rely on event propagation, but macro triggers might be missed
  2056. // if another element stops propagation before the document.
  2057.  
  2058. console.log(`[Xbox Macro] Initialized version ${CONFIG.VERSION}`);
  2059. showNotification(`Xbox Macro v${CONFIG.VERSION} initialized. Press \` to toggle GUI.`, 'info');
  2060. }
  2061.  
  2062. // Run initialization when the window is fully loaded
  2063. if (document.readyState === 'loading') {
  2064. window.addEventListener('DOMContentLoaded', init);
  2065. } else {
  2066. init();
  2067. }
  2068.  
  2069. })();
  2070.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement