Collect and view all images on any page with a two-finger upward swipe. Desktop users can press Ctrl+Shift+I.
// ==UserScript==
// @name Gesture Image Grabber
// @namespace GestureImageGrabber
// @version 1.0
// @description Collect and view all images on any page with a two-finger upward swipe. Desktop users can press Ctrl+Shift+I.
// @match *://*/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
/* =========================
CONFIG
========================= */
const CONFIG = {
// Gesture sensitivity
MIN_SWIPE_Y: 140,
MIN_DURATION: 80,
MAX_DURATION: 1000,
// Gesture validation
MIN_VERTICAL_RATIO: 1.25,
MIN_SIMILARITY: 0.72,
MAX_DISTANCE_CHANGE: 32,
MIN_MOVEMENT: 14,
// Image filtering
MIN_IMAGE_SIZE: 150,
MAX_RATIO: 5,
MIN_RATIO: 0.2
};
/* =========================
HELPERS
========================= */
const unique = arr => [...new Set(arr)];
function isHttp(url) {
return typeof url === 'string' && /^https?:\/\//i.test(url);
}
function safePush(set, url) {
if (isHttp(url)) set.add(url);
}
/* =========================
IMAGE COLLECTION
========================= */
function collectImages() {
const all = new Set();
// img tags
document.querySelectorAll('img').forEach(img => {
safePush(all, img.currentSrc || img.src);
if (img.srcset) {
img.srcset.split(',').forEach(s => {
const url = s.trim().split(' ')[0];
safePush(all, url);
});
}
});
// video posters
document.querySelectorAll('video').forEach(v => {
safePush(all, v.poster);
});
// linked preview images
document.querySelectorAll('a[href]').forEach(a => {
try {
const url = new URL(a.href);
for (const [key, value] of url.searchParams.entries()) {
if (
/img|image|media|photo|picture|thumbnail/i.test(key)
) {
safePush(all, value);
}
}
} catch (e) {}
});
// background images
document.querySelectorAll('[style*="background"], [class]').forEach(el => {
const bg = getComputedStyle(el).backgroundImage;
if (!bg || bg === 'none' || !bg.includes('url(')) return;
const matches = [...bg.matchAll(/url\(["']?(.*?)["']?\)/g)];
matches.forEach(m => {
safePush(all, m[1]);
});
});
return unique([...all]);
}
/* =========================
UI
========================= */
function createGrid() {
const g = document.createElement('div');
Object.assign(g.style, {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(140px,1fr))',
gap: '12px'
});
return g;
}
function createTitle(text) {
const d = document.createElement('div');
d.textContent = text;
Object.assign(d.style, {
margin: '36px 0 12px',
color: '#fff',
fontWeight: 'bold',
fontSize: '18px',
fontFamily: 'sans-serif'
});
return d;
}
function createDivider() {
const d = document.createElement('hr');
Object.assign(d.style, {
margin: '40px 0',
border: 'none',
borderTop: '1px solid #333'
});
return d;
}
function addImage(container, src) {
const a = document.createElement('a');
a.href = src;
a.target = '_blank';
Object.assign(a.style, {
display: 'block',
overflow: 'hidden',
borderRadius: '10px',
background: '#111'
});
const img = document.createElement('img');
img.src = src;
Object.assign(img.style, {
width: '100%',
height: '140px',
objectFit: 'cover',
display: 'block'
});
a.appendChild(img);
container.appendChild(a);
}
function classifyAndRender(urls, panel) {
const mainGrid = createGrid();
const junkGrid = createGrid();
const mainTitle = createTitle('Main Images (0)');
const junkTitle = createTitle('Background / Junk (0)');
panel.appendChild(mainTitle);
panel.appendChild(mainGrid);
panel.appendChild(createDivider());
panel.appendChild(junkTitle);
panel.appendChild(junkGrid);
let main = 0;
let junk = 0;
urls.forEach(src => {
const lower = src.toLowerCase();
const img = new Image();
img.onload = () => {
const w = img.naturalWidth;
const h = img.naturalHeight;
if (!w || !h) return;
const ratio = w / h;
const small =
w < CONFIG.MIN_IMAGE_SIZE ||
h < CONFIG.MIN_IMAGE_SIZE;
const weird =
ratio > CONFIG.MAX_RATIO ||
ratio < CONFIG.MIN_RATIO;
const ui =
/logo|icon|sprite|favicon|emoji|badge|avatar|ads|banner|thumb/i
.test(lower);
const vector =
lower.endsWith('.svg') ||
lower.endsWith('.ico');
if (small || weird || ui || vector) {
addImage(junkGrid, src);
junk++;
junkTitle.textContent = `Background / Junk (${junk})`;
} else {
addImage(mainGrid, src);
main++;
mainTitle.textContent = `Main Images (${main})`;
}
};
img.src = src;
});
}
function openUI(urls) {
if (!urls.length) {
alert('No images found on this page.');
return;
}
const existing = document.getElementById('img-panel');
if (existing) {
existing.remove();
}
const panel = document.createElement('div');
panel.id = 'img-panel';
Object.assign(panel.style, {
position: 'fixed',
inset: '0',
background: '#000',
zIndex: '999999999',
overflowY: 'auto',
padding: '14px'
});
const close = document.createElement('div');
close.textContent = '×';
Object.assign(close.style, {
position: 'fixed',
top: '8px',
right: '18px',
fontSize: '42px',
color: '#fff',
cursor: 'pointer',
zIndex: '2',
fontFamily: 'sans-serif'
});
close.onclick = () => panel.remove();
panel.appendChild(close);
document.body.appendChild(panel);
classifyAndRender(urls, panel);
}
/* =========================
GESTURE DETECTION
========================= */
let tracking = false;
let triggered = false;
let startTouches = null;
let startTime = 0;
document.addEventListener('touchstart', e => {
if (e.touches.length !== 2) {
tracking = false;
return;
}
tracking = true;
triggered = false;
startTime = Date.now();
startTouches = [...e.touches].map(t => ({
x: t.clientX,
y: t.clientY
}));
}, { passive: true });
document.addEventListener('touchmove', e => {
if (!tracking || triggered) return;
if (e.touches.length !== 2) {
tracking = false;
return;
}
const duration = Date.now() - startTime;
if (
duration < CONFIG.MIN_DURATION ||
duration > CONFIG.MAX_DURATION
) {
return;
}
const current = [...e.touches].map(t => ({
x: t.clientX,
y: t.clientY
}));
const startDist = Math.hypot(
startTouches[0].x - startTouches[1].x,
startTouches[0].y - startTouches[1].y
);
const currentDist = Math.hypot(
current[0].x - current[1].x,
current[0].y - current[1].y
);
const distChange = Math.abs(currentDist - startDist);
// reject pinch zoom gestures
if (distChange > CONFIG.MAX_DISTANCE_CHANGE) {
tracking = false;
return;
}
const v1 = {
dx: current[0].x - startTouches[0].x,
dy: current[0].y - startTouches[0].y
};
const v2 = {
dx: current[1].x - startTouches[1].x,
dy: current[1].y - startTouches[1].y
};
const mag1 = Math.hypot(v1.dx, v1.dy);
const mag2 = Math.hypot(v2.dx, v2.dy);
if (
mag1 < CONFIG.MIN_MOVEMENT ||
mag2 < CONFIG.MIN_MOVEMENT
) {
return;
}
// strong upward motion
const up1 = -v1.dy > CONFIG.MIN_SWIPE_Y;
const up2 = -v2.dy > CONFIG.MIN_SWIPE_Y;
// mostly vertical movement
const vertical1 =
Math.abs(v1.dy) >
Math.abs(v1.dx) * CONFIG.MIN_VERTICAL_RATIO;
const vertical2 =
Math.abs(v2.dy) >
Math.abs(v2.dx) * CONFIG.MIN_VERTICAL_RATIO;
// both fingers moving similarly
const dot =
v1.dx * v2.dx +
v1.dy * v2.dy;
const similarity = dot / (mag1 * mag2);
if (
up1 &&
up2 &&
vertical1 &&
vertical2 &&
similarity > CONFIG.MIN_SIMILARITY
) {
triggered = true;
tracking = false;
e.preventDefault();
openUI(collectImages());
}
}, { passive: false });
document.addEventListener('touchend', () => {
tracking = false;
triggered = false;
});
/* =========================
FOR DESKTOP USERS
========================= */
document.addEventListener('keydown', e => {
if (
e.ctrlKey &&
e.shiftKey &&
e.key.toLowerCase() === 'i'
) {
openUI(collectImages());
}
});
})();