Files
archived-vdo.ninja/clipboard.html
steveseguin a631bc074c v29.0
2026-01-18 03:27:00 -05:00

791 lines
27 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shared Clipboard - P2P Text Sync | VDO.Ninja</title>
<!-- SEO Meta Tags -->
<meta name="description" content="Share text instantly across devices with VDO.Ninja's P2P Shared Clipboard. No server storage, real-time sync, secure peer-to-peer connection. Perfect for quick text sharing between devices.">
<meta name="keywords" content="shared clipboard, p2p text sync, webrtc clipboard, cross-device clipboard, vdo.ninja, peer to peer, real-time text sharing, secure clipboard">
<meta name="author" content="VDO.Ninja">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://vdo.ninja/clipboard">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://vdo.ninja/clipboard">
<meta property="og:title" content="Shared Clipboard - P2P Text Sync | VDO.Ninja">
<meta property="og:description" content="Share text instantly across devices with peer-to-peer technology. No server storage, real-time sync, completely secure.">
<meta property="og:image" content="https://vdo.ninja/media/vdo-ninja-banner.png">
<meta property="og:site_name" content="VDO.Ninja">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://vdo.ninja/clipboard">
<meta property="twitter:title" content="Shared Clipboard - P2P Text Sync | VDO.Ninja">
<meta property="twitter:description" content="Share text instantly across devices with peer-to-peer technology. No server storage, real-time sync.">
<meta property="twitter:image" content="https://vdo.ninja/media/vdo-ninja-banner.png">
<!-- Additional Meta -->
<meta name="application-name" content="VDO.Ninja Shared Clipboard">
<meta name="theme-color" content="#2563eb">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Shared Clipboard">
<!-- Favicon -->
<link rel="icon" type="image/png" href="https://vdo.ninja/favicon.png">
<link rel="apple-touch-icon" href="https://vdo.ninja/apple-touch-icon.png">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "VDO.Ninja Shared Clipboard",
"description": "Real-time P2P text synchronization across devices using WebRTC technology",
"url": "https://vdo.ninja/clipboard",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"creator": {
"@type": "Organization",
"name": "VDO.Ninja",
"url": "https://vdo.ninja"
},
"featureList": [
"Peer-to-peer text synchronization",
"No server storage",
"Real-time updates",
"Cross-device compatibility",
"Secure WebRTC connections"
]
}
</script>
<style>
* {
box-sizing: border-box;
}
/* Light mode (default) */
:root {
--bg-primary: #fafafa;
--bg-secondary: #ffffff;
--bg-tertiary: #f5f5f5;
--bg-input: #f8f8f8;
--text-primary: #1a1a1a;
--text-secondary: #4a4a4a;
--text-tertiary: #6a6a6a;
--border-primary: #e0e0e0;
--border-secondary: #d0d0d0;
--accent-primary: #2563eb;
--accent-hover: #1d4ed8;
--success: #059669;
--danger: #dc2626;
--info-bg: #e8f0ff;
--info-text: #1e40af;
--info-border: #b8d4ff;
--shadow: rgba(0,0,0,0.08);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #242424;
--bg-tertiary: #2a2a2a;
--bg-input: #1e1e1e;
--text-primary: #e8e8e8;
--text-secondary: #b8b8b8;
--text-tertiary: #888888;
--border-primary: #3a3a3a;
--border-secondary: #4a4a4a;
--accent-primary: #3b82f6;
--accent-hover: #2563eb;
--success: #10b981;
--danger: #ef4444;
--info-bg: #1e293b;
--info-text: #94a3b8;
--info-border: #334155;
--shadow: rgba(0,0,0,0.3);
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: 0 4px 20px var(--shadow);
padding: 30px;
border: 1px solid var(--border-primary);
}
h1 {
margin: 0 0 10px 0;
color: var(--text-primary);
font-size: 28px;
font-weight: 600;
}
.subtitle {
color: var(--text-secondary);
margin-bottom: 30px;
font-size: 14px;
}
.share-section {
background: var(--bg-tertiary);
border: 2px dashed var(--border-secondary);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.share-label {
font-weight: 600;
margin-bottom: 10px;
color: var(--text-secondary);
font-size: 14px;
}
.share-link-container {
display: flex;
gap: 10px;
align-items: stretch;
}
#shareLink {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--border-primary);
border-radius: 6px;
font-size: 14px;
background: var(--bg-input);
color: var(--text-primary);
font-family: monospace;
}
.copy-button {
padding: 12px 20px;
background: var(--accent-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
white-space: nowrap;
}
.copy-button:hover {
background: var(--accent-hover);
}
.copy-button:active {
transform: scale(0.98);
}
.copy-button.copied {
background: var(--success);
}
.clipboard-section {
margin-bottom: 20px;
}
.clipboard-label {
font-weight: 600;
margin-bottom: 10px;
color: var(--text-secondary);
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
#sharedClipboard {
width: 100%;
min-height: 300px;
padding: 16px;
border: 2px solid var(--border-primary);
border-radius: 8px;
font-size: 15px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
resize: vertical;
background: var(--bg-input);
color: var(--text-primary);
transition: border-color 0.2s, background-color 0.3s ease;
}
#sharedClipboard:focus {
outline: none;
border-color: var(--accent-primary);
background: var(--bg-secondary);
}
#sharedClipboard::placeholder {
color: var(--text-tertiary);
}
.status {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 13px;
margin-bottom: 15px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--danger);
transition: background 0.3s;
}
.status-indicator.connected {
background: var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.status-text {
color: var(--text-tertiary);
}
.peers-count {
color: var(--text-secondary);
font-weight: 500;
}
.char-count {
color: var(--text-tertiary);
font-size: 12px;
}
.info-section {
margin-top: 30px;
padding: 20px;
background: var(--info-bg);
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
border: 1px solid var(--info-border);
}
.info-section h3 {
margin: 0 0 10px 0;
color: var(--info-text);
font-size: 16px;
}
.info-section ul {
margin: 10px 0;
padding-left: 20px;
}
.info-section li {
margin: 5px 0;
color: var(--info-text);
}
.copy-button[style*="--danger"]:hover {
background: #b91c1c;
}
@media (max-width: 600px) {
body {
padding: 10px;
font-size: 16px;
}
.container {
padding: 20px;
}
h1 {
font-size: 24px;
}
.share-link-container {
flex-direction: column;
gap: 12px;
}
#shareLink {
font-size: 16px;
padding: 14px 16px;
}
.copy-button {
width: 100%;
padding: 14px 20px;
font-size: 16px;
}
#sharedClipboard {
font-size: 16px;
min-height: 250px;
}
.info-section {
margin-top: 20px;
}
}
@media (max-width: 400px) {
body {
padding: 8px;
}
.container {
padding: 16px;
}
h1 {
font-size: 20px;
}
.subtitle {
font-size: 13px;
}
}
@media (max-width: 600px) {
/* Update the existing media query section */
div[style*="display: flex"] {
gap: 8px;
}
div[style*="display: flex"] .copy-button {
padding: 14px 10px;
font-size: 15px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🔗 Shared Clipboard</h1>
<p class="subtitle">Real-time P2P text synchronization across devices</p>
<div class="share-section">
<div class="share-label">Share this link to sync clipboards:</div>
<div class="share-link-container">
<input type="text" id="shareLink" readonly>
<button class="copy-button" onclick="copyShareLink()">Copy Link</button>
</div>
</div>
<div class="clipboard-section">
<div class="clipboard-label">
<span>Shared Clipboard</span>
<span class="char-count" id="charCount">0 characters</span>
</div>
<textarea
id="sharedClipboard"
placeholder="Type or paste text here. It will automatically sync with all connected devices..."
></textarea>
</div>
<div class="status">
<span class="status-indicator" id="statusIndicator"></span>
<span class="status-text" id="statusText">Connecting...</span>
<span class="peers-count" id="peersCount"></span>
</div>
<button class="copy-button" style="width: 100%; margin-top: 15px;" onclick="copyClipboardContent()">
Copy Clipboard Content
</button>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button class="copy-button" style="flex: 1; background: var(--danger);" onclick="clearClipboard()">
Clear
</button>
<button class="copy-button" style="flex: 1; background: var(--danger);" onclick="clearAndPaste()">
Clear & Paste
</button>
</div>
<div class="info-section">
<h3> How it works</h3>
<ul>
<li>Share the link above with other devices or users</li>
<li>Any text typed or pasted will sync automatically</li>
<li>All data is transmitted peer-to-peer (no server storage)</li>
<li>Perfect for quickly sharing text between devices</li>
</ul>
</div>
</div>
<!-- Load the VDO.Ninja SDK -->
<script src="vdoninja-sdk.js"></script>
<script>
let sdk = null;
let roomId = '';
let streamId = '';
let connectedPeers = new Set();
let isUpdatingFromRemote = false;
let syncDebounceTimer = null;
// Initialize on page load
window.addEventListener('load', async () => {
await initializeSharedClipboard();
});
async function initializeSharedClipboard() {
// Get or generate room ID from URL
const urlParams = new URLSearchParams(window.location.search);
roomId = urlParams.get('room');
if (!roomId) {
// Generate a new room ID
roomId = generateRoomId();
// Update URL without reloading
const newUrl = window.location.origin + window.location.pathname + '?room=' + roomId;
window.history.replaceState({}, '', newUrl);
}
// Generate unique stream ID for this instance
streamId = 'clipboard-' + Math.random().toString(36).substr(2, 9);
// Update share link
const shareLink = window.location.origin + window.location.pathname + '?room=' + roomId;
document.getElementById('shareLink').value = shareLink;
// Initialize SDK
sdk = new VDONinjaSDK({
room: roomId,
password: false, // No encryption for simplicity
debug: true
});
// Set up event listeners
setupSDKEventListeners();
// Set up textarea event listener
const textarea = document.getElementById('sharedClipboard');
textarea.addEventListener('input', handleTextareaChange);
// Connect to the network
try {
await sdk.connect();
// Announce ourselves as a data-only peer
await sdk.announce({
streamID: streamId,
room: roomId,
label: 'shared-clipboard'
});
// Join room to discover other peers
await sdk.joinRoom({ room: roomId });
updateStatus('Connected', true);
console.log('Connected to room:', roomId);
} catch (error) {
console.error('Connection error:', error);
updateStatus('Connection failed', false);
}
}
function clearClipboard() {
const textarea = document.getElementById('sharedClipboard');
textarea.value = '';
// Update character count
document.getElementById('charCount').textContent = '0 characters';
// Send update to peers
sendContentUpdate('');
// Visual feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Cleared!';
setTimeout(() => {
button.textContent = originalText;
}, 1500);
}
async function clearAndPaste() {
const textarea = document.getElementById('sharedClipboard');
try {
// Clear first
textarea.value = '';
// Try to read from clipboard
const text = await navigator.clipboard.readText();
textarea.value = text;
// Update character count
document.getElementById('charCount').textContent = text.length + ' characters';
// Send update to peers
sendContentUpdate(text);
// Visual feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Pasted!';
setTimeout(() => {
button.textContent = originalText;
}, 1500);
} catch (err) {
// Fallback if clipboard API fails
console.error('Failed to read clipboard:', err);
// Visual feedback for error
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Paste failed';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}
}
function setupSDKEventListeners() {
// Connection status
sdk.addEventListener('connected', () => {
console.log('Connected to signaling server');
});
sdk.addEventListener('disconnected', () => {
updateStatus('Disconnected', false);
connectedPeers.clear();
updatePeersCount();
});
// Peer events
sdk.addEventListener('peerConnected', (event) => {
const peerId = event.detail.uuid;
connectedPeers.add(peerId);
updatePeersCount();
console.log('Peer connected:', peerId);
// Send current clipboard content to new peer
sendCurrentContent(peerId);
});
sdk.addEventListener('peerDisconnected', (event) => {
const peerId = event.detail.uuid;
connectedPeers.delete(peerId);
updatePeersCount();
console.log('Peer disconnected:', peerId);
});
// Data channel events
sdk.addEventListener('dataChannelOpen', (event) => {
console.log('Data channel opened with:', event.detail.uuid);
});
// Handle incoming data
sdk.addEventListener('dataReceived', (event) => {
handleIncomingData(event.detail);
});
// Handle room listings
sdk.addEventListener('listing', async (event) => {
if (event.detail.list && event.detail.list.length > 0) {
console.log('Found peers in room:', event.detail.list.length);
// Connect to other peers in mesh mode
for (const peer of event.detail.list) {
if (peer.streamID && peer.streamID !== streamId) {
try {
await sdk.quickView({
streamID: peer.streamID,
password: false,
audio: false,
video: false
});
console.log('Connected to peer:', peer.streamID);
} catch (error) {
console.error('Failed to connect to peer:', peer.streamID, error);
}
}
}
}
});
}
function handleTextareaChange() {
if (isUpdatingFromRemote) return;
const textarea = document.getElementById('sharedClipboard');
const content = textarea.value;
// Update character count
document.getElementById('charCount').textContent = content.length + ' characters';
// Debounce sync to avoid too many messages
clearTimeout(syncDebounceTimer);
syncDebounceTimer = setTimeout(() => {
sendContentUpdate(content);
}, 100); // 100ms debounce
}
function sendContentUpdate(content) {
if (!sdk || connectedPeers.size === 0) return;
const message = {
type: 'clipboard-update',
content: content,
timestamp: Date.now(),
sender: streamId
};
// Send to all connected peers
sdk.sendData(message);
console.log('Sent content update to peers');
}
function sendCurrentContent(peerId) {
if (!sdk) return;
const textarea = document.getElementById('sharedClipboard');
const message = {
type: 'clipboard-sync',
content: textarea.value,
timestamp: Date.now(),
sender: streamId
};
// Send to specific peer
sdk.sendData(message, peerId);
console.log('Sent current content to peer:', peerId);
}
function handleIncomingData(detail) {
const { data, uuid } = detail;
if (!data || typeof data !== 'object') return;
if (data.type === 'clipboard-update' || data.type === 'clipboard-sync') {
// Update textarea with received content
isUpdatingFromRemote = true;
const textarea = document.getElementById('sharedClipboard');
textarea.value = data.content || '';
// Update character count
document.getElementById('charCount').textContent = textarea.value.length + ' characters';
// Reset flag after a short delay
setTimeout(() => {
isUpdatingFromRemote = false;
}, 50);
console.log('Received content update from:', data.sender);
}
}
function generateRoomId() {
// Generate a readable room ID
const adjectives = ['quick', 'bright', 'swift', 'smart', 'cool', 'neat', 'fast', 'sharp'];
const nouns = ['fox', 'hawk', 'wolf', 'bear', 'lion', 'eagle', 'shark', 'tiger'];
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
const num = Math.floor(Math.random() * 1000);
return `${adj}-${noun}-${num}`;
}
function updateStatus(text, isConnected) {
const indicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
statusText.textContent = text;
if (isConnected) {
indicator.classList.add('connected');
} else {
indicator.classList.remove('connected');
}
}
function updatePeersCount() {
const peersCount = document.getElementById('peersCount');
const count = connectedPeers.size;
if (count === 0) {
peersCount.textContent = '';
} else if (count === 1) {
peersCount.textContent = '• 1 device connected';
} else {
peersCount.textContent = `${count} devices connected`;
}
}
function copyShareLink() {
const shareLink = document.getElementById('shareLink');
shareLink.select();
document.execCommand('copy');
// Update button text temporarily
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('copied');
}, 2000);
}
function copyClipboardContent() {
const textarea = document.getElementById('sharedClipboard');
textarea.select();
document.execCommand('copy');
// Update button text temporarily
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('copied');
}, 2000);
}
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (sdk) {
sdk.disconnect();
}
});
</script>
</body>
</html>