feat: improve Clipboard Sniffer extension integration and UI fixes

This commit is contained in:
2026-02-27 03:37:33 +00:00
parent 1d7fd9a5bc
commit 8e8bf47297
14 changed files with 872 additions and 181 deletions

156
extension/background.js Normal file
View File

@@ -0,0 +1,156 @@
// background.js
// Listen for messages from content scripts or offscreen document
let isSniffing = false;
let lastClipboardContent = '';
let creatingOffscreenDocument;
// Hot-reconnect: Inject content script into existing tabs upon installation/update/restart
const injectContentScriptIfNeeded = async () => {
const tabs = await chrome.tabs.query({ url: ['http://localhost/*', 'http://localhost:*/*', 'https://tools.7u.pl/*'] });
for (const tab of tabs) {
try {
// Try to ping the tab first
try {
await chrome.tabs.sendMessage(tab.id, { action: 'ping' });
// console.log('Content script already active in tab:', tab.id);
} catch (e) {
// If ping fails (no listener), inject script
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
// console.log('Injected content script into existing tab:', tab.id);
}
} catch (err) {
// console.error('Failed to handle tab:', tab.id, err);
}
}
};
chrome.runtime.onInstalled.addListener(injectContentScriptIfNeeded);
// Also run on startup (when extension is enabled/reloaded)
injectContentScriptIfNeeded();
// Listen for alarms
try {
if (chrome.alarms) {
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') {
refreshOffscreenDocument();
}
});
} else {
// console.warn('chrome.alarms API is not available.');
}
} catch (e) {
// console.error('Error initializing alarms:', e);
}
// Setup offscreen document
async function setupOffscreenDocument(path) {
// Check if an offscreen document already exists
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
});
if (existingContexts.length > 0) {
return;
}
// Create an offscreen document
if (creatingOffscreenDocument) {
await creatingOffscreenDocument;
} else {
creatingOffscreenDocument = chrome.offscreen.createDocument({
url: path,
reasons: ['CLIPBOARD', 'AUDIO_PLAYBACK'],
justification: 'To read clipboard content in the background and play notification sounds',
});
await creatingOffscreenDocument;
creatingOffscreenDocument = null;
}
}
// Lifecycle management: Refresh offscreen document every 25s to avoid 30s timeout
async function refreshOffscreenDocument() {
if (isSniffing) {
await chrome.offscreen.closeDocument();
await setupOffscreenDocument('offscreen.html');
}
}
// Start sniffing when requested
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.action === 'startSniffing') {
if (isSniffing) {
sendResponse({ status: 'already_started' });
return true;
}
isSniffing = true;
// console.log('Starting sniffing process...');
await setupOffscreenDocument('offscreen.html');
// Setup interval to keep offscreen alive - more aggressive
chrome.alarms.create('keepAlive', { periodInMinutes: 0.1 }); // every 6 seconds
sendResponse({ status: 'started' });
return true;
}
if (request.action === 'stopSniffing') {
if (!isSniffing) {
sendResponse({ status: 'not_running' });
return true;
}
isSniffing = false;
// console.log('Stopping sniffing process...');
// Stop alarm
chrome.alarms.clear('keepAlive');
// Close offscreen document
if (creatingOffscreenDocument) {
await creatingOffscreenDocument;
}
await chrome.offscreen.closeDocument().catch(() => {});
creatingOffscreenDocument = null;
sendResponse({ status: 'stopped' });
return true;
}
if (request.type === 'clipboard-data' && request.target === 'background') {
// Received data from offscreen document
if (isSniffing && request.data && request.data !== lastClipboardContent) {
lastClipboardContent = request.data;
// console.log('Clipboard changed:', request.data.substring(0, 20) + '...');
// Check if sound should be played
chrome.storage.local.get(['playSound'], (result) => {
if (result.playSound !== false) {
// Send message to offscreen document to play sound
chrome.runtime.sendMessage({
target: 'offscreen',
type: 'play-sound'
});
}
});
// Broadcast to all active tabs (content scripts)
// We could filter by sender.tab.id if we knew which tab started sniffing,
// but broadcasting is simpler for now and covers multiple open tabs of the app.
const tabs = await chrome.tabs.query({ url: ['http://localhost/*', 'http://localhost:*/*', 'https://tools.7u.pl/*'] });
for (const tab of tabs) {
chrome.tabs.sendMessage(tab.id, {
action: 'clipboardUpdate',
content: request.data
}).catch(() => {
// Tab might be closed or content script not injected yet
});
}
}
}
});

66
extension/content.js Normal file
View File

