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 0000000..c8f1ad8 Binary files /dev/null and b/extension/icon-128.png differ diff --git a/extension/icon-16.png b/extension/icon-16.png new file mode 100644 index 0000000..128eec4 Binary files /dev/null and b/extension/icon-16.png differ diff --git a/extension/icon-48.png b/extension/icon-48.png new file mode 100644 index 0000000..05575ed Binary files /dev/null and b/extension/icon-48.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..cc081fd --- /dev/null +++ b/extension/manifest.json @@ -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" + } +} diff --git a/extension/offscreen.html b/extension/offscreen.html new file mode 100644 index 0000000..e18c193 --- /dev/null +++ b/extension/offscreen.html @@ -0,0 +1,10 @@ + + + + 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);