Primeira versao
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Waiting for focus…</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #0d0d0f;
|
||||||
|
--accent: #7fff6e;
|
||||||
|
--muted: #6b6b7a;
|
||||||
|
--text: #e8e8ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
gap: 24px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 2.5px solid transparent;
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-right-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label span {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dest {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 480px;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #2a2a32;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-now {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-now:hover { color: var(--accent); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ring"></div>
|
||||||
|
<p class="label">Waiting for <span>focus</span> to load</p>
|
||||||
|
<p class="dest" id="dest"></p>
|
||||||
|
<button class="go-now" id="goNow">Load now →</button>
|
||||||
|
|
||||||
|
<script src="loader.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
(() => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const destUrl = params.get('url');
|
||||||
|
|
||||||
|
if (!destUrl) {
|
||||||
|
document.querySelector('.label').textContent = 'No URL provided.';
|
||||||
|
document.getElementById('dest').textContent = '';
|
||||||
|
document.getElementById('goNow').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('dest').textContent = destUrl;
|
||||||
|
document.title = '⏳ ' + destUrl;
|
||||||
|
|
||||||
|
let navigated = false;
|
||||||
|
|
||||||
|
function navigate() {
|
||||||
|
if (navigated) return;
|
||||||
|
navigated = true;
|
||||||
|
location.replace(destUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Load now" button — immediate bypass
|
||||||
|
document.getElementById('goNow').addEventListener('click', navigate);
|
||||||
|
|
||||||
|
// Navigate when the tab becomes visible (user switches to it)
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
setTimeout(navigate, 80);
|
||||||
|
} else {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') navigate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "URL Launcher",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Paste a list of URLs and open them all as tabs, with optional lazy-load on focus.",
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "URL Launcher"
|
||||||
|
},
|
||||||
|
"permissions": ["tabs"],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["loader.html", "loader.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+404
@@ -0,0 +1,404 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>URL Launcher</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Syne:wght@400;700;800&display=swap');
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #0d0d0f;
|
||||||
|
--surface: #16161a;
|
||||||
|
--border: #2a2a32;
|
||||||
|
--accent: #7fff6e;
|
||||||
|
--accent-dim:#3a7a33;
|
||||||
|
--text: #e8e8ec;
|
||||||
|
--muted: #6b6b7a;
|
||||||
|
--danger: #ff6b6b;
|
||||||
|
--radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 400px;
|
||||||
|
min-height: 300px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 18px 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Body ── */
|
||||||
|
.body {
|
||||||
|
padding: 16px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Textarea ── */
|
||||||
|
.textarea-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 10px 12px;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
caret-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
border-color: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-count {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 10px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-count.has-urls {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Options row ── */
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1.5px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--surface);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"]:checked + .custom-checkbox {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox svg {
|
||||||
|
display: none;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"]:checked + .custom-checkbox svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label:hover .checkbox-text {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Button ── */
|
||||||
|
.launch-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 9px 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: opacity 0.15s, transform 0.1s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launch-btn:hover:not(:disabled) {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launch-btn:active:not(:disabled) {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.launch-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launch-btn svg {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
fill: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status bar ── */
|
||||||
|
.status {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
min-height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success { color: var(--accent); }
|
||||||
|
.status.error { color: var(--danger); }
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toolbar above textarea ── */
|
||||||
|
.textarea-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-label {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extract-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 4px 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extract-btn svg {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: stroke 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extract-btn:hover {
|
||||||
|
border-color: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(127,255,110,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extract-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extract-btn.flash {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(127,255,110,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Focus mode indicator ── */
|
||||||
|
.mode-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 100px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
background: rgba(127,255,110,0.08);
|
||||||
|
border: 1px solid var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.off {
|
||||||
|
background: rgba(107,107,122,0.12);
|
||||||
|
border-color: var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge::before {
|
||||||
|
content: '';
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<!-- Tab icon -->
|
||||||
|
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="1" y="4" width="14" height="10" rx="1.5"/>
|
||||||
|
<rect x="1" y="1" width="6" height="4" rx="1"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="title">URL Launcher</span>
|
||||||
|
<span class="subtitle" id="modeBadge">
|
||||||
|
<span class="mode-badge" id="modeBadgeInner">lazy</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="textarea-toolbar">
|
||||||
|
<span class="textarea-label">URLs / Text</span>
|
||||||
|
<button class="extract-btn" id="extractBtn" title="Extract all URLs from pasted text">
|
||||||
|
<svg viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4.5 2.5H3a2 2 0 0 0 0 4h1.5"/>
|
||||||
|
<path d="M6.5 2.5H8a2 2 0 0 1 0 4H6.5"/>
|
||||||
|
<line x1="3.5" y1="4.5" x2="7.5" y2="4.5"/>
|
||||||
|
</svg>
|
||||||
|
Extract URLs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="textarea-wrap">
|
||||||
|
<textarea
|
||||||
|
id="urlInput"
|
||||||
|
placeholder="Paste any text here — emails, docs, HTML, markdown… Hit 'Extract URLs' to pull out links automatically. Or just type one URL per line: https://example.com https://github.com"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
></textarea>
|
||||||
|
<span class="url-count" id="urlCount">0 URLs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options">
|
||||||
|
<label class="checkbox-label" title="Each URL opens via an intermediate page and only navigates when you focus that tab">
|
||||||
|
<input type="checkbox" id="lazyLoad" checked />
|
||||||
|
<span class="custom-checkbox">
|
||||||
|
<!-- checkmark -->
|
||||||
|
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.5 5L4 7.5L8.5 2.5" stroke="#0d0d0f" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="checkbox-text">Wait for focus to load</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="launch-btn" id="openBtn" disabled>
|
||||||
|
<svg viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 6.5h9M7.5 3l4 3.5-4 3.5" stroke="#0d0d0f" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
Open URLs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
(() => {
|
||||||
|
const urlInput = document.getElementById('urlInput');
|
||||||
|
const lazyLoad = document.getElementById('lazyLoad');
|
||||||
|
const openBtn = document.getElementById('openBtn');
|
||||||
|
const extractBtn = document.getElementById('extractBtn');
|
||||||
|
const urlCount = document.getElementById('urlCount');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const modeBadge = document.getElementById('modeBadgeInner');
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────
|
||||||
|
function parseURLs(text) {
|
||||||
|
return text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map(l => l.trim())
|
||||||
|
.filter(l => {
|
||||||
|
if (!l) return false;
|
||||||
|
try { new URL(l); return true; } catch { return false; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts every URL from arbitrary text (emails, HTML, markdown, prose…)
|
||||||
|
function extractURLs(text) {
|
||||||
|
// Match http/https URLs, including those wrapped in <>, (), [], quotes, or at end of sentence
|
||||||
|
const raw = text.match(/https?:\/\/[^\s<>"')\]},;]+/gi) || [];
|
||||||
|
// Deduplicate while preserving order
|
||||||
|
const seen = new Set();
|
||||||
|
return raw
|
||||||
|
.map(u => {
|
||||||
|
// Strip common trailing punctuation that isn't part of the URL
|
||||||
|
return u.replace(/[.,;:!?)"'\]}>]+$/, '');
|
||||||
|
})
|
||||||
|
.filter(u => {
|
||||||
|
if (seen.has(u)) return false;
|
||||||
|
seen.add(u);
|
||||||
|
try { new URL(u); return true; } catch { return false; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(msg, type = '') {
|
||||||
|
status.className = 'status' + (type ? ' ' + type : '');
|
||||||
|
status.innerHTML = msg
|
||||||
|
? `<span class="status-dot"></span><span>${msg}</span>`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCount() {
|
||||||
|
const urls = parseURLs(urlInput.value);
|
||||||
|
const n = urls.length;
|
||||||
|
urlCount.textContent = n === 1 ? '1 URL' : `${n} URLs`;
|
||||||
|
urlCount.className = 'url-count' + (n > 0 ? ' has-urls' : '');
|
||||||
|
openBtn.disabled = n === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBadge() {
|
||||||
|
if (lazyLoad.checked) {
|
||||||
|
modeBadge.textContent = 'lazy';
|
||||||
|
modeBadge.className = 'mode-badge';
|
||||||
|
} else {
|
||||||
|
modeBadge.textContent = 'eager';
|
||||||
|
modeBadge.className = 'mode-badge off';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Event listeners ────────────────────────────────────────
|
||||||
|
urlInput.addEventListener('input', () => {
|
||||||
|
updateCount();
|
||||||
|
setStatus('');
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyLoad.addEventListener('change', updateBadge);
|
||||||
|
|
||||||
|
extractBtn.addEventListener('click', () => {
|
||||||
|
const urls = extractURLs(urlInput.value);
|
||||||
|
if (!urls.length) {
|
||||||
|
setStatus('No URLs found in text', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
urlInput.value = urls.join('\n');
|
||||||
|
updateCount();
|
||||||
|
setStatus(`Extracted ${urls.length} URL${urls.length !== 1 ? 's' : ''}`, 'success');
|
||||||
|
|
||||||
|
// Brief visual flash on the button
|
||||||
|
extractBtn.classList.add('flash');
|
||||||
|
setTimeout(() => extractBtn.classList.remove('flash'), 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
openBtn.addEventListener('click', async () => {
|
||||||
|
const urls = parseURLs(urlInput.value);
|
||||||
|
if (!urls.length) return;
|
||||||
|
|
||||||
|
openBtn.disabled = true;
|
||||||
|
setStatus('');
|
||||||
|
|
||||||
|
const useLazy = lazyLoad.checked;
|
||||||
|
let opened = 0;
|
||||||
|
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
let targetUrl = url;
|
||||||
|
|
||||||
|
if (useLazy) {
|
||||||
|
// Build the extension loader URL with the target as ?url=…
|
||||||
|
const loaderBase = chrome.runtime.getURL('loader.html');
|
||||||
|
targetUrl = `${loaderBase}?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.tabs.create({ url: targetUrl, active: false });
|
||||||
|
opened++;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to open', url, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCount();
|
||||||
|
if (opened === urls.length) {
|
||||||
|
setStatus(`Opened ${opened} tab${opened !== 1 ? 's' : ''}`, 'success');
|
||||||
|
} else {
|
||||||
|
setStatus(`Opened ${opened} / ${urls.length} tabs`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Init ───────────────────────────────────────────────────
|
||||||
|
updateCount();
|
||||||
|
updateBadge();
|
||||||
|
urlInput.focus();
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user