@@ -0,0 +1,66 @@
// content.js
// This script runs on the web app page (e.g. localhost:5173)
console.log('Tools App Extension: Content script injected');
// Listen for messages from the Web App (Vue)
window.addEventListener('message', (event) => {
// We should verify the origin, but since we are running on the page itself, we trust window messages
// from our own app.
if (event.source !== window) return;
if (event.data.type && event.data.type === 'TOOLS_APP_INIT') {
// console.log('Tools App Extension: Received init from Web App');
window.postMessage({ type: 'TOOLS_APP_EXTENSION_READY', version: '1.0' }, '*');
}
// Heartbeat check
if (event.data.type === 'TOOLS_APP_PING') {
try {
// Only respond if the extension context is still valid
if (chrome.runtime && chrome.runtime.id) {
window.postMessage({ type: 'TOOLS_APP_PONG' }, '*');
}
} catch (e) {
// Extension context invalidated
// console.warn('Extension context invalidated during ping');
}
}
// Example: Receive request to sniff clipboard
if (event.data.type === 'TOOLS_APP_START_SNIFFING') {
// console.log('Tools App Extension: Start sniffing request');
// Relay to background script
try {
chrome.runtime.sendMessage({ action: 'startSniffing' });
} catch (e) {
console.warn('Tools App Extension: Connection lost, please reload the page', e);
}
}
if (event.data.type === 'TOOLS_APP_STOP_SNIFFING') {
// console.log('Tools App Extension: Stop sniffing request');
try {
chrome.runtime.sendMessage({ action: 'stopSniffing' });
} catch (e) {
// ignore
}
}
});
// Listen for messages from the Extension Background
try {
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'clipboardUpdate') {
// Send to Web App
window.postMessage({ type: 'TOOLS_APP_CLIPBOARD_UPDATE', content: request.content }, '*');
}
// Respond to background ping to confirm we are alive
if (request.action === 'ping') {
sendResponse('pong');
}
});
} catch (e) {
console.warn('Tools App Extension: Could not add listener', e);
}

BIN
extension/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
extension/icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
extension/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

47
extension/manifest.json Normal file
View File

@@ -0,0 +1,47 @@
{
"manifest_version": 3,
"name": "Tools App Extension",
"version": "1.0",
"description": "Browser extension for Tools App",
"permissions": [
"clipboardRead",
"offscreen",
"storage",
"alarms",
"scripting"
],
"host_permissions": [
"http://localhost/*",
"http://localhost:*/*",
"https://tools.7u.pl/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": [
"http://localhost/*",
"http://localhost:*/*",
"https://tools.7u.pl/*"
],
"js": [
"content.js"
],
"run_at": "document_start"
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
},
"icons": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
}

10
extension/offscreen.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Offscreen Clipboard Access</title>
</head>
<body>
<textarea id="text"></textarea>
<script src="offscreen.js"></script>
</body>
</html>

72
extension/offscreen.js Normal file
View File

@@ -0,0 +1,72 @@
// offscreen.js
// This script runs in the offscreen document to access DOM APIs like navigator.clipboard
const textEl = document.querySelector('#text');
let lastText = '';
setInterval(async () => {
try {
textEl.focus();
textEl.value = '';
textEl.select();
// Method 1: execCommand
try {
document.execCommand('paste');
} catch (e) {
// Ignore
}
let text = textEl.value;
// Method 2: navigator.clipboard (Fallback)
if (!text) {
try {
text = await navigator.clipboard.readText();
} catch (e) {
// Silent fail for navigator
}
}
if (text && text.trim().length > 0 && text !== lastText) {
lastText = text;
chrome.runtime.sendMessage({
type: 'clipboard-data',
target: 'background',
data: text
}).catch(() => {});
}
} catch (error) {
// Ignore critical errors to keep running
}
}, 50);
// Listen for messages from background if we need to change behavior
chrome.runtime.onMessage.addListener((message) => {
if (message.target === 'offscreen') {
// Handle commands
if (message.type === 'play-sound') {
playNotificationSound();
}
}
});
function playNotificationSound() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(500, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(1000, audioContext.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.1);
}

55
extension/popup.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<title>Tools App Extension</title>
<style>
body {
width: 250px;
padding: 15px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f5f5;
user-select: none;
}
h3 {
margin-top: 0;
color: #333;
}
.status {
padding: 10px;
border-radius: 6px;
background: #e0e0e0;
margin-top: 10px;
font-size: 14px;
}
.status.active {
background: #e3f2fd;
color: #1565c0;
border: 1px solid #90caf9;
}
.footer {
margin-top: 15px;
font-size: 12px;
color: #666;
text-align: center;
}
</style>
</head>
<body>
<h3>Tools App Extension</h3>
<div class="status active">
Extension is active and ready to communicate with Tools App.
</div>
<div style="margin-top: 15px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="soundToggle" style="margin-right: 8px;">
<span>Play sound on capture</span>
</label>
</div>
<div class="footer">
Visit <a href="https://tools.7u.pl" target="_blank">tools.7u.pl</a>
</div>
<script src="popup.js"></script>
</body>
</html>

34
extension/popup.js Normal file
View File

@@ -0,0 +1,34 @@
// popup.js
document.addEventListener('DOMContentLoaded', () => {
const soundToggle = document.getElementById('soundToggle');
// Load saved setting
chrome.storage.local.get(['playSound'], (result) => {
soundToggle.checked = result.playSound !== false; // Default to true
});
// Save setting on change
soundToggle.addEventListener('change', () => {
chrome.storage.local.set({ playSound: soundToggle.checked });
// Play test sound if enabled
if (soundToggle.checked) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(500, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(1000, audioContext.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.1);
}
});
});