feat: unify tool styles and add dynamic height for textareas
This commit is contained in:
@@ -11,6 +11,7 @@ defineProps({
|
||||
<aside class="sidebar unselectable" :class="{ 'is-open': isOpen }">
|
||||
<nav class="sidebar-nav">
|
||||
<router-link to="/passwords" class="nav-item" v-ripple>Passwords</router-link>
|
||||
<router-link to="/clipboard-sniffer" class="nav-item" v-ripple>Clipboard Sniffer</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
157
src/components/tools/ClipboardSniffer.vue
Normal file
157
src/components/tools/ClipboardSniffer.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup>
|
||||
import { ref, onUnmounted, nextTick } from 'vue'
|
||||
import { useFillHeight } from '../../composables/useFillHeight'
|
||||
|
||||
const clipboardContent = ref('')
|
||||
const isListening = ref(false)
|
||||
const lastClipboardText = ref('')
|
||||
const textareaRef = ref(null)
|
||||
let intervalId = null
|
||||
|
||||
const { height: textareaHeight } = useFillHeight(textareaRef, 40) // 40px margin bottom
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
} 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.
|
||||
}
|
||||
}, 1000)
|
||||
} catch (err) {
|
||||
console.error('Permission denied or clipboard error:', err)
|
||||
alert('Clipboard access denied. Please allow clipboard access to use this tool.')
|
||||
}
|
||||
}
|
||||
|
||||
const stopListening = () => {
|
||||
isListening.value = false
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
const clearText = () => {
|
||||
clipboardContent.value = ''
|
||||
// Don't reset lastClipboardText so if they copy the same thing again it's detected?
|
||||
// No, if they clear, they might want to see it again if they copy it again.
|
||||
// But usually "change" means diff from clipboard.
|
||||
// If I clear text, but clipboard still has "A", and I copy "A" again (refresh clipboard), readText still returns "A".
|
||||
// So it won't be detected as a change.
|
||||
// If user wants to capture "A" again, they need to copy something else then "A".
|
||||
// That's standard behavior for "sniffer" (detect changes).
|
||||
}
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (!clipboardContent.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(clipboardContent.value)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopListening()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-container" style="max-width: 100%;">
|
||||
<div class="tool-panel">
|
||||
<h2 class="tool-title">Clipboard Sniffer</h2>
|
||||
|
||||
<div class="controls">
|
||||
<button
|
||||
v-if="!isListening"
|
||||
class="btn-neon"
|
||||
@click="startListening"
|
||||
v-ripple
|
||||
>
|
||||
Start Sniffing
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn-neon active"
|
||||
@click="stopListening"
|
||||
v-ripple
|
||||
>
|
||||
Stop Sniffing
|
||||
</button>
|
||||
|
||||
<button class="btn-neon" @click="copyToClipboard" v-ripple>
|
||||
Copy
|
||||
</button>
|
||||
|
||||
<button class="btn-neon" @click="clearText" v-ripple>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-area" style="margin-top: 2rem;">
|
||||
<div ref="textareaRef" :style="{ height: textareaHeight, width: '100%' }">
|
||||
<textarea
|
||||
v-model="clipboardContent"
|
||||
class="tool-textarea"
|
||||
placeholder="Clipboard content will appear here line by line..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-neon {
|
||||
padding: 0.75rem 1.5rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.btn-neon.active {
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
border-color: rgba(255, 0, 0, 0.5);
|
||||
box-shadow: 0 0 15px rgba(255, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tool-textarea {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useFillHeight } from '../../composables/useFillHeight';
|
||||
|
||||
// Options
|
||||
const useLower = ref(true);
|
||||
@@ -12,6 +13,8 @@ const count = ref(20);
|
||||
|
||||
// Result
|
||||
const result = ref('');
|
||||
const textareaRef = ref(null);
|
||||
const { height: textareaHeight } = useFillHeight(textareaRef, 40);
|
||||
|
||||
// Character Sets
|
||||
const CHARS_LOWER = 'abcdefghijklmnopqrstuvwxyz';
|
||||
@@ -68,8 +71,8 @@ const generatePasswords = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="passwords-tool">
|
||||
<div class="glass-panel tool-panel">
|
||||
<div class="tool-container">
|
||||
<div class="tool-panel">
|
||||
<h2 class="tool-title">Bulk Passwords Generator</h2>
|
||||
|
||||
<div class="options-grid">
|
||||
@@ -129,66 +132,19 @@ const generatePasswords = () => {
|
||||
|
||||
<div class="result-area">
|
||||
<label>Passwords</label>
|
||||
<textarea
|
||||
class="result-textarea glass-panel selectable"
|
||||
v-model="result"
|
||||
placeholder="Generated passwords will appear here..."
|
||||
></textarea>
|
||||
<div ref="textareaRef" :style="{ height: textareaHeight, width: '100%' }">
|
||||
<textarea
|
||||
class="tool-textarea"
|
||||
v-model="result"
|
||||
placeholder="Generated passwords will appear here..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.passwords-tool {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
width: 100%;
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for tool panel */
|
||||
.tool-panel::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tool-panel::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tool-panel::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tool-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.tool-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
background: var(--title-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(0 0 10px var(--title-glow));
|
||||
}
|
||||
|
||||
.options-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -399,23 +355,12 @@ const generatePasswords = () => {
|
||||
.result-area label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.9rem;margin-top: auto;
|
||||
}
|
||||
|
||||
.result-textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
.tool-textarea {
|
||||
min-height: 200px;
|
||||
padding: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--toggle-border);
|
||||
border-radius: 12px;
|
||||
color: var(--text-strong);
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
resize: vertical;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.result-textarea:focus {
|
||||
|
||||
Reference in New Issue
Block a user