Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ <h1>Is This URL<br><span>Safe to Visit?</span></h1>
<span class="example-chip" onclick="fillExample('http://testsafebrowsing.appspot.com/s/malware.html')">malware test</span>
</div>

<!-- Screenshot Upload -->
<div class="screenshot-section" id="screenshotSection">
<div class="screenshot-divider">
<span>or</span>
</div>
<div
class="screenshot-dropzone"
id="screenshotDropzone"
role="button"
tabindex="0"
aria-label="Upload a screenshot to extract URLs"
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
ondrop="handleDrop(event)"
onclick="document.getElementById('screenshotInput').click()"
onkeydown="if(event.key==='Enter'||event.key===' ')document.getElementById('screenshotInput').click()"
>
<div class="dropzone-icon" aria-hidden="true">📸</div>
<div class="dropzone-text">Drag & drop a screenshot here</div>
<div class="dropzone-sub">or click to upload · JPG, PNG, WEBP</div>
<input
type="file"
id="screenshotInput"
accept="image/*"
style="display:none"
onchange="handleScreenshotUpload(event)"
aria-label="Choose screenshot file"
>
</div>
<div id="screenshotResult" class="screenshot-result" aria-live="polite"></div>
</div>

<div id="result"></div>
</div>

Expand Down Expand Up @@ -168,6 +200,7 @@ <h4>Resources</h4>
</footer>
</div>

<script src='https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js'></script>
<script src="script.js"></script>
</body>
</html>
119 changes: 119 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,126 @@ window.addEventListener('pageshow', (e) => {
if (main) { main.classList.remove('hidden'); main.style.opacity = '1'; }
}
});
// ─────────────────────────────
// SCREENSHOT URL DETECTION
// ─────────────────────────────

function handleDragOver(e) {
e.preventDefault();
document.getElementById('screenshotDropzone').classList.add('dragover');
}

function handleDragLeave(e) {
document.getElementById('screenshotDropzone').classList.remove('dragover');
}

function handleDrop(e) {
e.preventDefault();
document.getElementById('screenshotDropzone').classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
processScreenshot(file);
}
}

function handleScreenshotUpload(e) {
const file = e.target.files[0];
if (file) processScreenshot(file);
}

function processScreenshot(file) {
const resultEl = document.getElementById('screenshotResult');
resultEl.innerHTML = `<div class="screenshot-processing">🔍 Extracting URLs from screenshot...</div>`;

const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
// Draw image to canvas for Tesseract
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

// Use Tesseract.js to extract text
Tesseract.recognize(canvas, 'eng', {
logger: m => {
if (m.status === 'recognizing text') {
resultEl.innerHTML = `<div class="screenshot-processing">🔍 Scanning image... ${Math.round(m.progress * 100)}%</div>`;
}
}
}).then(({ data: { text } }) => {
const urls = extractUrlsFromText(text);
showScreenshotUrls(urls, text);
}).catch(err => {
resultEl.innerHTML = `<div class="screenshot-error">❌ OCR failed: ${err.message}</div>`;
});
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}

function extractUrlsFromText(text) {
// Match URLs including http, https, and bare domains
const urlRegex = /(?:https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,6}\b(?:[-a-zA-Z0-9@:%_+.~#?&/=]*)/g;
const matches = text.match(urlRegex) || [];

// Filter out noise — must have a dot and reasonable length
return [...new Set(
matches
.filter(u => u.includes('.') && u.length > 4 && u.length < 200)
.filter(u => !/^\d+\.\d+$/.test(u)) // exclude version numbers like 1.0
.map(u => u.trim().replace(/[.,;:'")\]>]+$/, '')) // strip trailing punctuation
)].slice(0, 10); // max 10 URLs
}

function showScreenshotUrls(urls, rawText) {
const resultEl = document.getElementById('screenshotResult');

if (urls.length === 0) {
resultEl.innerHTML = `
<div class="screenshot-no-urls">
<span aria-hidden="true">🔎</span>
No URLs detected in the screenshot. Try a clearer image.
</div>
`;
return;
}

resultEl.innerHTML = `
<div class="screenshot-urls-found" role="region" aria-label="URLs extracted from screenshot">
<div class="screenshot-urls-header">
<span aria-hidden="true">🔗</span>
${urls.length} URL${urls.length > 1 ? 's' : ''} found — click to scan
</div>
<div class="screenshot-urls-list" role="list">
${urls.map(url => `
<div class="screenshot-url-item" role="listitem">
<span class="screenshot-url-text">${url}</span>
<button
type="button"
class="screenshot-scan-btn"
onclick="scanExtractedUrl('${url.replace(/'/g, "\\'")}')"
aria-label="Scan ${url}"
>
Scan
</button>
</div>
`).join('')}
</div>
</div>
`;
}

function scanExtractedUrl(url) {
// Fill the URL input and trigger scan
const input = document.getElementById('urlInput');
input.value = url;
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
checkSecurity();
}
// ═══════════════════════════════════
// THEME TOGGLE
// ═══════════════════════════════════
Expand Down
136 changes: 136 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1295,3 +1295,139 @@ html.light-mode .result-url { background: rgba(0,0,0,0.06); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ── Screenshot URL Detection ── */
.screenshot-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0;
color: #475569;
font-size: 13px;
}

.screenshot-divider::before,
.screenshot-divider::after {
content: '';
flex: 1;
height: 1px;
background: rgba(255,255,255,0.07);
}

.screenshot-dropzone {
border: 2px dashed rgba(0,255,180,0.25);
border-radius: 12px;
padding: 28px 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
background: rgba(0,255,180,0.02);
}

.screenshot-dropzone:hover,
.screenshot-dropzone.dragover {
border-color: #00ffb4;
background: rgba(0,255,180,0.06);
}

.screenshot-dropzone:focus-visible {
outline: 2px solid #00ffb4;
outline-offset: 3px;
}

.dropzone-icon {
font-size: 28px;
margin-bottom: 8px;
}

.dropzone-text {
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #f1f5f9);
margin-bottom: 4px;
}

.dropzone-sub {
font-size: 12px;
color: #64748b;
}

.screenshot-result {
margin-top: 12px;
}

.screenshot-processing {
font-size: 13px;
color: #94a3b8;
padding: 10px;
text-align: center;
}

.screenshot-no-urls {
font-size: 13px;
color: #64748b;
padding: 10px;
text-align: center;
}

.screenshot-error {
font-size: 13px;
color: #f87171;
padding: 10px;
text-align: center;
}

.screenshot-urls-found {
background: var(--card-bg, #1e293b);
border: 1px solid rgba(255,255,255,0.07);
border-radius: 12px;
padding: 16px;
}

.screenshot-urls-header {
font-size: 13px;
font-weight: 600;
color: #00ffb4;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}

.screenshot-urls-list {
display: flex;
flex-direction: column;
gap: 8px;
}

.screenshot-url-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: rgba(255,255,255,0.04);
border-radius: 8px;
padding: 10px 12px;
}

.screenshot-url-text {
font-size: 12px;
color: #94a3b8;
word-break: break-all;
flex: 1;
}

.screenshot-scan-btn {
background: rgba(0,255,180,0.1);
border: 1px solid rgba(0,255,180,0.3);
color: #00ffb4;
padding: 5px 14px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s;
}

.screenshot-scan-btn:hover {
background: rgba(0,255,180,0.2);
}