From c6c367faf0975939dcacda1de8187eee14b5811d Mon Sep 17 00:00:00 2001 From: skylon Date: Sat, 17 Jan 2026 15:10:50 +0800 Subject: [PATCH 1/2] feat: Implement route override send and UI --- css/panel.css | 42 ++++++++++- js/main.js | 143 ++++++++++++++++++++++++++++++++++++++ js/network/handler.js | 23 ++++-- js/network/url-mapping.js | 40 +++++++++++ js/ui/main-ui.js | 1 + panel.html | 34 ++++++++- 6 files changed, 275 insertions(+), 8 deletions(-) create mode 100644 js/network/url-mapping.js diff --git a/css/panel.css b/css/panel.css index 5c64290..a497d45 100644 --- a/css/panel.css +++ b/css/panel.css @@ -1410,6 +1410,37 @@ body.theme-transitioning img { box-sizing: border-box; } +.url-mapping-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.url-mapping-row { + display: flex; + align-items: center; + gap: 8px; +} + +.url-mapping-row .form-control { + width: auto; + flex: 1; +} + +.url-mapping-arrow { + color: var(--text-color); + opacity: 0.6; +} + +.url-mapping-remove { + width: 24px; + height: 24px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + .form-row { display: flex; gap: 10px; @@ -1708,11 +1739,13 @@ tr.selected { } /* Custom Tooltip */ -.icon-btn[data-tooltip] { +.icon-btn[data-tooltip], +.secondary-btn[data-tooltip] { position: relative; } -.icon-btn[data-tooltip]:hover::after { +.icon-btn[data-tooltip]:hover::after, +.secondary-btn[data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; bottom: 100%; @@ -2176,6 +2209,10 @@ pre { align-self: flex-start; } +.send-btn-local { + margin-left: 8px; +} + .status-badge { padding: 4px 10px; border-radius: 4px; @@ -5316,4 +5353,3 @@ pre { transform: translateX(0); } } - diff --git a/js/main.js b/js/main.js index 7a9eef7..261d89e 100644 --- a/js/main.js +++ b/js/main.js @@ -20,6 +20,7 @@ import { initExtractorUI } from './features/extractors/index.js'; import { setupAIFeatures } from './features/ai/index.js'; import { setupLLMChat } from './features/llm-chat/index.js'; import { handleSendRequest } from './network/handler.js'; +import { getUrlMappings, saveUrlMappings } from './network/url-mapping.js'; import { initSearch } from './search/index.js'; // UI Modules @@ -119,6 +120,9 @@ document.addEventListener('DOMContentLoaded', () => { if (elements.sendBtn) { elements.sendBtn.addEventListener('click', handleSendRequest); } + if (elements.sendBtnLocal) { + elements.sendBtnLocal.addEventListener('click', () => handleSendRequest({ useUrlMapping: true })); + } // Remove Duplicates Toggle if (elements.removeDuplicatesBtn) { @@ -216,6 +220,145 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // URL Mapping + const urlMappingBtn = document.getElementById('url-mapping-btn'); + const urlMappingModal = document.getElementById('url-mapping-modal'); + const urlMappingList = document.getElementById('url-mapping-list'); + const addUrlMappingBtn = document.getElementById('add-url-mapping-btn'); + const saveUrlMappingBtn = document.getElementById('save-url-mapping-btn'); + const urlMappingBulkInput = document.getElementById('url-mapping-bulk-input'); + const applyUrlMappingBulkBtn = document.getElementById('apply-url-mapping-bulk-btn'); + + function createUrlMappingRow(mapping = { from: '', to: '' }) { + const row = document.createElement('div'); + row.className = 'url-mapping-row'; + row.innerHTML = ` + + + + + `; + + const fromInput = row.querySelector('.url-mapping-from'); + const toInput = row.querySelector('.url-mapping-to'); + const removeBtn = row.querySelector('.url-mapping-remove'); + + if (fromInput) fromInput.value = mapping.from || ''; + if (toInput) toInput.value = mapping.to || ''; + + if (removeBtn) { + removeBtn.addEventListener('click', () => { + row.remove(); + }); + } + + return row; + } + + function loadUrlMappingsIntoModal() { + if (!urlMappingList) return; + urlMappingList.innerHTML = ''; + const mappings = getUrlMappings(); + if (mappings.length === 0) { + urlMappingList.appendChild(createUrlMappingRow()); + return; + } + mappings.forEach(mapping => { + urlMappingList.appendChild(createUrlMappingRow(mapping)); + }); + } + + if (urlMappingBtn && urlMappingModal) { + urlMappingBtn.addEventListener('click', () => { + loadUrlMappingsIntoModal(); + if (urlMappingBulkInput) urlMappingBulkInput.value = ''; + urlMappingModal.style.display = 'block'; + }); + } + + if (addUrlMappingBtn) { + addUrlMappingBtn.addEventListener('click', () => { + if (urlMappingList) { + urlMappingList.appendChild(createUrlMappingRow()); + } + }); + } + + if (applyUrlMappingBulkBtn) { + applyUrlMappingBulkBtn.addEventListener('click', () => { + if (!urlMappingList || !urlMappingBulkInput) return; + const text = urlMappingBulkInput.value || ''; + const entries = parseBulkOverrides(text); + if (entries.length === 0) { + alert('No valid overrides found.'); + return; + } + + const rowMap = new Map(); + urlMappingList.querySelectorAll('.url-mapping-row').forEach(row => { + const fromInput = row.querySelector('.url-mapping-from'); + if (fromInput && fromInput.value.trim()) { + rowMap.set(fromInput.value.trim(), row); + } + }); + + entries.forEach(({ from, to }) => { + const existingRow = rowMap.get(from); + if (existingRow) { + const toInput = existingRow.querySelector('.url-mapping-to'); + if (toInput) toInput.value = to; + } else { + const newRow = createUrlMappingRow({ from, to }); + urlMappingList.appendChild(newRow); + rowMap.set(from, newRow); + } + }); + + urlMappingBulkInput.value = ''; + }); + } + + if (saveUrlMappingBtn && urlMappingModal) { + saveUrlMappingBtn.addEventListener('click', () => { + if (!urlMappingList) return; + const rows = Array.from(urlMappingList.querySelectorAll('.url-mapping-row')); + const mappings = rows.map(row => { + const fromInput = row.querySelector('.url-mapping-from'); + const toInput = row.querySelector('.url-mapping-to'); + return { + from: fromInput ? fromInput.value.trim() : '', + to: toInput ? toInput.value.trim() : '' + }; + }); + + if (rows.length === 0 || mappings.some(mapping => !mapping.from || !mapping.to)) { + alert('Both fields are required for each route override.'); + return; + } + + saveUrlMappings(mappings); + alert('Route overrides saved.'); + urlMappingModal.style.display = 'none'; + }); + } + + function parseBulkOverrides(text) { + return text + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + .map(line => { + const separator = '=>'; + const separatorIndex = line.indexOf(separator); + if (separatorIndex === -1) return null; + const from = line.slice(0, separatorIndex).trim(); + const to = line.slice(separatorIndex + separator.length).trim(); + if (!from || !to) return null; + return { from, to }; + }) + .filter(Boolean); + } + // History Navigation // Undo/Redo buttons if (elements.undoBtn) { diff --git a/js/network/handler.js b/js/network/handler.js index 70fb9fd..fc96140 100644 --- a/js/network/handler.js +++ b/js/network/handler.js @@ -4,6 +4,7 @@ import { elements } from '../ui/main-ui.js'; import { events, EVENT_NAMES } from '../core/events.js'; import { parseRequest } from './capture.js'; import { sendRequest } from './request-sender.js'; +import { applyUrlMappings, getUrlMappings } from './url-mapping.js'; import { formatRawResponse, getStatusClass } from './response-parser.js'; import { formatBytes } from '../core/utils/format.js'; import { renderDiff } from '../core/utils/misc.js'; @@ -12,7 +13,7 @@ import { generateHexView } from '../ui/hex-view.js' import { generateJsonView } from '../ui/json-view.js' import { saveEditorState } from '../ui/request-editor.js'; -export async function handleSendRequest() { +export async function handleSendRequest({ useUrlMapping = false } = {}) { const rawContent = elements.rawRequestInput.innerText; const useHttps = elements.useHttpsCheckbox.checked; @@ -30,13 +31,27 @@ export async function handleSendRequest() { try { const { url, options, method, filteredHeaders, bodyText } = parseRequest(rawContent, useHttps); + let targetUrl = url; + + if (useUrlMapping) { + const mappings = getUrlMappings(); + if (mappings.length === 0) { + alert('No route overrides configured. Add one in Route Override.'); + return; + } + targetUrl = applyUrlMappings(url, mappings); + if (targetUrl === url) { + alert('No route override matched this request.'); + return; + } + } elements.resStatus.textContent = 'Sending...'; elements.resStatus.className = 'status-badge'; - console.log('Sending request to:', url); + console.log('Sending request to:', targetUrl); - const result = await sendRequest(url, options); + const result = await sendRequest(targetUrl, options); elements.resTime.textContent = `${result.duration}ms`; elements.resSize.textContent = formatBytes(result.size); @@ -115,7 +130,7 @@ export async function handleSendRequest() { origins: [''] }, (granted) => { if (granted) { - handleSendRequest(); + handleSendRequest({ useUrlMapping }); } else { elements.rawResponseDisplay.innerHTML += '

Permission denied.

'; } diff --git a/js/network/url-mapping.js b/js/network/url-mapping.js new file mode 100644 index 0000000..8645c53 --- /dev/null +++ b/js/network/url-mapping.js @@ -0,0 +1,40 @@ +const STORAGE_KEY = 'rep_url_mappings'; + + +function normalizeMappings(mappings) { + if (!Array.isArray(mappings)) return []; + return mappings + .map(mapping => ({ + from: typeof mapping?.from === 'string' ? mapping.from.trim() : '', + to: typeof mapping?.to === 'string' ? mapping.to.trim() : '' + })) + .filter(mapping => mapping.from && mapping.to); +} + +export function getUrlMappings() { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === null) return []; + + try { + const parsed = JSON.parse(stored); + return normalizeMappings(parsed); + } catch (err) { + localStorage.removeItem(STORAGE_KEY); + return []; + } +} + +export function saveUrlMappings(mappings) { + const normalized = normalizeMappings(mappings); + localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized)); +} + +export function applyUrlMappings(url, mappings = getUrlMappings()) { + for (const mapping of mappings) { + if (!mapping.from || !mapping.to) continue; + if (url.startsWith(mapping.from)) { + return mapping.to + url.slice(mapping.from.length); + } + } + return url; +} diff --git a/js/ui/main-ui.js b/js/ui/main-ui.js index d65725e..1a77eb5 100644 --- a/js/ui/main-ui.js +++ b/js/ui/main-ui.js @@ -16,6 +16,7 @@ export function initUI() { elements.rawRequestInput = document.getElementById('raw-request-input'); elements.useHttpsCheckbox = document.getElementById('use-https'); elements.sendBtn = document.getElementById('send-btn'); + elements.sendBtnLocal = document.getElementById('send-btn-local'); elements.rawResponseDisplay = document.getElementById('raw-response-display'); elements.rawResponseText = document.getElementById('raw-response-text'); elements.hexResponseDisplay = document.getElementById('res-hex-display'); diff --git a/panel.html b/panel.html index 92ae0d2..0bdcb59 100644 --- a/panel.html +++ b/panel.html @@ -87,6 +87,12 @@ Settings + +
@@ -715,6 +722,31 @@

Settings

+ + +