feat: unify tool styles and add dynamic height for textareas
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user