|
117 | 117 | } |
118 | 118 |
|
119 | 119 | // ===== URL Parser ===== |
120 | | - function parseProxyURL(raw) { |
| 120 | + function extractLines(raw) { |
| 121 | + if (!raw) return []; |
| 122 | + return raw |
| 123 | + .split(/[\r\n]+/) |
| 124 | + .map(line => line.trim()) |
| 125 | + .filter(line => line.length > 0); |
| 126 | + } |
| 127 | + |
| 128 | + function parseProxyURLSingle(raw) { |
121 | 129 | const url = raw.trim(); |
122 | 130 | if (!url) return null; |
123 | 131 |
|
|
138 | 146 | return { error: 'Unknown protocol. Supported: vless, vmess, trojan, ss, socks, http' }; |
139 | 147 | } |
140 | 148 |
|
| 149 | + function parseProxyURL(raw) { |
| 150 | + const lines = extractLines(raw); |
| 151 | + if (lines.length === 0) return null; |
| 152 | + if (lines.length === 1) return parseProxyURLSingle(lines[0]); |
| 153 | + |
| 154 | + const parsedList = lines.map(line => parseProxyURLSingle(line)); |
| 155 | + const validList = parsedList.filter(p => p && !p.error); |
| 156 | + if (validList.length > 0) { |
| 157 | + return validList; |
| 158 | + } |
| 159 | + return parsedList[0] || { error: 'Unknown protocol. Supported: vless, vmess, trojan, ss, socks, http' }; |
| 160 | + } |
| 161 | + |
141 | 162 | // ===== SSH Parser (reads from form fields) ===== |
142 | 163 | function parseSSH(configNum) { |
143 | 164 | const server = document.getElementById(`ssh-server${configNum}`).value.trim(); |
|
426 | 447 |
|
427 | 448 | function onEnhancerInput() { |
428 | 449 | const val = enhancerInput.value.trim(); |
429 | | - let parsed = null; |
430 | | - let unsupported = false; |
431 | | - if (val) { |
432 | | - parsed = parseProxyURL(val); |
433 | | - if (parsed && parsed.error) { |
434 | | - parsed = null; |
435 | | - } |
436 | | - if (parsed && parsed.protocol !== 'vless' && parsed.protocol !== 'trojan') { |
437 | | - unsupported = true; |
438 | | - parsed = null; |
439 | | - } |
| 450 | + const lines = extractLines(val); |
| 451 | + let parsedList = []; |
| 452 | + let unsupportedCount = 0; |
| 453 | + let invalidCount = 0; |
| 454 | + |
| 455 | + if (lines.length > 0) { |
| 456 | + lines.forEach(line => { |
| 457 | + const p = parseProxyURLSingle(line); |
| 458 | + if (p && !p.error) { |
| 459 | + if (p.protocol === 'vless' || p.protocol === 'trojan') { |
| 460 | + parsedList.push(p); |
| 461 | + } else { |
| 462 | + unsupportedCount++; |
| 463 | + } |
| 464 | + } else { |
| 465 | + invalidCount++; |
| 466 | + } |
| 467 | + }); |
440 | 468 | } |
441 | 469 |
|
442 | | - // Auto-fill the server field from the URL, unless the user typed a custom value |
443 | | - if (parsed) { |
| 470 | + // Auto-fill the server field from the URL — only for a single config. |
| 471 | + // With multiple configs, auto-filling from the first URL would silently |
| 472 | + // override every config's server with the first one's. A user-typed |
| 473 | + // custom value is kept: it acts as a deliberate override for all configs. |
| 474 | + if (parsedList.length === 1) { |
| 475 | + const firstServer = parsedList[0].server || ''; |
444 | 476 | const current = enhancerServer.value.trim(); |
445 | | - if (!current || current === lastAutoServer || current === parsed.server) { |
446 | | - enhancerServer.value = parsed.server || ''; |
447 | | - lastAutoServer = parsed.server || ''; |
| 477 | + if (!current || current === lastAutoServer || current === firstServer) { |
| 478 | + enhancerServer.value = firstServer; |
| 479 | + lastAutoServer = firstServer; |
| 480 | + } |
| 481 | + } else if (parsedList.length > 1) { |
| 482 | + const current = enhancerServer.value.trim(); |
| 483 | + if (!current || current === lastAutoServer) { |
| 484 | + enhancerServer.value = ''; |
| 485 | + lastAutoServer = ''; |
448 | 486 | } |
449 | 487 | } else if (!enhancerServer.value.trim()) { |
450 | 488 | lastAutoServer = ''; |
451 | 489 | } |
452 | 490 |
|
453 | | - renderParsedInfo( |
454 | | - parsed |
455 | | - ? parsed |
456 | | - : (val ? { error: unsupported ? 'Only VLESS and Trojan URLs are supported' : 'Failed to parse URL' } : null), |
457 | | - enhancerParsed |
458 | | - ); |
459 | | - updateProtocolTag(enhancerProtocolTag, parsed); |
| 491 | + if (lines.length > 0) { |
| 492 | + if (parsedList.length > 0) { |
| 493 | + renderParsedInfo(parsedList.length === 1 ? parsedList[0] : parsedList, enhancerParsed); |
| 494 | + const protocols = Array.from(new Set(parsedList.map(p => p.protocol))); |
| 495 | + if (protocols.length === 1) { |
| 496 | + updateProtocolTag(enhancerProtocolTag, parsedList[0]); |
| 497 | + } else { |
| 498 | + enhancerProtocolTag.textContent = 'MULTI'; |
| 499 | + enhancerProtocolTag.classList.add('active'); |
| 500 | + enhancerProtocolTag.style.color = '#7c5cff'; |
| 501 | + enhancerProtocolTag.style.borderColor = '#7c5cff4d'; |
| 502 | + enhancerProtocolTag.style.background = '#7c5cff1a'; |
| 503 | + } |
| 504 | + } else { |
| 505 | + renderParsedInfo({ |
| 506 | + error: unsupportedCount > 0 |
| 507 | + ? 'Only VLESS and Trojan URLs are supported' |
| 508 | + : 'Failed to parse URL' |
| 509 | + }, enhancerParsed); |
| 510 | + updateProtocolTag(enhancerProtocolTag, null); |
| 511 | + } |
| 512 | + } else { |
| 513 | + renderParsedInfo(null, enhancerParsed); |
| 514 | + updateProtocolTag(enhancerProtocolTag, null); |
| 515 | + } |
460 | 516 |
|
461 | 517 | enhancerCard.classList.remove('valid', 'invalid'); |
462 | | - if (val && parsed) { |
| 518 | + if (lines.length > 0 && parsedList.length > 0) { |
463 | 519 | enhancerCard.classList.add('valid'); |
464 | | - } else if (val) { |
| 520 | + } else if (lines.length > 0) { |
465 | 521 | enhancerCard.classList.add('invalid'); |
466 | 522 | } |
467 | 523 |
|
468 | | - btnEnhance.disabled = !parsed; |
469 | | - enhanceHint.textContent = parsed |
470 | | - ? 'Ready to enhance!' |
471 | | - : 'Paste a VLESS or Trojan URL above to enable'; |
472 | | - enhanceHint.style.color = parsed ? '#4cdf86' : ''; |
| 524 | + const count = parsedList.length; |
| 525 | + btnEnhance.disabled = count === 0; |
| 526 | + btnEnhance.innerHTML = `<span class="btn-icon">✨</span> Enhance ${count > 1 ? count + ' URLs' : 'URL'}`; |
| 527 | + enhanceHint.textContent = count > 0 |
| 528 | + ? `Ready to enhance ${count} URL${count > 1 ? 's' : ''}!` |
| 529 | + : 'Paste VLESS or Trojan URL(s) above to enable'; |
| 530 | + enhanceHint.style.color = count > 0 ? '#4cdf86' : ''; |
473 | 531 | } |
474 | 532 |
|
475 | 533 | function onEnhance() { |
476 | | - const result = enhanceURL(enhancerInput.value); |
477 | | - if (!result || result.error) { |
| 534 | + const val = enhancerInput.value.trim(); |
| 535 | + const lines = extractLines(val); |
| 536 | + if (lines.length === 0) { |
| 537 | + enhancerOutputSection.style.display = 'none'; |
| 538 | + return; |
| 539 | + } |
| 540 | + |
| 541 | + const enhancedUrls = []; |
| 542 | + lines.forEach(line => { |
| 543 | + const p = parseProxyURLSingle(line); |
| 544 | + if (p && !p.error && (p.protocol === 'vless' || p.protocol === 'trojan')) { |
| 545 | + const res = enhanceURL(line); |
| 546 | + if (res && res.url) { |
| 547 | + enhancedUrls.push(res.url); |
| 548 | + } |
| 549 | + } |
| 550 | + }); |
| 551 | + |
| 552 | + if (enhancedUrls.length === 0) { |
478 | 553 | enhancerOutputSection.style.display = 'none'; |
479 | 554 | return; |
480 | 555 | } |
481 | 556 |
|
482 | | - const parsed = parseProxyURL(enhancerInput.value); |
483 | | - const remark = parsed && parsed.remark |
484 | | - ? `✨ ${parsed.protocol.toUpperCase()} ${parsed.server}:${parsed.port} | enhanced` |
485 | | - : '✨ Enhanced'; |
| 557 | + const count = enhancedUrls.length; |
| 558 | + const firstParsed = parseProxyURLSingle(lines[0]); |
| 559 | + const remark = count === 1 |
| 560 | + ? (firstParsed && firstParsed.remark |
| 561 | + ? `✨ ${firstParsed.protocol.toUpperCase()} ${firstParsed.server}:${firstParsed.port} | enhanced` |
| 562 | + : '✨ Enhanced') |
| 563 | + : `✨ Enhanced ${count} URLs`; |
486 | 564 |
|
487 | 565 | enhancerOutputRemark.textContent = remark; |
488 | | - enhancerOutputUrl.textContent = result.url; |
| 566 | + enhancerOutputUrl.textContent = enhancedUrls.join('\n\n'); |
489 | 567 | enhancerOutputSection.style.display = 'block'; |
490 | 568 | enhancerOutputSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
491 | 569 | } |
|
1379 | 1457 | return; |
1380 | 1458 | } |
1381 | 1459 |
|
| 1460 | + if (Array.isArray(params)) { |
| 1461 | + if (params.length === 0) { |
| 1462 | + container.innerHTML = ''; |
| 1463 | + return; |
| 1464 | + } |
| 1465 | + if (params.length === 1) { |
| 1466 | + renderParsedInfo(params[0], container); |
| 1467 | + return; |
| 1468 | + } |
| 1469 | + |
| 1470 | + const rows = []; |
| 1471 | + rows.push(['Loaded Configs', `Found ${params.length} URLs`]); |
| 1472 | + params.forEach((cfg, i) => { |
| 1473 | + const label = `#${i + 1} ${cfg.protocol ? cfg.protocol.toUpperCase() : ''}`; |
| 1474 | + const val = `${cfg.server || ''}:${cfg.port || ''}${cfg.remark ? ' (' + truncateStr(cfg.remark, 30) + ')' : ''}`; |
| 1475 | + rows.push([label, val]); |
| 1476 | + }); |
| 1477 | + |
| 1478 | + container.innerHTML = rows.map(([label, value]) => |
| 1479 | + `<div class="info-row"> |
| 1480 | + <span class="info-label">${label}</span> |
| 1481 | + <span class="info-value">${escapeHtml(String(value))}</span> |
| 1482 | + </div>` |
| 1483 | + ).join(''); |
| 1484 | + return; |
| 1485 | + } |
| 1486 | + |
1382 | 1487 | const rows = []; |
1383 | 1488 | rows.push(['Protocol', params.protocol.toUpperCase()]); |
1384 | 1489 | rows.push(['Server', params.server]); |
|
1477 | 1582 | parsed = val ? parseProxyURL(val) : null; |
1478 | 1583 | } |
1479 | 1584 |
|
| 1585 | + let singleConfig = null; |
| 1586 | + if (Array.isArray(parsed)) { |
| 1587 | + singleConfig = parsed[0]; |
| 1588 | + } else if (parsed && !parsed.error) { |
| 1589 | + singleConfig = parsed; |
| 1590 | + } |
| 1591 | + |
1480 | 1592 | if (isConfig1) { |
1481 | | - parsedConfig1 = parsed && !parsed.error ? parsed : null; |
| 1593 | + parsedConfig1 = singleConfig; |
1482 | 1594 | } else { |
1483 | | - parsedConfig2 = parsed && !parsed.error ? parsed : null; |
| 1595 | + parsedConfig2 = singleConfig; |
1484 | 1596 | } |
1485 | 1597 |
|
1486 | 1598 | renderParsedInfo(parsed, parsedContainer); |
1487 | | - updateProtocolTag(protocolTag, parsed); |
| 1599 | + updateProtocolTag(protocolTag, singleConfig || (parsed && !Array.isArray(parsed) ? parsed : null)); |
1488 | 1600 |
|
1489 | 1601 | // For SSH mode, we also consider the card valid if server is filled |
1490 | 1602 | card.classList.remove('valid', 'invalid'); |
|
1500 | 1612 | } |
1501 | 1613 | } else { |
1502 | 1614 | const val = inputEl.value.trim(); |
1503 | | - if (val && parsed) { |
1504 | | - card.classList.add(parsed.error ? 'invalid' : 'valid'); |
| 1615 | + if (val && (singleConfig || (parsed && !parsed.error))) { |
| 1616 | + card.classList.add('valid'); |
| 1617 | + } else if (val) { |
| 1618 | + card.classList.add('invalid'); |
1505 | 1619 | } |
1506 | 1620 | } |
1507 | 1621 |
|
|
0 commit comments