From 204aeda00c86ba7d3e975aa53ce1f1f14e4b17a4 Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Fri, 27 Feb 2026 06:14:43 +0000 Subject: [PATCH] feat: implement url cleaner tool, local storage persistence and extension integration --- .gitignore | 2 + src/components/Sidebar.vue | 1 + src/components/tools/ClipboardSniffer.vue | 377 +++------------- src/components/tools/UrlCleaner.vue | 425 ++++++++++++++++++ .../tools/common/ExtensionStatus.vue | 190 ++++++++ src/composables/useExtension.js | 79 ++++ src/composables/useLocalStorage.js | 18 + src/router/index.js | 6 + 8 files changed, 772 insertions(+), 326 deletions(-) create mode 100644 src/components/tools/UrlCleaner.vue create mode 100644 src/components/tools/common/ExtensionStatus.vue create mode 100644 src/composables/useExtension.js create mode 100644 src/composables/useLocalStorage.js diff --git a/.gitignore b/.gitignore index 96c79c5..d9daf14 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ dist-ssr *.sw? dev-dist extension-release.zip +*.zip +tools-app-extension-*.zip diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 657079e..f61ef26 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -12,6 +12,7 @@ defineProps({ diff --git a/src/components/tools/ClipboardSniffer.vue b/src/components/tools/ClipboardSniffer.vue index 932e2ac..3872041 100644 --- a/src/components/tools/ClipboardSniffer.vue +++ b/src/components/tools/ClipboardSniffer.vue @@ -1,91 +1,29 @@ @@ -278,145 +105,43 @@ onUnmounted(() => { width: 100%; } -.extension-status { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.1); - color: var(--text-secondary); - cursor: pointer; - transition: all 0.3s ease; -} - -:global(:root[data-theme="light"]) .extension-status { - background: rgba(0, 0, 0, 0.05); - color: #666; -} - -.extension-status:hover { - background: rgba(255, 255, 255, 0.2); - color: var(--text-color); -} - -:global(:root[data-theme="light"]) .extension-status:hover { - background: rgba(0, 0, 0, 0.1); - color: #000; -} - -.extension-status.connected { - color: #4ade80; /* Green for connected */ - cursor: pointer; /* Allow clicking to see status */ -} - -/* :global(:root[data-theme="light"]) .extension-status.connected { - color: #16a34a; -} */ - -.extension-status.connected:hover { - background: rgba(74, 222, 128, 0.1); -} - -/* :global(:root[data-theme="light"]) .extension-status.connected:hover { - background: rgba(22, 163, 74, 0.1); -} */ - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.modal-content { - position: relative; - width: 90%; - max-width: 500px; - padding: 2rem; - border-radius: 16px; - background: var(--glass-bg); - border: 1px solid var(--glass-border); - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); - text-align: center; -} - -.close-btn { - position: absolute; - top: 1rem; - right: 1rem; - background: none; - border: none; - color: var(--text-secondary); - cursor: pointer; - padding: 0.5rem; - border-radius: 50%; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.close-btn:hover { - background: rgba(255, 255, 255, 0.1); - color: var(--text-color); -} - -.modal-content h3 { - margin-top: 0; - margin-bottom: 1rem; - font-size: 1.5rem; - color: var(--text-color); -} - -.modal-content p { - margin-bottom: 1rem; - line-height: 1.6; - color: var(--text-color); -} - -.description { - color: var(--text-secondary) !important; - font-size: 0.9rem; - margin-bottom: 2rem !important; -} - -.modal-actions { - display: flex; - justify-content: center; -} - .controls { display: flex; - gap: 1rem; justify-content: center; + gap: 1rem; flex-wrap: wrap; } -.btn-neon { - padding: 0.75rem 1.5rem; - min-width: 120px; +.tool-container.full-width { + max-width: 100%; + height: 100%; + display: flex; + flex-direction: column; } -.btn-neon.active { - background: rgba(255, 0, 0, 0.2); - border-color: rgba(255, 0, 0, 0.5); - box-shadow: 0 0 15px rgba(255, 0, 0, 0.3); +.tool-panel { + display: flex; + flex-direction: column; + height: 100%; + gap: 1.5rem; } .tool-textarea { + width: 100%; height: 100%; + padding: 1rem; + background-color: var(--toggle-bg); + border: 1px solid var(--toggle-border); + border-radius: 8px; + color: var(--text-color); + font-family: monospace; + resize: none; + font-size: 0.9rem; + line-height: 1.5; +} + +.tool-textarea:focus { + outline: none; + border-color: var(--primary-accent); } diff --git a/src/components/tools/UrlCleaner.vue b/src/components/tools/UrlCleaner.vue new file mode 100644 index 0000000..a8246b9 --- /dev/null +++ b/src/components/tools/UrlCleaner.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/src/components/tools/common/ExtensionStatus.vue b/src/components/tools/common/ExtensionStatus.vue new file mode 100644 index 0000000..3a5e3ac --- /dev/null +++ b/src/components/tools/common/ExtensionStatus.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/composables/useExtension.js b/src/composables/useExtension.js new file mode 100644 index 0000000..37dcd2a --- /dev/null +++ b/src/composables/useExtension.js @@ -0,0 +1,79 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useExtension() { + const isExtensionReady = ref(false) + const isListening = ref(false) + const lastClipboardText = ref('') + let extensionCheckInterval = null + let lastPongTime = Date.now() + + const PING_INTERVAL = 200 + const TIMEOUT_THRESHOLD = 1000 + + const handleMessage = (event) => { + if (event.source !== window) return + + if (event.data.type === 'TOOLS_APP_EXTENSION_READY' || event.data.type === 'TOOLS_APP_PONG') { + isExtensionReady.value = true + lastPongTime = Date.now() + } + + if (event.data.type === 'TOOLS_APP_CLIPBOARD_UPDATE') { + const text = event.data.content + // Only update if listening + if (isListening.value && text) { + lastClipboardText.value = text + } + } + } + + const startWatchdog = () => { + extensionCheckInterval = setInterval(() => { + window.postMessage({ type: 'TOOLS_APP_PING' }, '*') + if (Date.now() - lastPongTime > TIMEOUT_THRESHOLD) { + isExtensionReady.value = false + } + }, PING_INTERVAL) + } + + const startListening = () => { + window.postMessage({ type: 'TOOLS_APP_START_SNIFFING' }, '*') + isListening.value = true + } + + const stopListening = () => { + window.postMessage({ type: 'TOOLS_APP_STOP_SNIFFING' }, '*') + isListening.value = false + } + + const writeClipboard = (text) => { + // Send to extension (background -> offscreen -> clipboard) + window.postMessage({ type: 'TOOLS_APP_CLIPBOARD_WRITE', content: text }, '*') + // Update local state to avoid echo loop if we are listening + lastClipboardText.value = text + } + + onMounted(() => { + window.addEventListener('message', handleMessage) + // Initial check + window.postMessage({ type: 'TOOLS_APP_INIT' }, '*') + startWatchdog() + }) + + onUnmounted(() => { + if (isListening.value) { + stopListening() + } + if (extensionCheckInterval) clearInterval(extensionCheckInterval) + window.removeEventListener('message', handleMessage) + }) + + return { + isExtensionReady, + isListening, + lastClipboardText, + startListening, + stopListening, + writeClipboard + } +} diff --git a/src/composables/useLocalStorage.js b/src/composables/useLocalStorage.js new file mode 100644 index 0000000..2e448a1 --- /dev/null +++ b/src/composables/useLocalStorage.js @@ -0,0 +1,18 @@ +import { ref, watch } from 'vue' + +export function useLocalStorage(key, defaultValue) { + // Initialize state from local storage or default value + const storedValue = localStorage.getItem(key) + const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue) + + // Watch for changes and update local storage + watch(data, (newValue) => { + if (newValue === null || newValue === undefined) { + localStorage.removeItem(key) + } else { + localStorage.setItem(key, JSON.stringify(newValue)) + } + }, { deep: true }) + + return data +} diff --git a/src/router/index.js b/src/router/index.js index fbbeeec..1d6383f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router' import Main from '../components/Main.vue' import Passwords from '../components/tools/Passwords.vue' import ClipboardSniffer from '../components/tools/ClipboardSniffer.vue' +import UrlCleaner from '../components/tools/UrlCleaner.vue' import PrivacyPolicy from '../views/PrivacyPolicy.vue' const routes = [ @@ -20,6 +21,11 @@ const routes = [ name: 'ClipboardSniffer', component: ClipboardSniffer }, + { + path: '/url-cleaner', + name: 'UrlCleaner', + component: UrlCleaner + }, { path: '/extension-privacy-policy', name: 'PrivacyPolicy',