10 Commits

Author SHA1 Message Date
98d76e3a35 0.3.4
All checks were successful
Deploy to Production / deploy (push) Successful in 13s
2026-02-27 04:44:34 +00:00
cc7e80a807 feat: improve mobile layout for Password Generator button 2026-02-27 04:43:58 +00:00
1f5500f7d7 0.3.3
All checks were successful
Deploy to Production / deploy (push) Successful in 13s
2026-02-27 04:37:29 +00:00
d404370027 fix: adjust layout for Password Generator and Clipboard Sniffer on smaller screens 2026-02-27 04:37:21 +00:00
8fb3ee1069 refactor: align privacy policy layout with app design
All checks were successful
Deploy to Production / deploy (push) Successful in 7s
2026-02-27 04:20:29 +00:00
348c78612d 0.3.2
All checks were successful
Deploy to Production / deploy (push) Successful in 13s
2026-02-27 04:16:17 +00:00
dc99dce485 fix: restore dark mode styles and scope privacy policy styles 2026-02-27 04:16:01 +00:00
20dc18dd28 0.3.1
All checks were successful
Deploy to Production / deploy (push) Successful in 12s
2026-02-27 04:03:50 +00:00
30d67472cb docs: add privacy policy and extension documentation 2026-02-27 04:03:35 +00:00
ac425d3df2 chore: remove dev-dist from version control
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
2026-02-27 03:39:16 +00:00
14 changed files with 461 additions and 3496 deletions

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
dev-dist
extension-release.zip

View File

@@ -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.

View File

@@ -1 +0,0 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })

View File

@@ -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} didnt 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.mj22prstr4"
}], {});
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
View 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.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "tools-app",
"version": "0.3.0",
"version": "0.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tools-app",
"version": "0.3.0",
"version": "0.3.4",
"dependencies": {
"lucide-vue-next": "^0.575.0",
"vue": "^3.5.25",

View File

@@ -1,7 +1,7 @@
{
"name": "tools-app",
"private": true,
"version": "0.3.0",
"version": "0.3.4",
"type": "module",
"scripts": {
"dev": "vite",

View 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.")

View 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

View File

@@ -224,7 +224,6 @@ onUnmounted(() => {
v-model="clipboardContent"
class="tool-textarea"
placeholder="Clipboard content will appear here line by line..."
readonly
></textarea>
</div>
</div>

View File

@@ -75,7 +75,7 @@ const generatePasswords = () => {
<div class="tool-panel">
<div class="panel-header">
<h2 class="tool-title">Bulk Passwords Generator</h2>
<div class="action-area">
<div class="action-area desktop-only">
<button class="btn-neon generate-btn" @click="generatePasswords" v-ripple>
Generate
</button>
@@ -131,6 +131,12 @@ const generatePasswords = () => {
</div>
</div>
<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" :style="{ height: textareaHeight }">
<textarea
class="tool-textarea"
@@ -189,9 +195,10 @@ const generatePasswords = () => {
.inputs-group {
display: flex;
gap: 2rem;
gap: 1rem;
flex: 1;
min-width: 300px;
min-width: 200px;
flex-wrap: wrap;
}
.input-wrapper {
@@ -199,6 +206,7 @@ const generatePasswords = () => {
flex-direction: column;
gap: 0.5rem;
flex: 1;
min-width: 140px;
}
.checkbox-label {
@@ -351,6 +359,14 @@ const generatePasswords = () => {
letter-spacing: 1px;
}
.desktop-only {
display: block;
}
.mobile-only {
display: none;
}
@media (max-width: 768px) {
.options-grid {
flex-direction: column;
@@ -370,5 +386,13 @@ const generatePasswords = () => {
.generate-btn {
width: 100%;
}
.desktop-only {
display: none;
}
.mobile-only {
display: block !important;
}
}
</style>

View File

@@ -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
}
]

209
src/views/PrivacyPolicy.vue Normal file
View 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>