158 lines
4.6 KiB
Vue
158 lines
4.6 KiB
Vue
<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>
|