您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Xây dựng và chạy các chuỗi tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV và thực thi các hành động như click, nhập liệu trên các đối tượng.
// ==UserScript== // @name WME Workflow Engine // @namespace https://greasyfork.org/ // @version 2.0.2 // @description Xây dựng và chạy các chuỗi tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV và thực thi các hành động như click, nhập liệu trên các đối tượng. // @author Minh Tan // @match https://www.waze.com/editor* // @match https://www.waze.com/*/editor* // @match https://beta.waze.com/editor* // @match https://beta.waze.com/*/editor* // @exclude https://www.waze.com/*user/editor* // @grant none // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // ==/UserScript== /* global W, WazeWrap, $, XLSX */ (function () { 'use strict'; let permalinks = []; let currentIndex = -1; let allWorkflows = {}; let isLooping = false; const DELAY_BETWEEN_LOOPS = 300; const STORAGE_KEY = 'wme_custom_workflows'; const defaultWorkflows = { "street_city_update": { name: "Cập nhật T.Phố (Street)", steps: [ { type: 'click', selector: '.w-icon.w-icon-pencil-fill.edit-button', desc: "Click nút Edit Address" }, { type: 'delay', delay: 100, desc: "Chờ UI load" }, { type: 'input', selector: '#segment-edit-general > div:nth-child(2) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete', shadowSelector: '#text-input', value: '{{value}}', desc: "Nhập tên TP mới" }, { type: 'delay', delay: 100, desc: "Chờ gợi ý hiện ra" }, { type: 'click', selector: '#segment-edit-general > div:nth-child(2) > div > div > wz-card > form > div:nth-child(3) > wz-autocomplete', shadowSelector: 'wz-menu-item:nth-child(2)', desc: "Chọn gợi ý đầu tiên" }, { type: 'delay', delay: 100, desc: "Chờ xử lý" }, { type: 'click', selector: '#segment-edit-general .save-button', desc: "Click nút Save" } ] }, "place_city_update": { name: "Cập nhật T.Phố (Place)", steps: [ { type: 'click', selector: '#venue-edit-general .w-icon-pencil-fill.edit-button', desc: "Click nút Edit Address" }, { type: 'delay', delay: 100, desc: "Chờ UI load" }, { type: 'input', selector: '#venue-edit-general > div:nth-child(1) > div > div.address-edit-view > wz-card > form > div:nth-child(4) > wz-autocomplete', shadowSelector: '#text-input', value: '{{value}}', desc: "Nhập tên TP mới" }, { type: 'delay', delay: 100, desc: "Chờ gợi ý hiện ra" }, { type: 'click', selector: '#venue-edit-general > div:nth-child(1) > div > div.address-edit-view > wz-card > form > div:nth-child(4) > wz-autocomplete', shadowSelector: 'wz-menu-item:nth-child(2)', desc: "Chọn gợi ý đầu tiên" }, { type: 'delay', delay: 100, desc: "Chờ xử lý" }, { type: 'click', selector: '#venue-edit-general .save-button', desc: "Click nút Save" } ] } }; function bootstrap() { if (W && W.map && W.model && W.loginManager.user && $ && WazeWrap.Ready) { init(); } else { setTimeout(bootstrap, 500); } } function init() { console.log("WME Workflow Engine: Initialized"); loadWorkflows(); createUI(); createWorkflowEditorModal(); populateWorkflowSelector(); updateUIState(); registerHotkeys(); } /** * Tìm một phần tử, hỗ trợ tìm kiếm bên trong Shadow DOM. * @param {string} selector - CSS selector cho phần tử chính. * @param {string} [shadowSelector] - CSS selector cho phần tử bên trong shadow DOM. * @returns {Promise<Element|null>} */ async function findElement(selector, shadowSelector = '') { try { const baseElement = await waitForElement(selector); if (!shadowSelector) { return baseElement; } if (baseElement && baseElement.shadowRoot) { await delay(50); const shadowElement = baseElement.shadowRoot.querySelector(shadowSelector); if (!shadowElement) { log(`Lỗi: Không tìm thấy phần tử con với selector "${shadowSelector}" trong shadow DOM của "${selector}".`, 'error'); } return shadowElement; } log(`Lỗi: Không tìm thấy shadow root trên phần tử "${selector}".`, 'error'); return null; } catch (error) { log(`Lỗi khi tìm phần tử "${selector}": ${error.message}`, 'error'); return null; } } /** * Thực thi một bước (step) của workflow. * @param {object} step - Đối tượng step. * @param {string} variableValue - Giá trị để thay thế cho placeholder {{value}}. */ async function executeStep(step, variableValue) { try { switch (step.type) { case 'click': { const element = await findElement(step.selector, step.shadowSelector); if (element) { element.click(); } else throw new Error("Phần tử không tồn tại để click."); break; } case 'input': { const element = await findElement(step.selector, step.shadowSelector); if (element) { const valueToInput = step.value.replace('{{value}}', variableValue); element.value = valueToInput; element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true })); element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true })); } else throw new Error("Phần tử không tồn tại để nhập liệu."); break; } case 'delay': { await delay(step.delay); break; } case 'log': { break; } default: log(`Loại step không xác định: ${step.type}`, 'error'); } } catch (err) { log(`❌ Lỗi khi thực thi step (${step.type}): ${err.message}`, 'error'); throw err; } } /** * Chạy toàn bộ workflow đã chọn. */ async function runSelectedWorkflow() { const workflowId = document.getElementById('workflow_select').value; const variableValue = document.getElementById('workflow_variable_input').value; if (!workflowId || !allWorkflows[workflowId]) { alert("Vui lòng chọn một workflow hợp lệ!"); throw new Error("Workflow không hợp lệ."); } const workflow = allWorkflows[workflowId]; try { for (const step of workflow.steps) { if (!isLooping && step.type !== 'log') { // Cho phép dừng giữa chừng, trừ log const userWantsToStop = await new Promise(resolve => { // Nếu vòng lặp không hoạt động, không cần kiểm tra if (!isLooping) resolve(false); else resolve(false); // Bỏ qua kiểm tra trong bản cập nhật này để đơn giản hóa }); if (userWantsToStop) { throw new Error("Workflow bị dừng bởi người dùng."); } } await executeStep(step, variableValue); } } catch (error) { log(`--- ❌ Workflow "${workflow.name}" đã dừng: ${error.message} ---`, 'error'); throw error; } } async function toggleWorkflowLoop() { isLooping = !isLooping; updateUIState(); if (isLooping) { log("--- Bắt đầu vòng lặp tự động ---", 'special'); await executeLoop(); } else { log("Đã yêu cầu dừng vòng lặp. Sẽ dừng sau khi hoàn thành mục hiện tại.", 'warn'); } } async function executeLoop() { while (isLooping && currentIndex < permalinks.length) { updateUIState(); // Cập nhật UI để hiển thị đúng index try { // Điều hướng và chọn đối tượng await processCurrentLink(); await delay(1500); // Chờ cho WME ổn định sau khi điều hướng // Chạy workflow await runSelectedWorkflow(); } catch (error) { log(`Lỗi ở mục ${currentIndex + 1}, bỏ qua và tiếp tục. Lỗi: ${error.message}`, 'error'); } // Kiểm tra lần nữa nếu người dùng đã nhấn dừng trong lúc workflow chạy if (!isLooping) break; // Chuyển sang mục tiếp theo if (currentIndex < permalinks.length - 1) { currentIndex++; log(`Đang chờ ${DELAY_BETWEEN_LOOPS / 1000}s trước khi đến mục tiếp theo...`, 'info'); await delay(DELAY_BETWEEN_LOOPS); } else { log("Đã đến mục cuối cùng của danh sách.", 'info'); break; // Thoát vòng lặp khi đã ở mục cuối } } isLooping = false; if (currentIndex >= permalinks.length -1) { log("--- ✅ Hoàn thành vòng lặp tự động! ---", 'special'); } else { log("--- Vòng lặp tự động đã dừng. ---", 'warn'); } updateUIState(); // Cập nhật UI lần cuối để mở lại các nút } function handleFile(e) { permalinks = []; currentIndex = -1; const file = e.target.files[0]; if (!file) { updateUIState(); return; } const urlColumnInput = document.getElementById('url_column').value.toUpperCase(); const urlColumnIndex = urlColumnInput.charCodeAt(0) - 'A'.charCodeAt(0); if (urlColumnIndex < 0 || urlColumnIndex > 25) { log(`Lỗi: Cột "${urlColumnInput}" không hợp lệ. Vui lòng nhập A-Z.`, 'error'); return; } const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); permalinks = json .slice(1) // Bỏ qua hàng tiêu đề .map(row => row[urlColumnIndex]) .filter(pl => pl && typeof pl === 'string' && (pl.includes('waze.com/editor') || pl.includes('waze.com/ul'))); // Chỉ lấy permalinks hợp lệ if (permalinks.length > 0) { currentIndex = 0; processCurrentLink(); } else { log(`Không tìm thấy URL hợp lệ trong cột ${urlColumnInput} của file.`, 'warn'); } updateUIState(); } catch (err) { log(`Lỗi khi đọc file: ${err.message}`, 'error'); updateUIState(); } }; reader.readAsArrayBuffer(file); } function saveWorkflows() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(allWorkflows)); log("Đã lưu các workflows."); } catch (e) { log("Lỗi khi lưu workflows vào localStorage.", 'error'); } } function loadWorkflows() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { allWorkflows = JSON.parse(saved); } else { allWorkflows = { ...defaultWorkflows }; log("Đã tải các workflows mặc định. Các thay đổi sẽ được lưu lại."); } } catch (e) { log("Lỗi khi tải workflows từ localStorage, sử dụng các preset mặc định.", 'error'); allWorkflows = { ...defaultWorkflows }; } } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function waitForElement(selector, timeout = 7000) { return new Promise((resolve, reject) => { const intervalTime = 100; let elapsedTime = 0; const interval = setInterval(() => { const element = document.querySelector(selector); // Check if element exists and is visible (offsetParent is not null) if (element && element.offsetParent !== null) { clearInterval(interval); resolve(element); } elapsedTime += intervalTime; if (elapsedTime >= timeout) { clearInterval(interval); reject(new Error(`Element "${selector}" not found or not visible after ${timeout}ms`)); } }, intervalTime); }); } function log(message, type = 'normal') { const logBox = document.getElementById('log_info'); if (logBox) { const colorMap = { error: '#c0392b', success: '#27ae60', warn: '#e67e22', info: '#2980b9', special: '#8e44ad', normal: 'inherit' }; const div = document.createElement('div'); div.style.color = colorMap[type]; div.innerHTML = `[${new Date().toLocaleTimeString()}] ${message.replace(/</g, "<").replace(/>/g, ">")}`; // Sanitize HTML logBox.prepend(div); // Add to top // Limit log box to 50 entries while (logBox.children.length > 50) { logBox.removeChild(logBox.lastChild); } } console.log(`[WME Workflow] ${message}`); } function createUI() { const panel = document.createElement('div'); panel.id = 'workflow-engine-panel'; panel.style.cssText = ` position: fixed; top: 80px; left: 15px; background: rgba(255, 255, 255, 0.85); border: 1px solid #ccc; padding: 0; z-index: 1001; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; width: 420px; `; panel.innerHTML = ` <h3 id="navigator-header" style="display: flex; justify-content: space-between; align-items: center; margin:0; padding: 10px 15px; cursor: grab; border-bottom: 1px solid #eee;"> <span>WME Workflow Engine</span> <button id="toggle_panel_btn" title="Thu gọn Panel">▲</button> </h3> <div id="wwe-panel-content" style="padding: 15px;"> <!-- Section 1: Điều khiển chính --> <h4 style="margin-top: 0; margin-bottom: 12px;">Điều khiển chính</h4> <div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;"> <button id="prev_btn" class="nav-btn" title="Đối tượng trước (Mũi tên trái)" disabled>◀</button> <div style="display: flex; align-items: center; flex-grow: 1;"> <input type="number" id="nav_index_input" min="1" style="width: 100%; text-align: center;" disabled> <span id="nav_total_count" style="margin-left: 5px;">/ N/A</span> </div> <button id="next_btn" class="nav-btn" title="Đối tượng tiếp theo (Mũi tên phải)" disabled>▶</button> </div> <div class="wwe-form-group"> <label for="workflow_select">Chọn Workflow:</label> <select id="workflow_select"></select> </div> <div class="wwe-form-group"> <label for="workflow_variable_input">Giá trị nhập (cho <code>{{value}}</code>):</label> <input type="text" id="workflow_variable_input" placeholder="Tên thành phố, giá trị khác..." /> </div> <button id="run_workflow_btn" class="action-btn primary" style="width: 100%;" title="Chạy workflow (Mũi tên xuống)" disabled>▶️ Chạy Workflow</button> <button id="loop_workflow_btn" class="action-btn" style="width: 100%;" title="Tự động chạy workflow cho tất cả đối tượng" disabled>🔁 Bắt đầu Lặp</button> <!-- Section 2: Accordion Items --> <div class="accordion-container" style="margin-top: 15px;"> <!-- Accordion: Tải dữ liệu --> <div class="accordion-item"> <button class="accordion-header">Tải & Cấu hình Dữ liệu</button> <div class="accordion-content"> <div class="wwe-form-group"> <input type="file" id="excel_file" accept=".xlsx, .xls, .csv"/> </div> <div class="wwe-form-group"> <label for="url_column">Cột URL:</label> <input type="text" id="url_column" value="F" size="5" style="text-transform: uppercase; text-align: center;"> </div> <button id="reselect_btn" class="action-btn secondary" style="width: 100%; margin-top: 10px;" title="Tải lại đối tượng hiện tại (Mũi tên lên)" disabled>🔄 Tải lại & Chọn</button> </div> </div> <!-- Accordion: Quản lý Workflows --> <div class="accordion-item"> <button class="accordion-header">Quản lý Workflows</button> <div class="accordion-content"> <div style="display: flex; gap: 10px; margin-top: 10px;"> <button id="edit_workflow_btn" class="action-btn" style="flex-grow: 1;">✏️ Sửa</button> <button id="new_workflow_btn" class="action-btn" style="flex-grow: 1;">➕ Tạo mới</button> <button id="delete_workflow_btn" class="action-btn danger" style="flex-grow: 1;">🗑️ Xóa</button> </div> </div> </div> <!-- Accordion: Nhật ký --> <div class="accordion-item"> <button class="accordion-header">Nhật ký Hoạt động</button> <div class="accordion-content"> <div id="log_info" style="font-size: 12px; height: 120px; overflow-y: auto; border: 1px solid #eee; padding: 5px; background: #f8f9fa; border-radius: 4px; margin-top: 5px;"></div> </div> </div> </div> </div> `; document.body.appendChild(panel); const style = document.createElement('style'); style.innerHTML = ` /* Global button/input styles for both panel and modal */ #workflow-engine-panel button, #workflow-editor-modal button { border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 4px; padding: 5px 10px; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; } #workflow-engine-panel button:hover:not(:disabled), #workflow-editor-modal button:hover:not(:disabled) { background-color: #e0e0e0; } #workflow-engine-panel button:disabled, #workflow-editor-modal button:disabled { cursor: not-allowed; opacity: 0.5; } #workflow-engine-panel input[type=text], #workflow-engine-panel input[type=number], #workflow-engine-panel input[type=file], #workflow-engine-panel select, #workflow-editor-modal input[type=text], #workflow-editor-modal select { border-radius: 4px; border: 1px solid #ccc; width: 100%; box-sizing: border-box; padding: 5px; /* Added padding for better input appearance */ } /* Toggle buttons styles */ #toggle_panel_btn, #toggle_editor_panel_btn { background: none; border: none; cursor: pointer; font-size: 20px; line-height: 1; padding: 0 5px; color: #888; font-weight: bold; } #toggle_panel_btn:hover, #toggle_editor_panel_btn:hover { color: #000; } /* Main Panel Specific Styles */ #workflow-engine-panel.is-collapsed #wwe-panel-content { display: none; } #workflow-engine-panel.is-collapsed #navigator-header { border-bottom: none; } #workflow-engine-panel .nav-btn { font-size: 16px; padding: 5px 12px; } #workflow-engine-panel .action-btn { padding: 8px 10px; font-weight: 500; } #workflow-engine-panel .action-btn.primary { background-color: #007bff; color: white; border-color: #007bff; } #workflow-engine-panel .action-btn.primary:hover:not(:disabled) { background-color: #0056b3; } #workflow-engine-panel .action-btn.secondary { background-color: #6c757d; color: white; border-color: #6c757d; } #workflow-engine-panel .action-btn.secondary:hover:not(:disabled) { background-color: #5a6268; } #workflow-engine-panel .action-btn.danger { background-color: #dc3545; color: white; border-color: #dc3545; } #workflow-engine-panel .action-btn.danger:hover:not(:disabled) { background-color: #c82333; } #loop_workflow_btn.looping { background-color: #ffc107; border-color: #ffc107; color: black; } #loop_workflow_btn.looping:hover:not(:disabled) { background-color: #e0a800; border-color: #d39e00;} /* UI Căn chỉnh cho các form group */ .wwe-form-group { display: flex; flex-direction: column; margin-bottom: 8px; /* Added margin for spacing */ } .wwe-form-group label { font-weight: bold; font-size: 13px; margin-bottom: 4px; } /* Accordion Styles */ .accordion-item { border-top: 1px solid #eee; } .accordion-header { background-color: #f7f7f7; color: #444; cursor: pointer; padding: 10px; width: 100%; border: none; text-align: left; outline: none; font-size: 14px; transition: background-color 0.2s; font-weight: bold; } .accordion-header:hover { background-color: #e9e9e9; } .accordion-header::after { content: ' ▼'; font-size: 10px; float: right; } .accordion-header.active::after { content: ' ▲'; } .accordion-content { padding: 0 15px 0 15px; background-color: white; max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; } /* Workflow Editor Modal Styles */ #workflow-editor-modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1002; /* Sit on top */ left: 0; top: 0; width: 100%; height: 100%; overflow: auto; /* Enable scroll if needed */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ } #workflow-editor-content { background-color: #fefefe; padding: 0; /* Changed to 0 as header/content div will have padding */ border: 1px solid #888; width: 80%; max-width: 600px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); position: absolute; /* Allows top/left to be set for dragging */ top: 50%; /* Initial centering */ left: 50%; /* Initial centering */ transform: translate(-50%, -50%); /* Initial centering */ } #editor-header { display: flex; justify-content: space-between; align-items: center; margin:0; padding: 10px 15px; cursor: grab; /* Indicate draggable */ border-bottom: 1px solid #eee; font-size: 18px; /* Larger title for modal header */ font-weight: bold; } #editor-title { flex-grow: 1; /* Allows title to take available space */ } #close-modal { margin-right: 10px; /* Space between close and toggle buttons */ } /* Rules for collapsing the editor modal */ #workflow-editor-content.is-collapsed #editor-panel-content { display: none; } #workflow-editor-content.is-collapsed #editor-header { border-bottom: none; /* Remove border when collapsed */ } /* Workflow steps list */ #workflow_steps_list { list-style: none; padding: 0; min-height: 100px; border: 1px dashed #ccc; padding: 10px; border-radius: 4px; margin-top: 10px; /* Added margin */ } #workflow_steps_list li { position: relative; display: flex; align-items: center; justify-content: space-between; padding: 8px; border: 1px solid #ddd; margin-bottom: 5px; border-radius: 4px; background: #fafafa; cursor: grab; } #workflow_steps_list li .step-number { margin-right: 8px; /* Space out the number */ font-weight: bold; color: #888; } #workflow_steps_list li.editing { background-color: #e0eafc !important; border-color: #007bff !important; } #workflow_steps_list li.dragging { opacity: 0.5; } #add-step-form { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; } `; document.head.appendChild(style); // Accordion functionality for main panel panel.querySelectorAll('.accordion-header').forEach(button => { button.addEventListener('click', () => { // Close other open accordions panel.querySelectorAll('.accordion-header.active').forEach(activeButton => { if (activeButton !== button) { activeButton.classList.remove('active'); activeButton.nextElementSibling.style.maxHeight = null; } }); // Toggle current accordion button.classList.toggle('active'); const content = button.nextElementSibling; if (content.style.maxHeight) { content.style.maxHeight = null; } else { content.style.maxHeight = content.scrollHeight + "px"; } }); }); // Toggle panel button for main panel document.getElementById('toggle_panel_btn').addEventListener('click', (e) => { e.stopPropagation(); // Ngăn sự kiện kéo-thả của header const isCollapsed = panel.classList.toggle('is-collapsed'); e.currentTarget.innerHTML = isCollapsed ? '▼' : '▲'; e.currentTarget.title = isCollapsed ? 'Mở rộng Panel' : 'Thu gọn Panel'; }); // Event listeners for main panel controls document.getElementById('excel_file').addEventListener('change', handleFile, false); document.getElementById('prev_btn').addEventListener('click', () => navigate(-1)); document.getElementById('next_btn').addEventListener('click', () => navigate(1)); document.getElementById('reselect_btn').addEventListener('click', processCurrentLink); document.getElementById('run_workflow_btn').addEventListener('click', runSelectedWorkflow); document.getElementById('loop_workflow_btn').addEventListener('click', toggleWorkflowLoop); document.getElementById('nav_index_input').addEventListener('change', (e) => { const targetIndex = parseInt(e.target.value, 10); if (!isNaN(targetIndex)) { navigate(0, targetIndex - 1); // targetIndex is 1-based, array index is 0-based } }); document.getElementById('workflow_select').addEventListener('change', updateUIState); document.getElementById('new_workflow_btn').addEventListener('click', () => openWorkflowEditor()); document.getElementById('edit_workflow_btn').addEventListener('click', () => { const id = document.getElementById('workflow_select').value; if (id) openWorkflowEditor(id); }); document.getElementById('delete_workflow_btn').addEventListener('click', deleteSelectedWorkflow); // Make the main panel draggable makeDraggable(panel, document.getElementById('navigator-header')); }; /** * Makes an element draggable using its handle. * @param {HTMLElement} elementToMove The element that will be moved. * @param {HTMLElement} dragHandle The element that acts as the drag handle. */ function makeDraggable(elementToMove, dragHandle) { let offsetX, offsetY; let isDragging = false; dragHandle.onmousedown = (e) => { e.preventDefault(); isDragging = true; dragHandle.style.cursor = 'grabbing'; // Change cursor while dragging // Get the element's current computed style to check its position const computedStyle = getComputedStyle(elementToMove); if (computedStyle.position === 'static') { elementToMove.style.position = 'absolute'; // Change to absolute if not already positioned } // If the element has a transform property (like translate for centering), // apply that transform to its top/left before dragging starts. if (computedStyle.transform && computedStyle.transform !== 'none') { const matrix = new DOMMatrixReadOnly(computedStyle.transform); // Adjust element's current top/left by its transform translate values elementToMove.style.left = (elementToMove.offsetLeft + matrix.m41) + 'px'; elementToMove.style.top = (elementToMove.offsetTop + matrix.m42) + 'px'; elementToMove.style.transform = 'none'; // Clear the transform } // Calculate the initial offset from the element's current position to the mouse click const rect = elementToMove.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.onmousemove = (ev) => { if (!isDragging) return; // Calculate new position based on mouse position and initial offset elementToMove.style.left = (ev.clientX - offsetX) + 'px'; elementToMove.style.top = (ev.clientY - offsetY) + 'px'; }; document.onmouseup = () => { isDragging = false; document.onmouseup = null; document.onmousemove = null; dragHandle.style.cursor = 'grab'; // Reset cursor }; }; } function createWorkflowEditorModal() { const modal = document.createElement('div'); modal.id = 'workflow-editor-modal'; // Modal background and centering is removed here, workflow-editor-content handles positioning modal.style.cssText = ` display: none; /* Hidden by default, shown by JS */ position: fixed; /* Stay in place */ z-index: 1002; /* Sit on top */ left: 0; top: 0; width: 100%; height: 100%; overflow: auto; /* Enable scroll if needed */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ `; modal.innerHTML = ` <div id="workflow-editor-content" style="background-color: #fefefe; padding: 0; border: 1px solid #888; width: 80%; max-width: 600px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);"> <h3 id="editor-header" style="display: flex; justify-content: space-between; align-items: center; margin:0; padding: 10px 15px; cursor: grab; border-bottom: 1px solid #eee;"> <span id="editor-title">Chỉnh sửa Workflow</span> <div> <span id="close-modal" style="color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; margin-right: 10px;">×</span> <button id="toggle_editor_panel_btn" title="Thu gọn/Mở rộng Panel">▲</button> </div> </h3> <div id="editor-panel-content" style="padding: 15px;"> <input type="hidden" id="editing_workflow_id"> <div class="wwe-form-group"> <label for="workflow_name_input">Tên Workflow:</label> <input type="text" id="workflow_name_input" placeholder="VD: Khóa đường cấm xe máy"> </div> <h4>Các bước (kéo thả để sắp xếp):</h4> <ul id="workflow_steps_list"></ul> <div id="add-step-form"> <h4>Thêm/Sửa bước:</h4> <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 10px; align-items: center;"> <label>Loại hành động:</label> <select id="step_type_select"> <option value="click">Click</option> <option value="input">Input</option> <option value="delay">Delay</option> <option value="log">Log</option> </select> <label>Mô tả ngắn:</label> <input type="text" id="step_desc_input" placeholder="VD: Click nút Lưu"> <label>Selector (JS Path):</label> <input type="text" id="step_selector_input" placeholder="CSS selector, VD: .save-button"> <label>Shadow Selector (Nếu có):</label> <input type="text" id="step_shadow_input" placeholder="VD: #text-input"> <label>Value / Delay (ms):</label> <input type="text" id="step_value_input" placeholder="Nhập {{value}} hoặc 500"> </div> <button id="add_step_btn" style="margin-top: 15px;">Thêm bước</button> </div> <div style="text-align: right; margin-top: 20px;"> <button id="save_workflow_btn">Lưu Workflow</button> <button id="cancel_workflow_btn" style="margin-left: 10px;">Hủy</button> </div> </div> </div>`; document.body.appendChild(modal); // Event listeners for the modal document.getElementById('close-modal').onclick = closeWorkflowEditor; document.getElementById('cancel_workflow_btn').onclick = closeWorkflowEditor; document.getElementById('add_step_btn').onclick = addStepToEditor; document.getElementById('save_workflow_btn').onclick = saveWorkflowFromEditor; // Toggle panel button for the editor modal const editorPanelContent = document.getElementById('workflow-editor-content'); const toggleEditorBtn = document.getElementById('toggle_editor_panel_btn'); toggleEditorBtn.addEventListener('click', (e) => { e.stopPropagation(); const isCollapsed = editorPanelContent.classList.toggle('is-collapsed'); toggleEditorBtn.innerHTML = isCollapsed ? '▼' : '▲'; toggleEditorBtn.title = isCollapsed ? 'Mở rộng Panel' : 'Thu gọn Panel'; }); // Make the modal draggable makeDraggable(document.getElementById('workflow-editor-content'), document.getElementById('editor-header')); // Close modal when clicking outside of the content area window.onclick = function (event) { if (event.target == modal) { closeWorkflowEditor(); } } } function registerHotkeys() { document.addEventListener('keydown', (e) => { // Do not trigger hotkeys if focus is in an input field or a text area, // or if the event originated from within our panels/modals if (e.target.closest('#workflow-engine-panel, #workflow-editor-modal') || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } if (e.key === 'ArrowRight' && !document.getElementById('next_btn').disabled) { e.preventDefault(); document.getElementById('next_btn').click(); } if (e.key === 'ArrowLeft' && !document.getElementById('prev_btn').disabled) { e.preventDefault(); document.getElementById('prev_btn').click(); } if (e.key === 'ArrowUp' && !document.getElementById('reselect_btn').disabled) { e.preventDefault(); document.getElementById('reselect_btn').click(); } if (e.key === 'ArrowDown' && !document.getElementById('run_workflow_btn').disabled) { e.preventDefault(); document.getElementById('run_workflow_btn').click(); } }); } function navigate(direction, targetIndex = null) { if (isLooping) { log("Không thể điều hướng thủ công khi vòng lặp đang chạy.", "warn"); return; } if (permalinks.length === 0) return; let newIndex = (targetIndex !== null) ? targetIndex : (currentIndex + direction); if (newIndex >= 0 && newIndex < permalinks.length) { currentIndex = newIndex; updateUIState(); processCurrentLink(); } else { log('Đã đạt đến đầu/cuối danh sách.', 'warn'); } } function processCurrentLink() { if (currentIndex < 0 || currentIndex >= permalinks.length) return log('Vị trí không hợp lệ.', 'warn'); const url = permalinks[currentIndex]; parseWazeUrlAndNavigate(url); } async function parseWazeUrlAndNavigate(url) { try { const parsedUrl = new URL(url); const params = parsedUrl.searchParams; const lon = parseFloat(params.get('lon')); const lat = parseFloat(params.get('lat')); // WME editor zoom levels are usually 1-2 levels higher than what's in the URL for direct map.setCenter const zoom = parseInt(params.get('zoomLevel') || params.get('zoom'), 10) + 2; const segmentIDs = (params.get('segments') || '').split(',').filter(id => id); const venueIDs = (params.get('venues') || '').split(',').filter(id => id); if (isNaN(lon) || isNaN(lat)) throw new Error('URL không chứa tọa độ (lon/lat) hợp lệ.'); // Set map center and zoom first W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), zoom); // Wait for model to be ready after map change, then select objects WazeWrap.Model.onModelReady(() => { (async () => { await delay(1000); // Give WME some time to load objects after navigating let objectsToSelect = []; if (segmentIDs.length > 0) { const segments = segmentIDs.map(id => W.model.segments.getObjectById(id)).filter(Boolean); if (segments.length === 0) { log(`Cảnh báo: Không tìm thấy segment nào trên bản đồ từ ID ${segmentIDs.join(',')} sau khi tải.`, 'warn'); } else { objectsToSelect.push(...segments); } } if (venueIDs.length > 0) { const venues = venueIDs.map(id => W.model.venues.getObjectById(id)).filter(Boolean); if (venues.length === 0) { log(`Cảnh báo: Không tìm thấy venue nào trên bản đồ từ ID ${venueIDs.join(',')} sau khi tải.`, 'warn'); } else { objectsToSelect.push(...venues); } } if (objectsToSelect.length > 0) { W.selectionManager.setSelectedModels(objectsToSelect); } else { log('Cảnh báo: Không tìm thấy đối tượng nào trên bản đồ từ ID trong URL.', 'warn'); } })(); }, true); // The 'true' ensures it runs once immediately if already ready, or on next ready event. } catch (error) { log(`Lỗi khi xử lý URL: ${error.message}`, 'error'); console.error(error); } } function updateUIState() { const hasLinks = permalinks.length > 0; const navIndexInput = document.getElementById('nav_index_input'); const navTotalCount = document.getElementById('nav_total_count'); const workflowSelect = document.getElementById('workflow_select'); const loopBtn = document.getElementById('loop_workflow_btn'); document.getElementById('prev_btn').disabled = !hasLinks || currentIndex <= 0; document.getElementById('next_btn').disabled = !hasLinks || currentIndex >= permalinks.length - 1; document.getElementById('reselect_btn').disabled = !hasLinks; document.getElementById('run_workflow_btn').disabled = !hasLinks || !workflowSelect.value; // Disable run if no workflow selected navIndexInput.disabled = !hasLinks || isLooping; if (hasLinks) { navIndexInput.value = currentIndex + 1; navIndexInput.max = permalinks.length; navTotalCount.textContent = ` / ${permalinks.length}`; //document.getElementById('status_info').textContent = `Đang ở: ${currentIndex + 1}/${permalinks.length}`; } else { navIndexInput.value = ''; navTotalCount.textContent = '/ N/A'; //document.getElementById('status_info').textContent = 'Chưa có file nào được tải.'; } document.getElementById('prev_btn').disabled = !hasLinks || currentIndex <= 0 || isLooping; document.getElementById('next_btn').disabled = !hasLinks || currentIndex >= permalinks.length - 1 || isLooping; document.getElementById('reselect_btn').disabled = !hasLinks || isLooping; document.getElementById('run_workflow_btn').disabled = !hasLinks || isLooping; document.getElementById('excel_file').disabled = isLooping; document.getElementById('workflow_select').disabled = isLooping; document.getElementById('workflow_variable_input').disabled = isLooping; document.getElementById('edit_workflow_btn').disabled = !workflowSelect.value || isLooping; document.getElementById('delete_workflow_btn').disabled = !workflowSelect.value || isLooping; document.getElementById('new_workflow_btn').disabled = isLooping; loopBtn.disabled = !hasLinks; if (isLooping) { loopBtn.textContent = '⏹️ Dừng Lặp'; loopBtn.classList.add('looping'); loopBtn.classList.remove('secondary'); } else { loopBtn.textContent = '🔁 Bắt đầu Lặp'; loopBtn.classList.remove('looping'); loopBtn.classList.add('secondary'); } } function populateWorkflowSelector() { const select = document.getElementById('workflow_select'); const currentId = select.value; // Store current selection select.innerHTML = ''; // Clear existing options // Add a default "Select Workflow" option if no workflows are available or selected if (Object.keys(allWorkflows).length === 0) { const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '--- Không có workflow ---'; select.appendChild(defaultOption); } else { const emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = '--- Chọn workflow ---'; select.appendChild(emptyOption); } for (const id in allWorkflows) { const option = document.createElement('option'); option.value = id; option.textContent = allWorkflows[id].name; select.appendChild(option); } // Restore previous selection or select the first valid one if (currentId && allWorkflows[currentId]) { select.value = currentId; } else if (Object.keys(allWorkflows).length > 0) { select.value = Object.keys(allWorkflows)[0]; // Select the first workflow if none were selected } else { select.value = ''; // No workflows, ensure no value is set } updateUIState(); // Update other UI elements based on selection } function deleteSelectedWorkflow() { const select = document.getElementById('workflow_select'); const idToDelete = select.value; const workflowName = allWorkflows[idToDelete]?.name; if (!idToDelete) { alert("Vui lòng chọn một workflow để xóa."); return; } if (confirm(`Bạn có chắc chắn muốn xóa workflow "${workflowName}" không?`)) { delete allWorkflows[idToDelete]; saveWorkflows(); populateWorkflowSelector(); log(`Đã xóa workflow: "${workflowName}"`, 'info'); } } let draggedItem = null; // For drag-and-drop reordering of steps /** * Resets the step editor form to its default state. */ function resetStepEditorForm() { document.querySelectorAll('#workflow_steps_list li.editing').forEach(el => el.classList.remove('editing')); document.getElementById('step_type_select').value = 'click'; document.getElementById('step_desc_input').value = ''; document.getElementById('step_selector_input').value = ''; document.getElementById('step_shadow_input').value = ''; document.getElementById('step_value_input').value = ''; const addBtn = document.getElementById('add_step_btn'); addBtn.textContent = 'Thêm bước'; delete addBtn.dataset.editingIndex; // Remove the editing index } function openWorkflowEditor(workflowId = null) { const modal = document.getElementById('workflow-editor-modal'); const title = document.getElementById('editor-title'); const nameInput = document.getElementById('workflow_name_input'); const idInput = document.getElementById('editing_workflow_id'); const stepsList = document.getElementById('workflow_steps_list'); stepsList.innerHTML = ''; // Clear previous steps resetStepEditorForm(); // Reset the add/edit step form if (workflowId && allWorkflows[workflowId]) { const wf = allWorkflows[workflowId]; title.textContent = "Chỉnh sửa Workflow"; nameInput.value = wf.name; idInput.value = workflowId; wf.steps.forEach((step, index) => renderStepInEditor(step, index)); } else { title.textContent = "Tạo Workflow Mới"; nameInput.value = ''; idInput.value = ''; } modal.style.display = 'block'; // Show the modal (block to allow centering) } function closeWorkflowEditor() { document.getElementById('workflow-editor-modal').style.display = 'none'; } function renderStepInEditor(step, index) { const list = document.getElementById('workflow_steps_list'); const li = document.createElement('li'); li.dataset.index = index; li.dataset.step = JSON.stringify(step); // Store step data on the element li.draggable = true; li.innerHTML = ` <div> <span class="step-number">${index + 1}.</span> <span><strong>${step.type.toUpperCase()}:</strong> ${step.desc || 'Không có mô tả'}</span> </div> <button class="delete-step-btn" title="Xóa bước này" style="background: none; border: none; color: #e74c3c; font-weight: bold; cursor: pointer; padding: 0;">✖</button> `; li.addEventListener('click', (e) => { // If delete button was clicked, don't enter edit mode if (e.target.classList.contains('delete-step-btn')) return; resetStepEditorForm(); // Reset any other editing state li.classList.add('editing'); // Mark this item as currently being edited const stepData = JSON.parse(li.dataset.step); document.getElementById('step_type_select').value = stepData.type; document.getElementById('step_desc_input').value = stepData.desc || ''; document.getElementById('step_selector_input').value = stepData.selector || ''; document.getElementById('step_shadow_input').value = stepData.shadowSelector || ''; document.getElementById('step_value_input').value = stepData.type === 'delay' ? stepData.delay : (stepData.value || ''); const addBtn = document.getElementById('add_step_btn'); addBtn.textContent = `Cập nhật bước ${index + 1}`; addBtn.dataset.editingIndex = index; // Store the index of the step being edited }); li.querySelector('.delete-step-btn').onclick = () => { li.remove(); updateStepNumbers(); // Re-index steps after deletion resetStepEditorForm(); // Clear editor form if the deleted step was being edited }; // Drag and drop event listeners li.addEventListener('dragstart', (e) => { draggedItem = e.target; e.target.classList.add('dragging'); // Add class for visual feedback e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', index); // Set data for Firefox setTimeout(() => e.target.style.opacity = '0.5', 0); // Hide the original element slightly }); li.addEventListener('dragend', (e) => { e.target.classList.remove('dragging'); e.target.style.opacity = '1'; draggedItem = null; updateStepNumbers(); // Update numbers and data-index after drop }); li.addEventListener('dragover', (e) => { e.preventDefault(); // Allow drop const afterElement = getDragAfterElement(list, e.clientY); if (draggedItem && draggedItem !== e.target) { // Ensure it's not dragging over itself if (afterElement == null) { list.appendChild(draggedItem); } else { list.insertBefore(draggedItem, afterElement); } } }); list.appendChild(li); } function updateStepNumbers() { const list = document.getElementById('workflow_steps_list'); const currentEditingIndex = document.getElementById('add_step_btn').dataset.editingIndex; let newEditingIndex = -1; Array.from(list.children).forEach((li, i) => { li.dataset.index = i; li.querySelector('.step-number').textContent = `${i + 1}.`; const stepData = JSON.parse(li.dataset.step); // Re-render text content in case the type or description was changed on a dragged item li.querySelector('span:not(.step-number)').innerHTML = `<strong>${stepData.type.toUpperCase()}:</strong> ${stepData.desc || 'Không có mô tả'}`; if (li.classList.contains('editing')) { newEditingIndex = i; } }); // Update the 'Add/Update Step' button's text if the edited item moved if (newEditingIndex !== -1) { const addBtn = document.getElementById('add_step_btn'); addBtn.textContent = `Cập nhật bước ${newEditingIndex + 1}`; addBtn.dataset.editingIndex = newEditingIndex; // Update the stored index } } function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('li:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } function addStepToEditor() { const addBtn = document.getElementById('add_step_btn'); const editingIndex = addBtn.dataset.editingIndex; // This will be undefined if adding a new step const stepData = { type: document.getElementById('step_type_select').value, desc: document.getElementById('step_desc_input').value.trim() }; const selector = document.getElementById('step_selector_input').value.trim(); const shadowSelector = document.getElementById('step_shadow_input').value.trim(); const valueOrDelay = document.getElementById('step_value_input').value.trim(); if (selector) stepData.selector = selector; if (shadowSelector) stepData.shadowSelector = shadowSelector; if (stepData.type === 'delay') { stepData.delay = parseInt(valueOrDelay, 10) || 100; // Default to 100ms } else if (stepData.type === 'input') { if (valueOrDelay) stepData.value = valueOrDelay; else { alert("Vui lòng nhập giá trị cho bước Input."); return; } } else if (stepData.type === 'log') { // Log step doesn't require selector or value beyond description stepData.value = valueOrDelay; // Can use value for log message if desired delete stepData.selector; delete stepData.shadowSelector; } // Basic validation if (!stepData.desc) { alert("Vui lòng nhập mô tả ngắn cho bước này."); return; } if ((stepData.type === 'click' || stepData.type === 'input') && !stepData.selector) { alert("Vui lòng nhập Selector (JS Path)."); return; } if (editingIndex !== undefined && editingIndex !== '') { // If editing an existing step const list = document.getElementById('workflow_steps_list'); const liToUpdate = list.querySelector(`li[data-index='${editingIndex}']`); if (liToUpdate) { liToUpdate.dataset.step = JSON.stringify(stepData); // Update stored data liToUpdate.querySelector('span:not(.step-number)').innerHTML = `<strong>${stepData.type.toUpperCase()}:</strong> ${stepData.desc || 'Không có mô tả'}`; } } else { // Adding a new step const list = document.getElementById('workflow_steps_list'); renderStepInEditor(stepData, list.children.length); // Add to the end } resetStepEditorForm(); } function saveWorkflowFromEditor() { const name = document.getElementById('workflow_name_input').value.trim(); if (!name) { alert("Vui lòng nhập tên cho workflow."); return; } const steps = []; document.querySelectorAll('#workflow_steps_list li').forEach(li => { steps.push(JSON.parse(li.dataset.step)); }); if (steps.length === 0) { alert("Workflow phải có ít nhất một bước."); return; } let id = document.getElementById('editing_workflow_id').value; if (!id) { // Generate a unique ID for new workflows id = `custom_${Date.now()}`; } allWorkflows[id] = { name, steps }; saveWorkflows(); populateWorkflowSelector(); document.getElementById('workflow_select').value = id; // Select the newly saved workflow closeWorkflowEditor(); log(`Đã lưu workflow "${name}"`, 'success'); } // Initialize the script bootstrap(); })();