Compare commits
18 Commits
v0.6.8
...
ee387d9637
| Author | SHA1 | Date | |
|---|---|---|---|
|
ee387d9637
|
|||
|
f2203e896e
|
|||
|
cb8d3d01ec
|
|||
|
5b31171964
|
|||
|
e98761a18e
|
|||
|
bc8168e724
|
|||
|
dcde3b0799
|
|||
|
60fc774586
|
|||
|
d65c0d0357
|
|||
|
02736ecc70
|
|||
|
7b1d19ca7a
|
|||
|
4846d0e61c
|
|||
|
3155e12b84
|
|||
|
b8bbe84aa9
|
|||
|
74984caf9e
|
|||
|
c8b799b078
|
|||
|
f3a4c1af05
|
|||
|
616f615d7c
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -27,3 +27,9 @@ dev-dist
|
||||
extension-release.zip
|
||||
*.zip
|
||||
tools-app-extension-*.zip
|
||||
tools-app-extension-*.crx
|
||||
|
||||
# Extension keys and builds
|
||||
*.pem
|
||||
*.crx
|
||||
scripts/*.pub
|
||||
|
||||
20
README.md
20
README.md
@@ -20,6 +20,26 @@ Monitor and capture your clipboard history in real-time.
|
||||
- Clears history on demand.
|
||||
- Privacy-focused: Data is processed locally and never sent to any server.
|
||||
|
||||
### 🔗 URL Cleaner
|
||||
Clean tracking parameters and clutter from URLs.
|
||||
- **Privacy:** Removes known tracking parameters (UTM, fbclid, gclid, etc.).
|
||||
- **Bulk Processing:** Clean list of URLs at once.
|
||||
- **Customizable:** Manage exceptions to keep specific parameters.
|
||||
- **Smart:** "Direct Clean" mode for quick single-URL cleaning.
|
||||
|
||||
### ⬛ QR Generator
|
||||
Create custom QR codes instantly.
|
||||
- **Customizable:** Adjustable error correction level and size.
|
||||
- **Download:** Save as PNG (raster) or SVG (vector) for high-quality printing.
|
||||
- **Persistent Settings:** Remembers your preferences.
|
||||
|
||||
### 📷 QR Scanner
|
||||
Scan QR codes directly from your camera or device.
|
||||
- **Privacy First:** Works entirely in the browser using local `BarcodeDetector` API. No images are sent to any server.
|
||||
- **Multi-Camera:** Support for front and back cameras with easy switching.
|
||||
- **History:** Keeps a log of scanned codes with options to copy or download as JSON.
|
||||
- **Responsive:** Fullscreen mode for immersive scanning experience.
|
||||
|
||||
---
|
||||
|
||||
## Chrome Extension 🧩
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "tools-app",
|
||||
"version": "0.6.8",
|
||||
"version": "0.6.16",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tools-app",
|
||||
"version": "0.6.8",
|
||||
"version": "0.6.16",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"barcode-detector": "^3.1.0",
|
||||
"lucide-vue-next": "^0.575.0",
|
||||
"marked": "^17.0.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^5.0.3"
|
||||
@@ -4750,6 +4751,18 @@
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz",
|
||||
"integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "tools-app",
|
||||
"private": true,
|
||||
"version": "0.6.8",
|
||||
"version": "0.6.17",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"pack-extension": "node scripts/pack_crx.js",
|
||||
"postinstall": "mkdir -p public/wasm && cp node_modules/zxing-wasm/dist/reader/zxing_reader.wasm public/wasm/"
|
||||
},
|
||||
"dependencies": {
|
||||
"barcode-detector": "^3.1.0",
|
||||
"lucide-vue-next": "^0.575.0",
|
||||
"marked": "^17.0.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^5.0.3"
|
||||
|
||||
@@ -6,9 +6,14 @@ import zipfile
|
||||
# Configuration
|
||||
SOURCE_DIR = "extension"
|
||||
BUILD_DIR = "dist-extension"
|
||||
OUTPUT_ZIP = "extension-release.zip"
|
||||
MANIFEST_FILE = "manifest.json"
|
||||
|
||||
# Read version to create dynamic zip name
|
||||
with open(os.path.join(SOURCE_DIR, MANIFEST_FILE), "r") as f:
|
||||
source_manifest = json.load(f)
|
||||
version = source_manifest.get("version", "unknown")
|
||||
OUTPUT_ZIP = f"tools-app-extension-v{version}.zip"
|
||||
|
||||
# Remove build directory if exists
|
||||
if os.path.exists(BUILD_DIR):
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
@@ -27,7 +32,7 @@ print("Removing localhost from manifest...")
|
||||
# Filter host_permissions
|
||||
if "host_permissions" in manifest:
|
||||
manifest["host_permissions"] = [
|
||||
perm for perm in manifest["host_permissions"]
|
||||
perm for perm in manifest["host_permissions"]
|
||||
if "localhost" not in perm
|
||||
]
|
||||
|
||||
@@ -36,7 +41,7 @@ if "content_scripts" in manifest:
|
||||
for script in manifest["content_scripts"]:
|
||||
if "matches" in script:
|
||||
script["matches"] = [
|
||||
match for match in script["matches"]
|
||||
match for match in script["matches"]
|
||||
if "localhost" not in match
|
||||
]
|
||||
|
||||
@@ -55,4 +60,4 @@ with zipfile.ZipFile(OUTPUT_ZIP, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
print(f"Done! {OUTPUT_ZIP} is ready for upload to Chrome Web Store.")
|
||||
print(f"Done! {OUTPUT_ZIP} is ready for upload to Chrome Web Store.")
|
||||
|
||||
110
scripts/pack_crx.js
Normal file
110
scripts/pack_crx.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function findKey() {
|
||||
// 1. Check if key provided via CLI
|
||||
if (process.argv[3]) return process.argv[3];
|
||||
|
||||
// 2. Check local project directory (gitignored)
|
||||
const localKey = path.join(process.cwd(), 'scripts', 'key.pem');
|
||||
if (fs.existsSync(localKey)) return localKey;
|
||||
|
||||
// 3. Check common SSH key locations in ~/.ssh
|
||||
const sshDir = path.join(process.env.HOME, '.ssh');
|
||||
const commonKeys = ['id_rsa', 'id_ecdsa', 'id_ed25519'];
|
||||
|
||||
for (const keyName of commonKeys) {
|
||||
const fullPath = path.join(sshDir, keyName);
|
||||
if (fs.existsSync(fullPath)) return fullPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const KEY_PATH = findKey();
|
||||
const INPUT_SOURCE = process.argv[2] || path.join(process.cwd(), 'extension');
|
||||
const TEMP_DIR = path.join(process.cwd(), 'temp_extension_build');
|
||||
|
||||
function pack() {
|
||||
console.log('📦 Packing extension to CRX...');
|
||||
|
||||
if (!KEY_PATH) {
|
||||
console.error('❌ Error: No private key found.');
|
||||
console.log('Tried local scripts/key.pem and common ~/.ssh/id_* keys.');
|
||||
console.log('You can provide a path manually: npm run pack-extension <source> <path/to/key>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let extensionDir = INPUT_SOURCE;
|
||||
let isTemp = false;
|
||||
|
||||
try {
|
||||
// If input is a zip file, unzip it first
|
||||
if (INPUT_SOURCE.endsWith('.zip')) {
|
||||
console.log('🤐 Unzipping extension...');
|
||||
if (fs.existsSync(TEMP_DIR)) fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEMP_DIR);
|
||||
execSync(`unzip -o "${INPUT_SOURCE}" -d "${TEMP_DIR}"`, { stdio: 'pipe' });
|
||||
extensionDir = TEMP_DIR;
|
||||
isTemp = true;
|
||||
}
|
||||
|
||||
// Resolve actual extension directory (handle subdirs in zip)
|
||||
if (!fs.existsSync(path.join(extensionDir, 'manifest.json'))) {
|
||||
const subdirs = fs.readdirSync(extensionDir).filter(f => fs.statSync(path.join(extensionDir, f)).isDirectory());
|
||||
if (subdirs.length === 1 && fs.existsSync(path.join(extensionDir, subdirs[0], 'manifest.json'))) {
|
||||
extensionDir = path.join(extensionDir, subdirs[0]);
|
||||
console.log(`📂 Found manifest in subdirectory: ${extensionDir}`);
|
||||
} else {
|
||||
console.error(`❌ Error: manifest.json not found in ${extensionDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine output filename
|
||||
let outputName;
|
||||
if (INPUT_SOURCE.endsWith('.zip')) {
|
||||
outputName = path.basename(INPUT_SOURCE).replace('.zip', '.crx');
|
||||
} else {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(extensionDir, 'manifest.json'), 'utf8'));
|
||||
outputName = `tools-app-extension-v${manifest.version}.crx`;
|
||||
}
|
||||
const outputFull = path.join(process.cwd(), outputName);
|
||||
|
||||
// Get version for logging
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(extensionDir, 'manifest.json'), 'utf8'));
|
||||
const version = manifest.version;
|
||||
|
||||
console.log(`🔑 Using key: ${KEY_PATH}`);
|
||||
console.log(`📂 Source: ${extensionDir} (v${version})`);
|
||||
console.log(`🚀 Running crx3 via npx...`);
|
||||
|
||||
// Command: npx -y crx3 --key <key> --crx <output> <dir>
|
||||
execSync(`npx -y crx3 --key "${KEY_PATH}" --crx "${outputFull}" "${extensionDir}"`, {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// Cleanup any file that crx3 might have created automatically based on temp dir name
|
||||
const unintendedFile = extensionDir + '.crx';
|
||||
if (fs.existsSync(unintendedFile) && unintendedFile !== outputFull) {
|
||||
fs.unlinkSync(unintendedFile);
|
||||
}
|
||||
|
||||
console.log(`\n✅ Success! Extension packed to: ${outputFull}`);
|
||||
} catch (error) {
|
||||
console.error('\n❌ Failed to pack extension.');
|
||||
if (error.message.includes('algorithm')) {
|
||||
console.error('⚠️ Note: Chrome CRX format requires RSA or ECDSA (P-256) keys.');
|
||||
} else {
|
||||
console.error('Error details:', error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (isTemp && fs.existsSync(TEMP_DIR)) {
|
||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pack();
|
||||
@@ -44,7 +44,7 @@ onMounted(() => {
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="app-title">Tools App</h1>
|
||||
<router-link to="/" class="app-title">Tools App</router-link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -115,10 +115,11 @@ onMounted(() => {
|
||||
.app-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
background: var(--title-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,12 +51,20 @@ const dismissPrompt = () => {
|
||||
deferredPrompt = null
|
||||
}
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape' && showInstallPrompt.value) {
|
||||
dismissPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,99 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { computed } from 'vue'
|
||||
import readmeContent from '../../README.md?raw'
|
||||
|
||||
const htmlContent = computed(() => {
|
||||
return marked(readmeContent)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
main
|
||||
<div class="readme-container glass-panel">
|
||||
<div class="markdown-body" v-html="htmlContent"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.readme-container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1) {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-accent);
|
||||
}
|
||||
|
||||
.markdown-body :deep(h2) {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.markdown-body :deep(h3) {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.markdown-body :deep(p) {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.markdown-body :deep(ul) {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.markdown-body :deep(li) {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.markdown-body :deep(a) {
|
||||
color: var(--primary-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body :deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body :deep(code) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre) {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.markdown-body :deep(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@ const scannedCodes = ref([])
|
||||
const hasMultipleCameras = ref(false)
|
||||
const isFullscreen = ref(false)
|
||||
const videoAspect = ref(1)
|
||||
const isFront = computed(() => facingMode.value === 'user')
|
||||
const isMirrored = ref(false)
|
||||
const wrapperRef = ref(null)
|
||||
const bgCanvas = ref(null)
|
||||
let bgRafId = null
|
||||
@@ -65,31 +65,27 @@ const paintDetections = (codes) => {
|
||||
}
|
||||
|
||||
const scale = drawWidth / vw
|
||||
const isMirrored = isFront.value
|
||||
// Canvas is mirrored via CSS if isMirrored is true, so no manual coordinate mirroring needed
|
||||
|
||||
// Styles
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
const accent = styles.getPropertyValue('--primary-accent').trim() || '#00f2fe'
|
||||
// Styles
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
const accent = styles.getPropertyValue('--primary-accent').trim() || '#00f2fe'
|
||||
|
||||
ctx.lineWidth = 4
|
||||
ctx.strokeStyle = accent
|
||||
ctx.fillStyle = accent
|
||||
ctx.lineWidth = 4
|
||||
ctx.strokeStyle = accent
|
||||
ctx.fillStyle = accent
|
||||
|
||||
codes.forEach(code => {
|
||||
const points = code.cornerPoints
|
||||
if (!points || points.length < 4) return
|
||||
codes.forEach(code => {
|
||||
const points = code.cornerPoints
|
||||
if (!points || points.length < 4) return
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.beginPath()
|
||||
|
||||
const transform = (p) => {
|
||||
let x = p.x * scale + startX
|
||||
let y = p.y * scale + startY
|
||||
|
||||
if (isMirrored) {
|
||||
x = width - x
|
||||
}
|
||||
return { x, y }
|
||||
}
|
||||
const transform = (p) => {
|
||||
let x = p.x * scale + startX
|
||||
let y = p.y * scale + startY
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
const p0 = transform(points[0])
|
||||
ctx.moveTo(p0.x, p0.y)
|
||||
@@ -203,6 +199,23 @@ const startScan = async () => {
|
||||
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints)
|
||||
|
||||
// Detect actual facing mode to mirror front camera correctly
|
||||
const videoTrack = stream.getVideoTracks()[0]
|
||||
if (videoTrack) {
|
||||
const settings = videoTrack.getSettings()
|
||||
if (settings.facingMode) {
|
||||
isMirrored.value = settings.facingMode === 'user'
|
||||
} else {
|
||||
// Fallback: check label for desktop cameras or assume requested mode
|
||||
const label = videoTrack.label ? videoTrack.label.toLowerCase() : ''
|
||||
if (label.includes('front') || label.includes('facetime') || label.includes('macbook')) {
|
||||
isMirrored.value = true
|
||||
} else {
|
||||
isMirrored.value = facingMode.value === 'user'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = stream
|
||||
// Wait for metadata to play
|
||||
@@ -364,11 +377,18 @@ watch(scannedCodes, (newVal) => {
|
||||
localStorage.setItem('qr-history', JSON.stringify(newVal))
|
||||
}, { deep: true })
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape' && isFullscreen.value) {
|
||||
toggleFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkCameras()
|
||||
loadHistory()
|
||||
window.addEventListener('resize', updateVideoAspect)
|
||||
window.addEventListener('resize', startBackgroundLoop)
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
watch(isFullscreen, (fs) => {
|
||||
if (fs) {
|
||||
startBackgroundLoop()
|
||||
@@ -376,13 +396,14 @@ onMounted(() => {
|
||||
stopBackgroundLoop()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
startScan()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateVideoAspect)
|
||||
window.removeEventListener('resize', startBackgroundLoop)
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
stopScan()
|
||||
})
|
||||
|
||||
@@ -457,6 +478,7 @@ const isUrl = (string) => {
|
||||
v-if="isFullscreen"
|
||||
ref="bgCanvas"
|
||||
class="camera-bg"
|
||||
:class="{ 'is-mirrored': isMirrored }"
|
||||
></canvas>
|
||||
<button v-if="isFullscreen" class="close-fullscreen-btn" @click="toggleFullscreen">
|
||||
<X size="24" />
|
||||
@@ -464,7 +486,7 @@ const isUrl = (string) => {
|
||||
|
||||
<div
|
||||
class="camera-wrapper"
|
||||
:class="{ 'clickable': !isFullscreen, 'is-front': isFront }"
|
||||
:class="{ 'clickable': !isFullscreen, 'is-mirrored': isMirrored }"
|
||||
:style="desktopFullscreenStyle"
|
||||
ref="wrapperRef"
|
||||
@click="!isFullscreen && toggleFullscreen()"
|
||||
@@ -472,13 +494,13 @@ const isUrl = (string) => {
|
||||
<video
|
||||
ref="videoRef"
|
||||
class="camera-feed"
|
||||
:class="{ 'is-front': isFront }"
|
||||
:class="{ 'is-mirrored': isMirrored }"
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
></video>
|
||||
|
||||
<canvas ref="overlayCanvas" class="scan-overlay-canvas"></canvas>
|
||||
<canvas ref="overlayCanvas" class="scan-overlay-canvas" :class="{ 'is-mirrored': isMirrored }"></canvas>
|
||||
|
||||
<div v-if="error" class="error-overlay">
|
||||
<p>{{ error }}</p>
|
||||
@@ -625,6 +647,10 @@ const isUrl = (string) => {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.camera-bg.is-mirrored {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.camera-feed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -632,7 +658,7 @@ const isUrl = (string) => {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.camera-feed.is-front {
|
||||
.camera-feed.is-mirrored {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
@@ -646,6 +672,10 @@ const isUrl = (string) => {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.scan-overlay-canvas.is-mirrored {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* front mirror canvas removed */
|
||||
|
||||
.error-overlay {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { X, Plus, Trash2, RotateCcw } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -10,6 +10,24 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'update:exceptions'])
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.isOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
const newRule = ref({
|
||||
domainPattern: '',
|
||||
keepParams: '',
|
||||
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { Plug, Plus, X } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -13,6 +13,24 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape' && showModal.value) {
|
||||
showModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(showModal, (isOpen) => {
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -38,7 +56,14 @@ const showModal = ref(false)
|
||||
With the extension, you can capture and process 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>
|
||||
<a
|
||||
href="https://chromewebstore.google.com/detail/tools-app-extension/bhcpbmfncohogehbhebiffcgjcndnneg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-neon"
|
||||
>
|
||||
Install Extension
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user