mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
.
This commit is contained in:
791
clipboard.html
Normal file
791
clipboard.html
Normal 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>
|
||||
Reference in New Issue
Block a user