Compare commits
12 Commits
v0.2.0
...
98d76e3a35
| Author | SHA1 | Date | |
|---|---|---|---|
|
98d76e3a35
|
|||
|
cc7e80a807
|
|||
|
1f5500f7d7
|
|||
|
d404370027
|
|||
|
8fb3ee1069
|
|||
|
348c78612d
|
|||
|
dc99dce485
|
|||
|
20dc18dd28
|
|||
|
30d67472cb
|
|||
|
ac425d3df2
|
|||
|
e817ff6169
|
|||
|
8e8bf47297
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
dev-dist
|
||||
extension-release.zip
|
||||
|
||||
68
README.md
68
README.md
@@ -1 +1,67 @@
|
||||
# Tools App
|
||||
# Tools App 🛠️
|
||||
|
||||
A collection of useful developer tools in one place. Built with Vue 3 and Vite.
|
||||
|
||||
**Live App:** [https://tools.7u.pl/](https://tools.7u.pl/)
|
||||
|
||||
## Available Tools
|
||||
|
||||
### 🔐 Bulk Passwords Generator
|
||||
Generate strong, secure passwords in bulk.
|
||||
- Customizable character sets (lowercase, uppercase, digits, special characters).
|
||||
- Option to skip similar characters (e.g., `l`, `1`, `I`, `O`, `0`).
|
||||
- Adjustable length and quantity.
|
||||
- Generates thousands of passwords instantly.
|
||||
|
||||
### 📋 Clipboard Sniffer
|
||||
Monitor and capture your clipboard history in real-time.
|
||||
- **Web Mode:** Works when the tab is active and focused.
|
||||
- **Background Mode (with Extension):** Captures clipboard changes even when you are working in other applications or tabs.
|
||||
- Clears history on demand.
|
||||
- Privacy-focused: Data is processed locally and never sent to any server.
|
||||
|
||||
---
|
||||
|
||||
## Chrome Extension 🧩
|
||||
|
||||
To unlock the full potential of the **Clipboard Sniffer**, you can install the companion Chrome Extension.
|
||||
|
||||
### Why install the extension?
|
||||
By default, web browsers restrict clipboard access to when the tab is active and focused. The **Tools App Extension** runs in the background, allowing the application to detect clipboard changes even when you are using other apps or browsing different websites.
|
||||
|
||||
### Features
|
||||
- **Background Monitoring:** seamlessly captures copied text while you work.
|
||||
- **Smart Integration:** automatically connects to the Tools App when open.
|
||||
- **Privacy First:** The extension only communicates with the Tools App (`tools.7u.pl` or `localhost`). No data is sent to third-party servers. [Privacy Policy](https://tools.7u.pl/extension-privacy-policy)
|
||||
|
||||
### Installation
|
||||
1. Download the latest release or build from source.
|
||||
2. Open Chrome and navigate to `chrome://extensions/`.
|
||||
3. Enable "Developer mode" in the top right.
|
||||
4. Click "Load unpacked" and select the `extension` folder (or drag and drop the `.zip` file).
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Project Setup
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run for Development
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build for Production
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Build Extension
|
||||
To create a production-ready zip file for the Chrome Extension:
|
||||
```bash
|
||||
python3 scripts/build_extension.py
|
||||
```
|
||||
This will generate `extension-release.zip` in the project root.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.define) {
|
||||
let registry = {};
|
||||
|
||||
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||
let nextDefineUri;
|
||||
|
||||
const singleRequire = (uri, parentUri) => {
|
||||
uri = new URL(uri + ".js", parentUri).href;
|
||||
return registry[uri] || (
|
||||
|
||||
new Promise(resolve => {
|
||||
if ("document" in self) {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
let promise = registry[uri];
|
||||
if (!promise) {
|
||||
throw new Error(`Module ${uri} didn’t register its module`);
|
||||
}
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
self.define = (depsNames, factory) => {
|
||||
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||
if (registry[uri]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
let exports = {};
|
||||
const require = depUri => singleRequire(depUri, uri);
|
||||
const specialDeps = {
|
||||
module: { uri },
|
||||
exports,
|
||||
require
|
||||
};
|
||||
registry[uri] = Promise.all(depsNames.map(
|
||||
depName => specialDeps[depName] || require(depName)
|
||||
)).then(deps => {
|
||||
factory(...deps);
|
||||
return exports;
|
||||
});
|
||||
};
|
||||
}
|
||||
define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
||||
|
||||
self.skipWaiting();
|
||||
workbox.clientsClaim();
|
||||
|
||||
/**
|
||||
* The precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "registerSW.js",
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.3pcduqlbss8"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
allowlist: [/^\/$/]
|
||||
}));
|
||||
|
||||
}));
|
||||
File diff suppressed because it is too large
Load Diff
28
extension/README.md
Normal file
28
extension/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Tools App Extension 🚀
|
||||
|
||||
This is the companion Chrome Extension for **Tools App** ([tools.7u.pl](https://tools.7u.pl/)).
|
||||
|
||||
## Overview
|
||||
**Tools App Extension** enhances your experience with the Tools App, specifically designed to power the **Clipboard Sniffer** tool. By default, web applications can only read your clipboard when the browser tab is active and focused. This extension runs in the background, allowing you to capture clipboard history seamlessly while you work in other applications or browse different websites.
|
||||
|
||||
## Key Features
|
||||
- **Background Clipboard Monitoring:** Automatically captures copied text even when the Tools App tab is in the background.
|
||||
- **Privacy-First Design:** All clipboard data is processed locally within your browser and sent directly to the open Tools App tab. **No data is ever sent to external servers.**
|
||||
- **Smart Activation:** The extension only activates when you explicitly start the "Clipboard Sniffer" tool in the web app. It stops monitoring immediately when you stop the tool or close the tab.
|
||||
- **Visual Feedback:** The extension icon changes to indicate when it is actively monitoring your clipboard.
|
||||
|
||||
## How it Works
|
||||
1. Open [tools.7u.pl](https://tools.7u.pl/) and navigate to the **Clipboard Sniffer** tool.
|
||||
2. The web app will detect the extension and show a "Connected" status.
|
||||
3. Click "Start Sniffing". The extension will begin monitoring clipboard changes in the background.
|
||||
4. Any text you copy (from any application) will appear in the Tools App list.
|
||||
5. Click "Stop Sniffing" to disable monitoring.
|
||||
|
||||
## Permissions Explained
|
||||
- **Read data from the clipboard (`clipboardRead`):** Essential for detecting when you copy text.
|
||||
- **Run scripts on tools.7u.pl (`scripting`):** Allows the extension to communicate with the Tools App web page to send clipboard updates.
|
||||
- **Storage:** Used to save user preferences (e.g., sound notifications).
|
||||
- **Alarms:** Used to keep the background process alive while sniffing is active (to prevent timeout).
|
||||
|
||||
## Privacy Policy
|
||||
We value your privacy. This extension does not collect, store, or transmit any personal data. Clipboard content is only temporarily read and passed to the local instance of the Tools App running in your browser tab.
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tools-app",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tools-app",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.4",
|
||||
"dependencies": {
|
||||
"lucide-vue-next": "^0.575.0",
|
||||
"vue": "^3.5.25",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tools-app",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
58
scripts/build_extension.py
Normal file
58
scripts/build_extension.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
# Configuration
|
||||
SOURCE_DIR = "extension"
|
||||
BUILD_DIR = "dist-extension"
|
||||
OUTPUT_ZIP = "extension-release.zip"
|
||||
MANIFEST_FILE = "manifest.json"
|
||||
|
||||
# Remove build directory if exists
|
||||
if os.path.exists(BUILD_DIR):
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
|
||||
# Copy source directory to build directory
|
||||
shutil.copytree(SOURCE_DIR, BUILD_DIR)
|
||||
|
||||
# Modify manifest.json for production
|
||||
manifest_path = os.path.join(BUILD_DIR, MANIFEST_FILE)
|
||||
with open(manifest_path, "r") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
# Filter permissions and content scripts (remove localhost)
|
||||
print("Removing localhost from manifest...")
|
||||
|
||||
# Filter host_permissions
|
||||
if "host_permissions" in manifest:
|
||||
manifest["host_permissions"] = [
|
||||
perm for perm in manifest["host_permissions"]
|
||||
if "localhost" not in perm
|
||||
]
|
||||
|
||||
# Filter content_scripts matches
|
||||
if "content_scripts" in manifest:
|
||||
for script in manifest["content_scripts"]:
|
||||
if "matches" in script:
|
||||
script["matches"] = [
|
||||
match for match in script["matches"]
|
||||
if "localhost" not in match
|
||||
]
|
||||
|
||||
# Save modified manifest
|
||||
with open(manifest_path, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
# Create ZIP file
|
||||
print(f"Creating {OUTPUT_ZIP}...")
|
||||
with zipfile.ZipFile(OUTPUT_ZIP, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(BUILD_DIR):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, BUILD_DIR)
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
print(f"Done! {OUTPUT_ZIP} is ready for upload to Chrome Web Store.")
|
||||
61
scripts/resize_screenshot.py
Normal file
61
scripts/resize_screenshot.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
def resize_image_canvas(input_path, output_path, canvas_width, canvas_height, bg_color=(255, 255, 255)):
|
||||
"""
|
||||
Resizes an image to fit within a specific canvas size, centering it and adding padding.
|
||||
"""
|
||||
try:
|
||||
original_image = Image.open(input_path)
|
||||
|
||||
# Calculate aspect ratios
|
||||
img_ratio = original_image.width / original_image.height
|
||||
canvas_ratio = canvas_width / canvas_height
|
||||
|
||||
# Determine new dimensions
|
||||
if img_ratio > canvas_ratio:
|
||||
# Image is wider than canvas
|
||||
new_width = canvas_width
|
||||
new_height = int(canvas_width / img_ratio)
|
||||
else:
|
||||
# Image is taller than canvas
|
||||
new_height = canvas_height
|
||||
new_width = int(canvas_height * img_ratio)
|
||||
|
||||
# Resize the original image
|
||||
resized_image = original_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# Create a new canvas with the specified background color
|
||||
new_image = Image.new("RGB", (canvas_width, canvas_height), bg_color)
|
||||
|
||||
# Calculate position to center the image
|
||||
x_offset = (canvas_width - new_width) // 2
|
||||
y_offset = (canvas_height - new_height) // 2
|
||||
|
||||
# Paste the resized image onto the canvas
|
||||
new_image.paste(resized_image, (x_offset, y_offset))
|
||||
|
||||
# Save the result
|
||||
new_image.save(output_path, quality=95)
|
||||
print(f"Success! Image saved to {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage:
|
||||
# Replace 'screenshot.png' with your actual file name
|
||||
# We will look for png or jpg files in current dir if not specified
|
||||
import glob
|
||||
import sys
|
||||
|
||||
files = glob.glob("*.png") + glob.glob("*.jpg") + glob.glob("*.jpeg")
|
||||
|
||||
if not files:
|
||||
print("No image files found in the current directory.")
|
||||
sys.exit(1)
|
||||
|
||||
print("Found images:", files)
|
||||
input_file = files[0] # Take the first one found
|
||||
print(f"Processing {input_file}...")
|
||||
|
||||
resize_image_canvas(input_file, "cws_screenshot_1280x800.png", 1280, 800, bg_color=(245, 247, 250)) # Light gray-ish background matching the app
|
||||
@@ -1,51 +1,135 @@
|
||||
<script setup>
|
||||
import { ref, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, onUnmounted, nextTick, onMounted } from 'vue'
|
||||
import { useFillHeight } from '../../composables/useFillHeight'
|
||||
import { Plug, Info, X } from 'lucide-vue-next'
|
||||
|
||||
const clipboardContent = ref('')
|
||||
const isListening = ref(false)
|
||||
const lastClipboardText = ref('')
|
||||
const textareaRef = ref(null)
|
||||
const isExtensionReady = ref(false)
|
||||
const showExtensionModal = ref(false)
|
||||
let intervalId = null
|
||||
let extensionCheckInterval = null
|
||||
|
||||
const { height: textareaHeight } = useFillHeight(textareaRef, 40) // 40px margin bottom
|
||||
|
||||
// Listen for extension messages
|
||||
const handleExtensionMessage = (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()
|
||||
// console.log('Extension is ready')
|
||||
}
|
||||
|
||||
if (event.data.type === 'TOOLS_APP_CLIPBOARD_UPDATE' && isListening.value) {
|
||||
const text = event.data.content
|
||||
if (text && text !== lastClipboardText.value) {
|
||||
lastClipboardText.value = text
|
||||
clipboardContent.value += (clipboardContent.value ? '\n' : '') + text
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const closeModalOnEsc = (e) => {
|
||||
if (e.key === 'Escape' && showExtensionModal.value) {
|
||||
showExtensionModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Watchdog for extension
|
||||
let lastPongTime = Date.now()
|
||||
const PING_INTERVAL = 200
|
||||
const TIMEOUT_THRESHOLD = 500
|
||||
|
||||
const startExtensionWatchdog = () => {
|
||||
extensionCheckInterval = setInterval(() => {
|
||||
// 1. Send Ping
|
||||
window.postMessage({ type: 'TOOLS_APP_PING' }, '*')
|
||||
|
||||
// 2. Check timeout
|
||||
// If current time - lastPongTime > threshold, then disconnected
|
||||
if (Date.now() - lastPongTime > TIMEOUT_THRESHOLD) {
|
||||
isExtensionReady.value = false
|
||||
}
|
||||
}, PING_INTERVAL)
|
||||
}
|
||||
|
||||
// Wrapper to intercept PONG and update heartbeat
|
||||
const messageListener = (event) => {
|
||||
if (event.source !== window) return
|
||||
|
||||
if (event.data.type === 'TOOLS_APP_PONG' || event.data.type === 'TOOLS_APP_EXTENSION_READY') {
|
||||
lastPongTime = Date.now()
|
||||
isExtensionReady.value = true
|
||||
}
|
||||
|
||||
handleExtensionMessage(event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', messageListener)
|
||||
window.addEventListener('keydown', closeModalOnEsc)
|
||||
|
||||
// Initial check
|
||||
window.postMessage({ type: 'TOOLS_APP_INIT' }, '*')
|
||||
|
||||
// Start heartbeat
|
||||
startExtensionWatchdog()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopListening()
|
||||
if (extensionCheckInterval) clearInterval(extensionCheckInterval)
|
||||
window.removeEventListener('message', messageListener)
|
||||
window.removeEventListener('keydown', closeModalOnEsc)
|
||||
})
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
const textarea = document.querySelector('.tool-textarea')
|
||||
if (textarea) {
|
||||
textarea.scrollTop = textarea.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const startListening = async () => {
|
||||
try {
|
||||
// Initial read to ask for permission/check access
|
||||
const text = await navigator.clipboard.readText()
|
||||
lastClipboardText.value = text // Don't paste existing content immediately, only new content?
|
||||
// Or maybe we want to paste the current content immediately?
|
||||
// "wklejaj nasluchane wartosci" - usually implies new values.
|
||||
// Let's set current as last seen so we don't duplicate it if it's already there?
|
||||
// Actually, user might want the current clipboard too.
|
||||
// Let's assume we start clean or append.
|
||||
// If I set lastClipboardText to current, it won't be added.
|
||||
// Let's add the current text if it's not empty.
|
||||
if (text) {
|
||||
lastClipboardText.value = text
|
||||
clipboardContent.value += (clipboardContent.value ? '\n' : '') + text
|
||||
}
|
||||
|
||||
isListening.value = true
|
||||
|
||||
// Try native API first (for web app usage without extension)
|
||||
// Initial read
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
lastClipboardText.value = text
|
||||
clipboardContent.value += (clipboardContent.value ? '\n' : '') + text
|
||||
scrollToBottom()
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Native clipboard read failed (expected if not focused), relying on extension if available')
|
||||
}
|
||||
|
||||
// If extension is ready, ask it to start sniffing
|
||||
if (isExtensionReady.value) {
|
||||
window.postMessage({ type: 'TOOLS_APP_START_SNIFFING' }, '*')
|
||||
}
|
||||
|
||||
// Fallback polling for web app (only works when focused usually)
|
||||
intervalId = setInterval(async () => {
|
||||
try {
|
||||
const currentText = await navigator.clipboard.readText()
|
||||
if (currentText && currentText !== lastClipboardText.value) {
|
||||
lastClipboardText.value = currentText
|
||||
clipboardContent.value += (clipboardContent.value ? '\n' : '') + currentText
|
||||
|
||||
// Auto-scroll to bottom
|
||||
const textarea = document.querySelector('.tool-textarea')
|
||||
if (textarea) {
|
||||
textarea.scrollTop = textarea.scrollHeight
|
||||
}
|
||||
scrollToBottom()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard:', err)
|
||||
// Don't stop immediately on one error, could be temporary focus loss?
|
||||
// But if permission revoked, maybe stop.
|
||||
// Ignore errors in polling (e.g. lost focus)
|
||||
}
|
||||
}, 1000)
|
||||
} catch (err) {
|
||||
@@ -60,6 +144,10 @@ const stopListening = () => {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
|
||||
if (isExtensionReady.value) {
|
||||
window.postMessage({ type: 'TOOLS_APP_STOP_SNIFFING' }, '*')
|
||||
}
|
||||
}
|
||||
|
||||
const clearText = () => {
|
||||
@@ -90,7 +178,18 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div class="tool-container" style="max-width: 100%;">
|
||||
<div class="tool-panel">
|
||||
<h2 class="tool-title">Clipboard Sniffer</h2>
|
||||
<div class="tool-header">
|
||||
<h2 class="tool-title">Clipboard Sniffer</h2>
|
||||
<div
|
||||
class="extension-status"
|
||||
:class="{ 'connected': isExtensionReady }"
|
||||
@click="showExtensionModal = true"
|
||||
:title="isExtensionReady ? 'Extension connected' : 'Extension not detected - Click for info'"
|
||||
>
|
||||
<Plug v-if="isExtensionReady" size="20" />
|
||||
<Info v-else size="20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button
|
||||
@@ -119,7 +218,7 @@ onUnmounted(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-area" style="margin-top: 2rem;">
|
||||
<div class="result-area" style="margin-top: 1rem;">
|
||||
<div ref="textareaRef" :style="{ height: textareaHeight, width: '100%' }">
|
||||
<textarea
|
||||
v-model="clipboardContent"
|
||||
@@ -129,10 +228,176 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extension Info Modal -->
|
||||
<div v-if="showExtensionModal" class="modal-overlay" @click.self="showExtensionModal = false">
|
||||
<div class="modal-content glass-panel">
|
||||
<button class="close-btn" @click="showExtensionModal = false">
|
||||
<X size="24" />
|
||||
</button>
|
||||
|
||||
<div v-if="!isExtensionReady">
|
||||
<h3>Enhance Your Experience</h3>
|
||||
<p>
|
||||
Install our browser extension to enable <strong>background clipboard sniffing</strong>!
|
||||
</p>
|
||||
<p class="description">
|
||||
Without the extension, this tool can only capture clipboard content when the tab is active.
|
||||
With the extension, you can capture content even when you're working in other apps.
|
||||
</p>
|
||||
<div class="modal-actions">
|
||||
<a href="#" class="btn-neon" @click.prevent>Extension Coming Soon</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<h3>Extension Connected!</h3>
|
||||
<p>
|
||||
You have successfully enabled <strong>background clipboard sniffing</strong>.
|
||||
</p>
|
||||
<p class="description">
|
||||
The extension is active and monitoring your clipboard in the background.
|
||||
You can now switch to other apps and copy text - it will appear here automatically.
|
||||
</p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-neon" @click="showExtensionModal = false">Got it!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
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;
|
||||
|
||||
@@ -71,9 +71,16 @@ const generatePasswords = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-container">
|
||||
<div class="tool-container full-width">
|
||||
<div class="tool-panel">
|
||||
<h2 class="tool-title">Bulk Passwords Generator</h2>
|
||||
<div class="panel-header">
|
||||
<h2 class="tool-title">Bulk Passwords Generator</h2>
|
||||
<div class="action-area desktop-only">
|
||||
<button class="btn-neon generate-btn" @click="generatePasswords" v-ripple>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-grid">
|
||||
<div class="checkbox-group">
|
||||
@@ -100,13 +107,13 @@ const generatePasswords = () => {
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="skipSimilar">
|
||||
<span class="checkmark"></span>
|
||||
Skip Similar Chars (I, l, 1, O, 0, o)
|
||||
Skip Similar (I, l, 1, O, 0, o)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="inputs-group">
|
||||
<div class="input-wrapper">
|
||||
<label>Password Length</label>
|
||||
<label>Length</label>
|
||||
<div class="number-control">
|
||||
<button class="control-btn" @click="length > 4 ? length-- : null">-</button>
|
||||
<input type="number" v-model="length" min="4" max="128" class="number-input">
|
||||
@@ -114,7 +121,7 @@ const generatePasswords = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<label>Passwords Number</label>
|
||||
<label>Count</label>
|
||||
<div class="number-control">
|
||||
<button class="control-btn" @click="count > 1 ? count-- : null">-</button>
|
||||
<input type="number" v-model="count" min="1" max="1000" class="number-input">
|
||||
@@ -124,37 +131,82 @@ const generatePasswords = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-area">
|
||||
<button class="btn-neon generate-btn" @click="generatePasswords" v-ripple>
|
||||
<div class="mobile-only" style="margin-top: 1rem; width: 100%;">
|
||||
<button class="btn-neon generate-btn" @click="generatePasswords" v-ripple style="width: 100%;">
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-area">
|
||||
<label>Passwords</label>
|
||||
<div ref="textareaRef" :style="{ height: textareaHeight, width: '100%' }">
|
||||
<textarea
|
||||
class="tool-textarea"
|
||||
v-model="result"
|
||||
placeholder="Generated passwords will appear here..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="result-area" :style="{ height: textareaHeight }">
|
||||
<textarea
|
||||
class="tool-textarea"
|
||||
v-model="result"
|
||||
placeholder="Generated passwords will appear here..."
|
||||
readonly
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.options-grid {
|
||||
.tool-container.full-width {
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tool-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.options-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--toggle-bg);
|
||||
border: 1px solid var(--toggle-border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.inputs-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
@@ -166,9 +218,9 @@ const generatePasswords = () => {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
/* Custom Checkbox */
|
||||
.checkbox-label input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
@@ -212,86 +264,63 @@ const generatePasswords = () => {
|
||||
top: 2px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border: solid #000;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.inputs-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.input-wrapper label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Number Control */
|
||||
.number-control {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: var(--toggle-bg);
|
||||
border: 1px solid var(--toggle-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 42px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
font-size: 1.2rem;
|
||||
width: 40px;
|
||||
height: auto;
|
||||
min-height: 40px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--toggle-bg);
|
||||
border: 1px solid var(--toggle-border);
|
||||
color: var(--text-color);
|
||||
border-radius: 0;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.control-btn:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.control-btn:last-child {
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background-color: var(--button-hover-bg);
|
||||
border-color: var(--toggle-hover-border);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.98);
|
||||
background: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
.number-input {
|
||||
flex: 1;
|
||||
padding: 0.8rem;
|
||||
background-color: var(--toggle-bg);
|
||||
border: 1px solid var(--toggle-border);
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
appearance: textfield; /* Remove default spinner */
|
||||
min-width: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.number-input:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.number-input::-webkit-outer-spin-button,
|
||||
@@ -300,90 +329,70 @@ const generatePasswords = () => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.number-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-accent);
|
||||
box-shadow: 0 0 0 2px var(--toggle-active-shadow);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-neon {
|
||||
width: 100%;
|
||||
background: var(--button-bg);
|
||||
border: 1px solid var(--button-border);
|
||||
color: var(--button-text);
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-neon:hover {
|
||||
background: var(--button-hover-bg);
|
||||
box-shadow: var(--button-hover-shadow);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--toggle-hover-border);
|
||||
}
|
||||
|
||||
.btn-neon:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: var(--button-active-shadow);
|
||||
}
|
||||
|
||||
.result-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.result-area label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;margin-top: auto;
|
||||
}
|
||||
|
||||
.tool-textarea {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.result-textarea:focus {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-color);
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
resize: none;
|
||||
outline: none;
|
||||
border-color: var(--primary-accent);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Scrollbar for textarea */
|
||||
.result-textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
.generate-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.result-textarea::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
.desktop-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-textarea::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-textarea::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
@media (max-width: 768px) {
|
||||
.options-grid {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.inputs-group {
|
||||
flex-direction: column;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 PrivacyPolicy from '../views/PrivacyPolicy.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -18,6 +19,11 @@ const routes = [
|
||||
path: '/clipboard-sniffer',
|
||||
name: 'ClipboardSniffer',
|
||||
component: ClipboardSniffer
|
||||
},
|
||||
{
|
||||
path: '/extension-privacy-policy',
|
||||
name: 'PrivacyPolicy',
|
||||
component: PrivacyPolicy
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
209
src/views/PrivacyPolicy.vue
Normal file
209
src/views/PrivacyPolicy.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<script setup>
|
||||
import { ArrowLeft } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-container">
|
||||
<div class="tool-panel privacy-panel">
|
||||
<header class="privacy-header">
|
||||
<router-link to="/" class="back-link">
|
||||
<ArrowLeft size="20" />
|
||||
Back to Tools
|
||||
</router-link>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="last-updated">Last Updated: February 27, 2026</p>
|
||||
</header>
|
||||
|
||||
<div class="privacy-body">
|
||||
<section>
|
||||
<h2>1. Introduction</h2>
|
||||
<p>
|
||||
Welcome to Tools App ("we," "our," or "us"). We are committed to protecting your privacy.
|
||||
This Privacy Policy explains how our Chrome Extension ("Tools App Extension") handles your data.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Data Collection and Usage</h2>
|
||||
<p>
|
||||
The Tools App Extension is designed with privacy as a priority.
|
||||
<strong>We do not collect, store, or transmit any of your personal data to external servers.</strong>
|
||||
</p>
|
||||
|
||||
<h3>Clipboard Data</h3>
|
||||
<p>
|
||||
The extension requires the <code>clipboardRead</code> permission to function.
|
||||
It reads text from your clipboard <strong>only</strong> when you explicitly enable the "Clipboard Sniffer" tool in the Tools App web interface.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Clipboard data is processed locally within your browser.</li>
|
||||
<li>Data is sent directly from the extension to the open Tools App tab via a secure local communication channel.</li>
|
||||
<li>Once you close the tab or stop the tool, the extension stops monitoring the clipboard immediately.</li>
|
||||
<li>We do not have access to your clipboard history, and it is never uploaded to any cloud storage or third-party service.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Permissions</h2>
|
||||
<p>The extension requests the following permissions for specific functional purposes:</p>
|
||||
<ul>
|
||||
<li><strong>clipboardRead:</strong> To detect copied text when the Sniffer tool is active.</li>
|
||||
<li><strong>scripting:</strong> To communicate with the Tools App web page.</li>
|
||||
<li><strong>storage:</strong> To save local user preferences (e.g., sound settings).</li>
|
||||
<li><strong>alarms:</strong> To maintain the background process active during monitoring sessions.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Third-Party Services</h2>
|
||||
<p>
|
||||
Our extension operates independently and does not use any third-party analytics, tracking scripts, or advertising networks.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about this Privacy Policy, please contact us via the repository or support channels provided in the Chrome Web Store listing.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.privacy-panel {
|
||||
max-width: 900px; /* Slightly wider for reading */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.privacy-body {
|
||||
overflow-y: auto;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for privacy body */
|
||||
.privacy-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.privacy-body::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.privacy-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.privacy-header {
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
padding-bottom: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--primary-accent);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
background: var(--title-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.7;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--text-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
:global(html[data-theme="light"]) code {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.privacy-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.privacy-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user