feat: improve Clipboard Sniffer extension integration and UI fixes
This commit is contained in:
156
extension/background.js
Normal file
156
extension/background.js
Normal 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
66
extension/content.js
Normal 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
BIN
extension/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
extension/icon-16.png
Normal file
BIN
extension/icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 862 B |
BIN
extension/icon-48.png
Normal file
BIN
extension/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
47
extension/manifest.json
Normal file
47
extension/manifest.json
Normal 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
10
extension/offscreen.html
Normal 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
72
extension/offscreen.js
Normal 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
55
extension/popup.html
Normal 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
34
extension/popup.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user