This commit is contained in:
steveseguin
2025-08-26 00:49:40 -04:00
parent 6ea075b67c
commit 45f80890fa
36 changed files with 5435 additions and 299 deletions

791
clipboard.html Normal file
View File

@@ -0,0 +1,791 @@
<!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>