From 8e8bf4729764a36e0c03137915c253b7544f8c9d Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Fri, 27 Feb 2026 03:37:33 +0000 Subject: [PATCH] feat: improve Clipboard Sniffer extension integration and UI fixes --- dev-dist/sw.js | 2 +- extension/background.js | 156 +++++++++++ extension/content.js | 66 +++++ extension/icon-128.png | Bin 0 -> 11522 bytes extension/icon-16.png | Bin 0 -> 862 bytes extension/icon-48.png | Bin 0 -> 3671 bytes extension/manifest.json | 47 ++++ extension/offscreen.html | 10 + extension/offscreen.js | 72 +++++ extension/popup.html | 55 ++++ extension/popup.js | 34 +++ src/components/tools/ClipboardSniffer.vue | 320 ++++++++++++++++++++-- src/components/tools/Passwords.vue | 289 +++++++++---------- src/style.css | 2 +- 14 files changed, 872 insertions(+), 181 deletions(-) create mode 100644 extension/background.js create mode 100644 extension/content.js create mode 100644 extension/icon-128.png create mode 100644 extension/icon-16.png create mode 100644 extension/icon-48.png create mode 100644 extension/manifest.json create mode 100644 extension/offscreen.html create mode 100644 extension/offscreen.js create mode 100644 extension/popup.html create mode 100644 extension/popup.js diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 1e1413b..64d60e3 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.3pcduqlbss8" + "revision": "0.mj22prstr4" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..da4f095 --- /dev/null +++ b/extension/background.js @@ -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 + }); + } + } + } +}); diff --git a/extension/content.js b/extension/content.js new file mode 100644 index 0000000..1ed2391 --- /dev/null +++ b/extension/content.js @@ -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); +} diff --git a/extension/icon-128.png b/extension/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f1ad83474accc78e8dd0782668d272bdc5b1f6 GIT binary patch literal 11522 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 literal 0 HcmV?d00001 diff --git a/extension/icon-16.png b/extension/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..128eec4c6bac196fbb522b4a35f66f27a047e4f5 GIT binary patch literal 862 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 + + + Offscreen Clipboard Access + + + + + + diff --git a/extension/offscreen.js b/extension/offscreen.js new file mode 100644 index 0000000..0a46dc9 --- /dev/null +++ b/extension/offscreen.js @@ -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); +} diff --git a/extension/popup.html b/extension/popup.html new file mode 100644 index 0000000..c0b9e65 --- /dev/null +++ b/extension/popup.html @@ -0,0 +1,55 @@ + + + + Tools App Extension + + + +

Tools App Extension

+
+ Extension is active and ready to communicate with Tools App. +
+ +
+ +
+ + + + + diff --git a/extension/popup.js b/extension/popup.js new file mode 100644 index 0000000..4658b58 --- /dev/null +++ b/extension/popup.js @@ -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); + } + }); +}); diff --git a/src/components/tools/ClipboardSniffer.vue b/src/components/tools/ClipboardSniffer.vue index 6c85bc9..20eee50 100644 --- a/src/components/tools/ClipboardSniffer.vue +++ b/src/components/tools/ClipboardSniffer.vue @@ -1,51 +1,135 @@ diff --git a/src/style.css b/src/style.css index 1a494a3..6864dc0 100644 --- a/src/style.css +++ b/src/style.css @@ -55,7 +55,7 @@ } :root[data-theme="light"] { - --bg-gradient: radial-gradient(circle at center, #ffffff 0%, #cccccc 100%); + --bg-gradient: radial-gradient(circle at center, #ffffff 0%, #e5e7eb 100%); --glass-bg: rgba(255, 255, 255, 0.75); --glass-border: rgba(15, 23, 42, 0.12); --glass-shadow: 0 8px 32px 0 rgba(15, 23, 42, 0.12);