Refactor examples into per-app directories with separate HTML, CSS, and JS
- Give each example its own directory: chat/, whiteboard/, sdk-demo/, hrpc-demo/, data-demo/, screenshare/ - Split each into index.html, style.css, and app.js (no inline script/style; CSP-friendly) - Remove old flat files (e.g. chat.html, whiteboard.html, whiteboard.js) - Update README, examples/README, and docs/HRPC to use new paths
This commit is contained in:
+14
-12
@@ -2,16 +2,18 @@
|
||||
|
||||
Open these in your browser **after** installing the BridgeSwarm extension and native host.
|
||||
|
||||
## Chat (`chat.html`)
|
||||
Each example lives in its own directory with separated concerns: **index.html** (structure), **style.css** (styles), **app.js** (logic). Open the directory’s **index.html** (e.g. `chat/index.html` or run a local server and go to `http://localhost:3000/chat/`).
|
||||
|
||||
## Chat (`chat/`)
|
||||
|
||||
A minimal P2P chat: join a topic, see peers, send and receive messages.
|
||||
|
||||
1. Open `chat.html` in Chrome (or Edge/Firefox) — e.g. drag the file into the browser or use **File → Open**.
|
||||
1. Open `chat/index.html` in Chrome (or Edge/Firefox) — e.g. drag the file into the browser or use **File → Open**.
|
||||
2. Click **Join** (default topic is `bridge-swarm-demo`).
|
||||
3. Open the same file in another tab (or another window/device) and join the same topic.
|
||||
4. Type a message and click **Send**; it appears in the other tab.
|
||||
|
||||
## SDK Demo (`sdk-demo.html`)
|
||||
## SDK Demo (`sdk-demo/`)
|
||||
|
||||
A complete example of all BridgeSwarm SDK features:
|
||||
|
||||
@@ -20,33 +22,33 @@ A complete example of all BridgeSwarm SDK features:
|
||||
3. **Raw messages** — `conn.write(data)` and `conn.on('data')` for raw bytes.
|
||||
4. **Protomux** — `swarm.createProtomux(conn)`, create a channel with protocol `sdk-demo/v1`, add string and binary messages via `compact-encoding` (`c.string`, `c.binary`), `channel.open()`, and `msg.send()`.
|
||||
|
||||
Open `sdk-demo.html` in two tabs (or two devices), join the same topic, then try sending raw messages and Protomux messages. The log shows which path each message used.
|
||||
Open `sdk-demo/index.html` in two tabs (or two devices), join the same topic, then try sending raw messages and Protomux messages. The log shows which path each message used.
|
||||
|
||||
## HRPC Demo (`hrpc-demo.html`)
|
||||
## HRPC Demo (`hrpc-demo/`)
|
||||
|
||||
Demonstrates **HRPC** on a connection: HRPC is **auto-enabled** on each connection. Open the page in two tabs, join the same topic; when a peer connects, the host enables HRPC and either tab can use “Ping peer” to get a pong. No manual enable step. See [../docs/HRPC.md](../docs/HRPC.md).
|
||||
|
||||
## Whiteboard (`whiteboard.html`)
|
||||
## Whiteboard (`whiteboard/`)
|
||||
|
||||
Collaborative P2P whiteboard: join a topic, draw on the canvas, and strokes sync to all peers in real time. Open `whiteboard.html` in two tabs (or two devices), join the same topic, then draw; strokes and “Clear board” are broadcast to everyone. Uses raw `conn.write()` / `conn.on('data')` with JSON stroke and clear messages.
|
||||
Collaborative P2P whiteboard: join a topic, draw on the canvas, and strokes sync to all peers in real time. Open `whiteboard/index.html` in two tabs (or two devices), join the same topic, then draw; strokes and “Clear board” are broadcast to everyone. Uses raw `conn.write()` / `conn.on('data')` with JSON stroke and clear messages.
|
||||
|
||||
## Screenshare (`screenshare.html`)
|
||||
## Screenshare (`screenshare/`)
|
||||
|
||||
P2P screen sharing using **WebRTC** for the media stream and **BridgeSwarm for signaling** (no separate signaling server). SDP offers/answers and ICE candidates are exchanged over the BridgeSwarm connection.
|
||||
|
||||
1. Open `screenshare.html` in two tabs (or two windows).
|
||||
1. Open `screenshare/index.html` in two tabs (or two windows).
|
||||
2. **Tab 1 (sharer):** Click **Join**, then **Share my screen** and pick a screen or window to share.
|
||||
3. **Tab 2 (viewer):** Join the same topic; the shared screen appears in the video area.
|
||||
|
||||
Signaling uses BridgeSwarm; the actual video flows via the browser's WebRTC (RTCPeerConnection). No public STUN or TURN is used — fully enclosed P2P; ICE uses only host and local-network candidates. The sharer can start sharing before any viewer joins; viewers see the stream when they join the topic.
|
||||
|
||||
## Data API Demo (`data-demo.html`)
|
||||
## Data API Demo (`data-demo/`)
|
||||
|
||||
Uses the native host’s **Hypercore**, **Hyperbee**, **Hyperdrive**, and **Autobase** via `BridgeSwarm.request(type, payload)`.
|
||||
|
||||
1. Open `data-demo.html` in the browser (with the extension installed).
|
||||
1. Open `data-demo/index.html` in the browser (with the extension installed).
|
||||
2. Use the buttons to get core info, append to the core, put/get/del in Hyperbee, put/get/list/delete files in Hyperdrive, and append/read the Autobase linearized view.
|
||||
|
||||
See [../docs/DATA-API.md](../docs/DATA-API.md) for the full command reference.
|
||||
|
||||
**Note:** If the page says the extension is not detected: (1) The page waits a few seconds for the extension to inject—try again after a moment. (2) For `file://` URLs, open the extension’s **Options** and ensure “Do not inject on file:// URLs” is *off*, then in `chrome://extensions` (or the browser’s extension page) find BridgeSwarm and enable **Allow access to file URLs**. (3) Or run a local server (e.g. `npx serve .` in the `examples` folder) and open `http://localhost:3000/chat.html`.
|
||||
**Note:** If the page says the extension is not detected: (1) The page waits a few seconds for the extension to inject—try again after a moment. (2) For `file://` URLs, open the extension’s **Options** and ensure “Do not inject on file:// URLs” is *off*, then in `chrome://extensions` (or the browser’s extension page) find BridgeSwarm and enable **Allow access to file URLs**. (3) Or run a local server (e.g. `npx serve .` in the `examples` folder) and open `http://localhost:3000/chat/`.
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Chat</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; color: #7aa2f7; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; align-items: center; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status {
|
||||
font-size: 0.85rem;
|
||||
color: #9ece6a;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.status.error { color: #f7768e; }
|
||||
.log {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
height: 220px;
|
||||
overflow-y: auto;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.log .peer { color: #bb9af7; }
|
||||
.log .msg { color: #9ece6a; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.peers { font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Chat</h1>
|
||||
<p class="status" id="status">Load the extension, then join a topic. Open this page in another tab or device to chat.</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (e.g. my-chat-room)" value="bridge-swarm-demo">
|
||||
<button class="primary" id="btnJoin">Join</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="row">
|
||||
<input type="text" id="message" placeholder="Type a message..." disabled>
|
||||
<button class="primary" id="btnSend" disabled>Send</button>
|
||||
</div>
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
const messageEl = document.getElementById('message');
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const btnSend = document.getElementById('btnSend');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'status' + (isError ? ' error' : '');
|
||||
}
|
||||
|
||||
function log(msg, type) {
|
||||
const line = document.createElement('div');
|
||||
line.className = type || 'sys';
|
||||
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
(function initWhenReady(attempts) {
|
||||
if (attempts >= 50) {
|
||||
setStatus('BridgeSwarm extension not detected. Install the extension and reload, or wait a moment and refresh.', true);
|
||||
return;
|
||||
}
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Waiting for extension…');
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
setStatus('Load the extension, then join a topic. Open this page in another tab or device to chat.');
|
||||
}).catch(function () {
|
||||
setStatus('BridgeSwarm extension not detected.', true);
|
||||
});
|
||||
})(0);
|
||||
|
||||
let swarm = null;
|
||||
const connections = [];
|
||||
|
||||
function updatePeers() {
|
||||
peersEl.textContent = 'Peers: ' + connections.length;
|
||||
}
|
||||
|
||||
btnJoin.addEventListener('click', async () => {
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Extension not ready yet. Wait a moment and try again.', true);
|
||||
return;
|
||||
}
|
||||
const topic = topicEl.value.trim() || 'default-topic';
|
||||
if (swarm) return;
|
||||
try {
|
||||
setStatus('Joining topic "' + topic + '"...');
|
||||
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-chat' });
|
||||
swarm.on('connection', (conn, peerInfo) => {
|
||||
connections.push({ conn, peerInfo });
|
||||
updatePeers();
|
||||
log('Peer connected: ' + (peerInfo.publicKey || '').slice(0, 16) + '…', 'peer');
|
||||
conn.on('data', (data) => {
|
||||
const text = new TextDecoder().decode(data);
|
||||
log('Received: ' + text, 'msg');
|
||||
});
|
||||
conn.on('end', () => {
|
||||
const i = connections.findIndex(c => c.conn === conn);
|
||||
if (i !== -1) connections.splice(i, 1);
|
||||
updatePeers();
|
||||
log('Peer left', 'sys');
|
||||
});
|
||||
conn.on('error', (err) => log('Peer error: ' + err.message, 'err'));
|
||||
});
|
||||
await swarm.join(topic);
|
||||
setStatus('Joined "' + topic + '". Send a message or open this page in another tab.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
messageEl.disabled = false;
|
||||
btnSend.disabled = false;
|
||||
log('Joined topic: ' + topic, 'sys');
|
||||
} catch (err) {
|
||||
setStatus('Error: ' + err.message, true);
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
btnLeave.addEventListener('click', async () => {
|
||||
if (!swarm) return;
|
||||
try {
|
||||
const topic = topicEl.value.trim() || 'default-topic';
|
||||
await swarm.leave(topic);
|
||||
await swarm.destroy();
|
||||
connections.length = 0;
|
||||
updatePeers();
|
||||
swarm = null;
|
||||
setStatus('Left topic. You can join again.');
|
||||
btnJoin.disabled = false;
|
||||
btnLeave.disabled = true;
|
||||
messageEl.disabled = true;
|
||||
btnSend.disabled = true;
|
||||
log('Left topic', 'sys');
|
||||
} catch (err) {
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const text = messageEl.value.trim();
|
||||
if (!text || !swarm || connections.length === 0) return;
|
||||
const data = new TextEncoder().encode(text);
|
||||
connections.forEach(({ conn }) => {
|
||||
try { conn.write(data); } catch (e) {}
|
||||
});
|
||||
log('Sent: ' + text, 'msg');
|
||||
messageEl.value = '';
|
||||
}
|
||||
|
||||
btnSend.addEventListener('click', sendMessage);
|
||||
messageEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendMessage(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,120 @@
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
const messageEl = document.getElementById('message');
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const btnSend = document.getElementById('btnSend');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'status' + (isError ? ' error' : '');
|
||||
}
|
||||
|
||||
function log(msg, type) {
|
||||
const line = document.createElement('div');
|
||||
line.className = type || 'sys';
|
||||
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
(function initWhenReady(attempts) {
|
||||
if (attempts >= 50) {
|
||||
setStatus('BridgeSwarm extension not detected. Install the extension and reload, or wait a moment and refresh.', true);
|
||||
return;
|
||||
}
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Waiting for extension…');
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
setStatus('Load the extension, then join a topic. Open this page in another tab or device to chat.');
|
||||
}).catch(function () {
|
||||
setStatus('BridgeSwarm extension not detected.', true);
|
||||
});
|
||||
})(0);
|
||||
|
||||
let swarm = null;
|
||||
const connections = [];
|
||||
|
||||
function updatePeers() {
|
||||
peersEl.textContent = 'Peers: ' + connections.length;
|
||||
}
|
||||
|
||||
btnJoin.addEventListener('click', async () => {
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Extension not ready yet. Wait a moment and try again.', true);
|
||||
return;
|
||||
}
|
||||
const topic = topicEl.value.trim() || 'default-topic';
|
||||
if (swarm) return;
|
||||
try {
|
||||
setStatus('Joining topic "' + topic + '"...');
|
||||
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-chat' });
|
||||
swarm.on('connection', (conn, peerInfo) => {
|
||||
connections.push({ conn, peerInfo });
|
||||
updatePeers();
|
||||
log('Peer connected: ' + (peerInfo.publicKey || '').slice(0, 16) + '…', 'peer');
|
||||
conn.on('data', (data) => {
|
||||
const text = new TextDecoder().decode(data);
|
||||
log('Received: ' + text, 'msg');
|
||||
});
|
||||
conn.on('end', () => {
|
||||
const i = connections.findIndex(c => c.conn === conn);
|
||||
if (i !== -1) connections.splice(i, 1);
|
||||
updatePeers();
|
||||
log('Peer left', 'sys');
|
||||
});
|
||||
conn.on('error', (err) => log('Peer error: ' + err.message, 'err'));
|
||||
});
|
||||
await swarm.join(topic);
|
||||
setStatus('Joined "' + topic + '". Send a message or open this page in another tab.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
messageEl.disabled = false;
|
||||
btnSend.disabled = false;
|
||||
log('Joined topic: ' + topic, 'sys');
|
||||
} catch (err) {
|
||||
setStatus('Error: ' + err.message, true);
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
btnLeave.addEventListener('click', async () => {
|
||||
if (!swarm) return;
|
||||
try {
|
||||
const topic = topicEl.value.trim() || 'default-topic';
|
||||
await swarm.leave(topic);
|
||||
await swarm.destroy();
|
||||
connections.length = 0;
|
||||
updatePeers();
|
||||
swarm = null;
|
||||
setStatus('Left topic. You can join again.');
|
||||
btnJoin.disabled = false;
|
||||
btnLeave.disabled = true;
|
||||
messageEl.disabled = true;
|
||||
btnSend.disabled = true;
|
||||
log('Left topic', 'sys');
|
||||
} catch (err) {
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const text = messageEl.value.trim();
|
||||
if (!text || !swarm || connections.length === 0) return;
|
||||
const data = new TextEncoder().encode(text);
|
||||
connections.forEach(({ conn }) => {
|
||||
try { conn.write(data); } catch (e) {}
|
||||
});
|
||||
log('Sent: ' + text, 'msg');
|
||||
messageEl.value = '';
|
||||
}
|
||||
|
||||
btnSend.addEventListener('click', sendMessage);
|
||||
messageEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendMessage(); });
|
||||
})();
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Chat</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Chat</h1>
|
||||
<p class="status" id="status">Load the extension, then join a topic. Open this page in another tab or device to chat.</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (e.g. my-chat-room)" value="bridge-swarm-demo">
|
||||
<button class="primary" id="btnJoin">Join</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="row">
|
||||
<input type="text" id="message" placeholder="Type a message..." disabled>
|
||||
<button class="primary" id="btnSend" disabled>Send</button>
|
||||
</div>
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; color: #7aa2f7; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; align-items: center; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status {
|
||||
font-size: 0.85rem;
|
||||
color: #9ece6a;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.status.error { color: #f7768e; }
|
||||
.log {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
height: 220px;
|
||||
overflow-y: auto;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.log .peer { color: #bb9af7; }
|
||||
.log .msg { color: #9ece6a; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.peers { font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.5rem; }
|
||||
@@ -1,129 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Data API Demo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
|
||||
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
|
||||
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.status.ok { color: #9ece6a; }
|
||||
.status.error { color: #f7768e; }
|
||||
.log {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
height: 220px;
|
||||
overflow-y: auto;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.log .val { color: #9ece6a; }
|
||||
.section { margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Data API Demo</h1>
|
||||
<p>Uses <code>BridgeSwarm.request(type, payload)</code> to call Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb in the native host.</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hypercore</h2>
|
||||
<div class="row">
|
||||
<button class="primary" id="coreInfo">Core info</button>
|
||||
<input type="text" id="coreAppend" placeholder="Data to append (text)">
|
||||
<button class="secondary" id="coreAppendBtn">Append</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hyperbee (key/value)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="beeKey" placeholder="Key">
|
||||
<input type="text" id="beeValue" placeholder="Value">
|
||||
<button class="primary" id="beePut">Put</button>
|
||||
<button class="secondary" id="beeGet">Get</button>
|
||||
<button class="secondary" id="beeDel">Del</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hyperdrive (files)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="drivePath" placeholder="Path (e.g. /foo.txt)" value="/readme.txt">
|
||||
<input type="text" id="driveContent" placeholder="Content to write">
|
||||
<button class="primary" id="drivePut">Put</button>
|
||||
<button class="secondary" id="driveGet">Get</button>
|
||||
<button class="secondary" id="driveList">List /</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Autobase (log)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="autobaseValue" placeholder="Value to append">
|
||||
<button class="primary" id="autobaseAppend">Append</button>
|
||||
<button class="secondary" id="autobaseInfo">Info</button>
|
||||
<button class="secondary" id="autobaseView">Read view (0..4)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hyperdb (records)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="hyperdbId" placeholder="Id">
|
||||
<input type="text" id="hyperdbValue" placeholder="Value">
|
||||
<button class="primary" id="hyperdbPut">Put</button>
|
||||
<button class="secondary" id="hyperdbGet">Get</button>
|
||||
<button class="secondary" id="hyperdbDel">Del</button>
|
||||
<button class="secondary" id="hyperdbFind">Find all</button>
|
||||
<button class="secondary" id="hyperdbFlush">Flush</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Log</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const logEl = document.getElementById('log');
|
||||
function log(msg, type) {
|
||||
@@ -298,6 +172,3 @@
|
||||
}).catch(function () { log('BridgeSwarm not available.', 'err'); });
|
||||
})(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Data API Demo</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Data API Demo</h1>
|
||||
<p>Uses <code>BridgeSwarm.request(type, payload)</code> to call Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb in the native host.</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hypercore</h2>
|
||||
<div class="row">
|
||||
<button class="primary" id="coreInfo">Core info</button>
|
||||
<input type="text" id="coreAppend" placeholder="Data to append (text)">
|
||||
<button class="secondary" id="coreAppendBtn">Append</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hyperbee (key/value)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="beeKey" placeholder="Key">
|
||||
<input type="text" id="beeValue" placeholder="Value">
|
||||
<button class="primary" id="beePut">Put</button>
|
||||
<button class="secondary" id="beeGet">Get</button>
|
||||
<button class="secondary" id="beeDel">Del</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hyperdrive (files)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="drivePath" placeholder="Path (e.g. /foo.txt)" value="/readme.txt">
|
||||
<input type="text" id="driveContent" placeholder="Content to write">
|
||||
<button class="primary" id="drivePut">Put</button>
|
||||
<button class="secondary" id="driveGet">Get</button>
|
||||
<button class="secondary" id="driveList">List /</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Autobase (log)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="autobaseValue" placeholder="Value to append">
|
||||
<button class="primary" id="autobaseAppend">Append</button>
|
||||
<button class="secondary" id="autobaseInfo">Info</button>
|
||||
<button class="secondary" id="autobaseView">Read view (0..4)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Hyperdb (records)</h2>
|
||||
<div class="row">
|
||||
<input type="text" id="hyperdbId" placeholder="Id">
|
||||
<input type="text" id="hyperdbValue" placeholder="Value">
|
||||
<button class="primary" id="hyperdbPut">Put</button>
|
||||
<button class="secondary" id="hyperdbGet">Get</button>
|
||||
<button class="secondary" id="hyperdbDel">Del</button>
|
||||
<button class="secondary" id="hyperdbFind">Find all</button>
|
||||
<button class="secondary" id="hyperdbFlush">Flush</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Log</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 640px; margin: 0 auto; padding: 1rem; background: #1a1b26; color: #c0caf5; min-height: 100vh; }
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
|
||||
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
|
||||
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] { flex: 1; min-width: 100px; padding: 0.5rem 0.75rem; border: 1px solid #3b4261; border-radius: 6px; background: #24283b; color: #c0caf5; font-size: 0.9rem; }
|
||||
button { padding: 0.5rem 0.75rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; }
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.status.ok { color: #9ece6a; }
|
||||
.status.error { color: #f7768e; }
|
||||
.log { background: #24283b; border: 1px solid #3b4261; border-radius: 6px; padding: 0.6rem; height: 220px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.75rem; line-height: 1.35; margin-top: 0.5rem; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.log .val { color: #9ece6a; }
|
||||
.section { margin-bottom: 1rem; }
|
||||
@@ -1,112 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – HRPC Demo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
|
||||
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
|
||||
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.status.ok { color: #9ece6a; }
|
||||
.status.error { color: #f7768e; }
|
||||
.status.warn { color: #e0af68; }
|
||||
.log {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
height: 220px;
|
||||
overflow-y: auto;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.log .peer { color: #bb9af7; }
|
||||
.log .hrpc { color: #7dcfff; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.section { margin-bottom: 1rem; }
|
||||
.badge { font-size: 0.7rem; padding: 0.15rem 0.4rem; border-radius: 4px; background: #3b4261; color: #a9b1d6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – HRPC Demo</h1>
|
||||
<p>HRPC is <strong>auto-enabled</strong> on every connection. Open this page in two tabs, join the same topic in both; once connected, you can "Ping peer" in either tab (no manual enable step).</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Swarm <span class="badge">join / leave</span></h2>
|
||||
<p id="status" class="status">Waiting for extension…</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic" value="bridge-swarm-hrpc-demo">
|
||||
<button class="primary" id="btnJoin">Join topic</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave & destroy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Connection & HRPC <span class="badge">attachHrpc, hrpcInvoke</span></h2>
|
||||
<p id="peers">Peers: 0</p>
|
||||
<div class="row" id="connSelectRow" style="display:none;">
|
||||
<label for="connSelect" style="font-size:0.85rem; color:#a9b1d6;">Use connection:</label>
|
||||
<select id="connSelect" style="min-width:180px; padding:0.35rem 0.5rem; border-radius:6px; border:1px solid #3b4261; background:#24283b; color:#c0caf5; font-size:0.85rem;"></select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="secondary" id="btnEnableHrpc" disabled>Retry HRPC on selected connection</button>
|
||||
</div>
|
||||
<p id="hrpcStatus" class="status" style="display:none;"></p>
|
||||
<div class="row" id="pingRow" style="display:none;">
|
||||
<input type="text" id="pingValue" placeholder="Ping value (optional)">
|
||||
<button class="primary" id="btnPing">Ping peer</button>
|
||||
</div>
|
||||
<p id="pongResult" class="status" style="display:none;"></p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Log</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
@@ -396,6 +287,3 @@
|
||||
btnPing.disabled = false;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – HRPC Demo</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – HRPC Demo</h1>
|
||||
<p>HRPC is <strong>auto-enabled</strong> on every connection. Open this page in two tabs, join the same topic in both; once connected, you can "Ping peer" in either tab (no manual enable step).</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Swarm <span class="badge">join / leave</span></h2>
|
||||
<p id="status" class="status">Waiting for extension…</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic" value="bridge-swarm-hrpc-demo">
|
||||
<button class="primary" id="btnJoin">Join topic</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave & destroy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Connection & HRPC <span class="badge">attachHrpc, hrpcInvoke</span></h2>
|
||||
<p id="peers">Peers: 0</p>
|
||||
<div class="row" id="connSelectRow" style="display:none;">
|
||||
<label for="connSelect" style="font-size:0.85rem; color:#a9b1d6;">Use connection:</label>
|
||||
<select id="connSelect" style="min-width:180px; padding:0.35rem 0.5rem; border-radius:6px; border:1px solid #3b4261; background:#24283b; color:#c0caf5; font-size:0.85rem;"></select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="secondary" id="btnEnableHrpc" disabled>Retry HRPC on selected connection</button>
|
||||
</div>
|
||||
<p id="hrpcStatus" class="status" style="display:none;"></p>
|
||||
<div class="row" id="pingRow" style="display:none;">
|
||||
<input type="text" id="pingValue" placeholder="Ping value (optional)">
|
||||
<button class="primary" id="btnPing">Ping peer</button>
|
||||
</div>
|
||||
<p id="pongResult" class="status" style="display:none;"></p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Log</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 640px; margin: 0 auto; padding: 1rem; background: #1a1b26; color: #c0caf5; min-height: 100vh; }
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
|
||||
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
|
||||
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] { flex: 1; min-width: 120px; padding: 0.5rem 0.75rem; border: 1px solid #3b4261; border-radius: 6px; background: #24283b; color: #c0caf5; font-size: 0.9rem; }
|
||||
input::placeholder { color: #565f89; }
|
||||
button { padding: 0.5rem 0.75rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; }
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.status.ok { color: #9ece6a; }
|
||||
.status.error { color: #f7768e; }
|
||||
.status.warn { color: #e0af68; }
|
||||
.log { background: #24283b; border: 1px solid #3b4261; border-radius: 6px; padding: 0.6rem; height: 220px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.75rem; line-height: 1.35; margin-top: 0.5rem; }
|
||||
.log .peer { color: #bb9af7; }
|
||||
.log .hrpc { color: #7dcfff; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.section { margin-bottom: 1rem; }
|
||||
.badge { font-size: 0.7rem; padding: 0.15rem 0.4rem; border-radius: 4px; background: #3b4261; color: #a9b1d6; }
|
||||
@@ -1,124 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Screenshare</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; color: #7aa2f7; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; color: #9ece6a; margin-bottom: 0.5rem; }
|
||||
.status.error { color: #f7768e; }
|
||||
.peers { font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.5rem; }
|
||||
.video-wrap {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
min-height: 360px;
|
||||
aspect-ratio: 16/10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.video-wrap video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
#remoteVideo { display: none; }
|
||||
.placeholder-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.video-wrap .placeholder {
|
||||
color: #565f89;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.stream-state { font-size: 0.85rem; color: #a9b1d6; margin: 0.25rem 0 0; min-height: 1.2em; }
|
||||
.preview-wrap {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: #a9b1d6;
|
||||
}
|
||||
.preview-wrap video {
|
||||
max-width: 160px;
|
||||
max-height: 90px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #3b4261;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Screenshare</h1>
|
||||
<p class="status" id="status">Load the extension, then join a topic. Share your screen or watch another tab’s stream.</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (e.g. my-screenshare)" value="bridge-swarm-screenshare">
|
||||
<button class="primary" id="btnJoin">Join</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="row">
|
||||
<button class="primary" id="btnShare" disabled>Share my screen</button>
|
||||
<button class="secondary" id="btnStopShare" disabled style="display: none;">Stop sharing</button>
|
||||
</div>
|
||||
<div class="video-wrap" id="videoWrap">
|
||||
<video id="remoteVideo" autoplay playsinline muted controls></video>
|
||||
<div class="placeholder-block" id="placeholderBlock">
|
||||
<span class="placeholder" id="videoPlaceholder">Join a topic. Share your screen, or wait for a peer to share.</span>
|
||||
<button class="primary" id="btnPlay" style="display: none;">Play</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="secondary" id="btnPip" disabled style="display: none;">Picture-in-Picture</button>
|
||||
</div>
|
||||
<p class="stream-state" id="streamState"></p>
|
||||
<div class="preview-wrap" id="previewWrap" style="display: none;">
|
||||
<span>Your shared screen (local preview):</span>
|
||||
<video id="localPreview" autoplay playsinline muted controls></video>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
@@ -473,6 +352,3 @@
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Screenshare</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Screenshare</h1>
|
||||
<p class="status" id="status">Load the extension, then join a topic. Share your screen or watch another tab's stream.</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (e.g. my-screenshare)" value="bridge-swarm-screenshare">
|
||||
<button class="primary" id="btnJoin">Join</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="row">
|
||||
<button class="primary" id="btnShare" disabled>Share my screen</button>
|
||||
<button class="secondary" id="btnStopShare" disabled style="display: none;">Stop sharing</button>
|
||||
</div>
|
||||
<div class="video-wrap" id="videoWrap">
|
||||
<video id="remoteVideo" autoplay playsinline muted controls></video>
|
||||
<div class="placeholder-block" id="placeholderBlock">
|
||||
<span class="placeholder" id="videoPlaceholder">Join a topic. Share your screen, or wait for a peer to share.</span>
|
||||
<button class="primary" id="btnPlay" style="display: none;">Play</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="secondary" id="btnPip" disabled style="display: none;">Picture-in-Picture</button>
|
||||
</div>
|
||||
<p class="stream-state" id="streamState"></p>
|
||||
<div class="preview-wrap" id="previewWrap" style="display: none;">
|
||||
<span>Your shared screen (local preview):</span>
|
||||
<video id="localPreview" autoplay playsinline muted controls></video>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 0 auto; padding: 1rem; background: #1a1b26; color: #c0caf5; min-height: 100vh; }
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; color: #7aa2f7; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] { flex: 1; min-width: 140px; padding: 0.5rem 0.75rem; border: 1px solid #3b4261; border-radius: 6px; background: #24283b; color: #c0caf5; font-size: 0.95rem; }
|
||||
input::placeholder { color: #565f89; }
|
||||
button { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.9rem; cursor: pointer; font-weight: 500; }
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; color: #9ece6a; margin-bottom: 0.5rem; }
|
||||
.status.error { color: #f7768e; }
|
||||
.peers { font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.5rem; }
|
||||
.video-wrap { background: #24283b; border: 1px solid #3b4261; border-radius: 6px; overflow: hidden; margin-top: 0.5rem; min-height: 360px; aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
|
||||
.video-wrap video { max-width: 100%; max-height: 100%; object-fit: contain; }
|
||||
#remoteVideo { display: none; }
|
||||
.placeholder-block { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; }
|
||||
.video-wrap .placeholder { color: #565f89; font-size: 0.9rem; }
|
||||
.stream-state { font-size: 0.85rem; color: #a9b1d6; margin: 0.25rem 0 0; min-height: 1.2em; }
|
||||
.preview-wrap { margin-top: 0.5rem; font-size: 0.85rem; color: #a9b1d6; }
|
||||
.preview-wrap video { max-width: 160px; max-height: 90px; border-radius: 4px; border: 1px solid #3b4261; margin-top: 0.25rem; }
|
||||
@@ -1,312 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – SDK Demo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
|
||||
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
|
||||
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.status.ok { color: #9ece6a; }
|
||||
.status.error { color: #f7768e; }
|
||||
.status.warn { color: #e0af68; }
|
||||
.log {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.log .peer { color: #bb9af7; }
|
||||
.log .raw { color: #9ece6a; }
|
||||
.log .protomux { color: #7dcfff; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.section { margin-bottom: 1rem; }
|
||||
.badge { font-size: 0.7rem; padding: 0.15rem 0.4rem; border-radius: 4px; background: #3b4261; color: #a9b1d6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – SDK Demo</h1>
|
||||
<p>This page demonstrates all SDK features: swarm, connections, raw messages, and Protomux. Open in two tabs (or two devices) and join the same topic.</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Swarm <span class="badge">join / leave / destroy</span></h2>
|
||||
<p id="status" class="status">Waiting for extension…</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (32 bytes or string)" value="bridge-swarm-sdk-demo">
|
||||
<button class="primary" id="btnJoin">Join topic</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave & destroy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Connections <span class="badge">peerInfo, connection events</span></h2>
|
||||
<p id="peers">Peers: 0</p>
|
||||
<p class="status ok" id="connStatus" style="display:none;">On connection you get (conn, peerInfo). peerInfo: publicKey (hex), topics (hex[]).</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>3. Raw messages <span class="badge">conn.write / on('data')</span></h2>
|
||||
<div class="row">
|
||||
<input type="text" id="rawMsg" placeholder="Send raw bytes (text)…" disabled>
|
||||
<button class="secondary" id="btnRawSend" disabled>Send raw</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>4. Protomux <span class="badge">createProtomux, channel, c.string / c.binary</span></h2>
|
||||
<div class="row">
|
||||
<input type="text" id="protoMsg" placeholder="Send via Protomux (string)…" disabled>
|
||||
<button class="secondary" id="btnProtoSend" disabled>Send Protomux</button>
|
||||
</div>
|
||||
<p class="status ok" id="protoStatus" style="display:none;">Channel protocol: sdk-demo/v1. Binary messages supported too.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Log</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const connStatusEl = document.getElementById('connStatus');
|
||||
const rawMsgEl = document.getElementById('rawMsg');
|
||||
const btnRawSend = document.getElementById('btnRawSend');
|
||||
const protoMsgEl = document.getElementById('protoMsg');
|
||||
const btnProtoSend = document.getElementById('btnProtoSend');
|
||||
const protoStatusEl = document.getElementById('protoStatus');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function setStatus(msg, className) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'status ' + (className || 'ok');
|
||||
}
|
||||
|
||||
function log(msg, type) {
|
||||
const line = document.createElement('div');
|
||||
line.className = type || 'sys';
|
||||
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
(function initWhenReady(attempts) {
|
||||
if (attempts >= 50) {
|
||||
setStatus('BridgeSwarm extension not detected. Install it and reload.', 'error');
|
||||
return;
|
||||
}
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Waiting for extension…', 'warn');
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
setStatus('Ready. Join a topic to start.');
|
||||
}).catch(function () {
|
||||
setStatus('BridgeSwarm extension not detected.', 'error');
|
||||
});
|
||||
})(0);
|
||||
|
||||
let swarm = null;
|
||||
const connectionEntries = [];
|
||||
|
||||
function updatePeers() {
|
||||
peersEl.textContent = 'Peers: ' + connectionEntries.length;
|
||||
if (connectionEntries.length > 0) connStatusEl.style.display = '';
|
||||
}
|
||||
|
||||
function enableConnected() {
|
||||
rawMsgEl.disabled = false;
|
||||
btnRawSend.disabled = false;
|
||||
protoMsgEl.disabled = false;
|
||||
btnProtoSend.disabled = false;
|
||||
}
|
||||
|
||||
function disableConnected() {
|
||||
rawMsgEl.disabled = true;
|
||||
btnRawSend.disabled = true;
|
||||
protoMsgEl.disabled = true;
|
||||
btnProtoSend.disabled = true;
|
||||
}
|
||||
|
||||
btnJoin.addEventListener('click', async function () {
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Extension not ready.', 'error');
|
||||
return;
|
||||
}
|
||||
const topic = topicEl.value.trim() || 'sdk-demo';
|
||||
if (swarm) return;
|
||||
try {
|
||||
setStatus('Joining "' + topic + '"…');
|
||||
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-sdk-demo' });
|
||||
|
||||
swarm.on('connection', function (conn, peerInfo) {
|
||||
const keyShort = (peerInfo.publicKey || '').slice(0, 16) + '…';
|
||||
log('Connection: peer ' + keyShort + ', topics: ' + (peerInfo.topics ? peerInfo.topics.length : 0), 'peer');
|
||||
conn.on('end', function () {
|
||||
const i = connectionEntries.findIndex(function (e) { return e.conn === conn; });
|
||||
if (i !== -1) connectionEntries.splice(i, 1);
|
||||
updatePeers();
|
||||
if (connectionEntries.length === 0) disableConnected();
|
||||
log('Peer left', 'sys');
|
||||
});
|
||||
conn.on('error', function (err) { log('Peer error: ' + err.message, 'err'); });
|
||||
|
||||
// Raw messages
|
||||
conn.on('data', function (data) {
|
||||
const text = new TextDecoder().decode(data);
|
||||
log('Raw received: ' + text, 'raw');
|
||||
});
|
||||
|
||||
const entry = { conn, peerInfo, mux: null, channel: null };
|
||||
connectionEntries.push(entry);
|
||||
updatePeers();
|
||||
enableConnected();
|
||||
|
||||
// Protomux: createProtomux(conn), createChannel, addMessage (c.string, c.binary), open(), msg.send()
|
||||
const mux = swarm.createProtomux(conn);
|
||||
if (mux && window.BridgeSwarmProtomux) {
|
||||
entry.mux = mux;
|
||||
const c = window.BridgeSwarmProtomux.c;
|
||||
const ch = mux.createChannel({
|
||||
protocol: 'sdk-demo/v1',
|
||||
onopen: function () { log('Protomux channel opened', 'protomux'); protoStatusEl.style.display = ''; },
|
||||
onclose: function () { log('Protomux channel closed', 'protomux'); }
|
||||
});
|
||||
entry.stringMsg = ch.addMessage({
|
||||
encoding: c.string,
|
||||
onmessage: function (m) { log('Protomux (string): ' + m, 'protomux'); }
|
||||
});
|
||||
ch.addMessage({
|
||||
encoding: c.binary,
|
||||
onmessage: function (buf) {
|
||||
const str = new TextDecoder().decode(buf);
|
||||
log('Protomux (binary): ' + str, 'protomux');
|
||||
}
|
||||
});
|
||||
ch.open();
|
||||
entry.channel = ch;
|
||||
} else {
|
||||
log('Protomux not available (ensure protomux-bundle.js is loaded)', 'err');
|
||||
}
|
||||
});
|
||||
|
||||
await swarm.join(topic);
|
||||
setStatus('Joined "' + topic + '". Connect another tab to try raw and Protomux messages.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
log('Joined topic: ' + topic, 'sys');
|
||||
} catch (err) {
|
||||
setStatus('Error: ' + err.message, 'error');
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
btnLeave.addEventListener('click', async function () {
|
||||
if (!swarm) return;
|
||||
try {
|
||||
const topic = topicEl.value.trim() || 'sdk-demo';
|
||||
await swarm.leave(topic);
|
||||
await swarm.destroy();
|
||||
connectionEntries.length = 0;
|
||||
updatePeers();
|
||||
disableConnected();
|
||||
swarm = null;
|
||||
setStatus('Left topic. You can join again.');
|
||||
btnJoin.disabled = false;
|
||||
btnLeave.disabled = true;
|
||||
log('Left topic and destroyed swarm', 'sys');
|
||||
} catch (err) {
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
function sendRaw() {
|
||||
const text = rawMsgEl.value.trim();
|
||||
if (!text || !swarm || connectionEntries.length === 0) return;
|
||||
const data = new TextEncoder().encode(text);
|
||||
connectionEntries.forEach(function (e) {
|
||||
try { e.conn.write(data); } catch (_) {}
|
||||
});
|
||||
log('Sent raw: ' + text, 'raw');
|
||||
rawMsgEl.value = '';
|
||||
}
|
||||
|
||||
function sendProtomux() {
|
||||
const text = protoMsgEl.value.trim();
|
||||
if (!text || connectionEntries.length === 0) return;
|
||||
let sent = 0;
|
||||
connectionEntries.forEach(function (e) {
|
||||
if (e.stringMsg) {
|
||||
try {
|
||||
e.stringMsg.send(text);
|
||||
sent++;
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
if (sent > 0) {
|
||||
log('Sent via Protomux: ' + text, 'protomux');
|
||||
protoMsgEl.value = '';
|
||||
} else {
|
||||
log('No Protomux channel ready', 'err');
|
||||
}
|
||||
}
|
||||
|
||||
btnRawSend.addEventListener('click', sendRaw);
|
||||
rawMsgEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') sendRaw(); });
|
||||
btnProtoSend.addEventListener('click', sendProtomux);
|
||||
protoMsgEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') sendProtomux(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,192 @@
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const connStatusEl = document.getElementById('connStatus');
|
||||
const rawMsgEl = document.getElementById('rawMsg');
|
||||
const btnRawSend = document.getElementById('btnRawSend');
|
||||
const protoMsgEl = document.getElementById('protoMsg');
|
||||
const btnProtoSend = document.getElementById('btnProtoSend');
|
||||
const protoStatusEl = document.getElementById('protoStatus');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function setStatus(msg, className) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'status ' + (className || 'ok');
|
||||
}
|
||||
|
||||
function log(msg, type) {
|
||||
const line = document.createElement('div');
|
||||
line.className = type || 'sys';
|
||||
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
(function initWhenReady(attempts) {
|
||||
if (attempts >= 50) {
|
||||
setStatus('BridgeSwarm extension not detected. Install it and reload.', 'error');
|
||||
return;
|
||||
}
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Waiting for extension…', 'warn');
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
setStatus('Ready. Join a topic to start.');
|
||||
}).catch(function () {
|
||||
setStatus('BridgeSwarm extension not detected.', 'error');
|
||||
});
|
||||
})(0);
|
||||
|
||||
let swarm = null;
|
||||
const connectionEntries = [];
|
||||
|
||||
function updatePeers() {
|
||||
peersEl.textContent = 'Peers: ' + connectionEntries.length;
|
||||
if (connectionEntries.length > 0) connStatusEl.style.display = '';
|
||||
}
|
||||
|
||||
function enableConnected() {
|
||||
rawMsgEl.disabled = false;
|
||||
btnRawSend.disabled = false;
|
||||
protoMsgEl.disabled = false;
|
||||
btnProtoSend.disabled = false;
|
||||
}
|
||||
|
||||
function disableConnected() {
|
||||
rawMsgEl.disabled = true;
|
||||
btnRawSend.disabled = true;
|
||||
protoMsgEl.disabled = true;
|
||||
btnProtoSend.disabled = true;
|
||||
}
|
||||
|
||||
btnJoin.addEventListener('click', async function () {
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Extension not ready.', 'error');
|
||||
return;
|
||||
}
|
||||
const topic = topicEl.value.trim() || 'sdk-demo';
|
||||
if (swarm) return;
|
||||
try {
|
||||
setStatus('Joining "' + topic + '"…');
|
||||
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-sdk-demo' });
|
||||
|
||||
swarm.on('connection', function (conn, peerInfo) {
|
||||
const keyShort = (peerInfo.publicKey || '').slice(0, 16) + '…';
|
||||
log('Connection: peer ' + keyShort + ', topics: ' + (peerInfo.topics ? peerInfo.topics.length : 0), 'peer');
|
||||
conn.on('end', function () {
|
||||
const i = connectionEntries.findIndex(function (e) { return e.conn === conn; });
|
||||
if (i !== -1) connectionEntries.splice(i, 1);
|
||||
updatePeers();
|
||||
if (connectionEntries.length === 0) disableConnected();
|
||||
log('Peer left', 'sys');
|
||||
});
|
||||
conn.on('error', function (err) { log('Peer error: ' + err.message, 'err'); });
|
||||
|
||||
conn.on('data', function (data) {
|
||||
const text = new TextDecoder().decode(data);
|
||||
log('Raw received: ' + text, 'raw');
|
||||
});
|
||||
|
||||
const entry = { conn, peerInfo, mux: null, channel: null };
|
||||
connectionEntries.push(entry);
|
||||
updatePeers();
|
||||
enableConnected();
|
||||
|
||||
const mux = swarm.createProtomux(conn);
|
||||
if (mux && window.BridgeSwarmProtomux) {
|
||||
entry.mux = mux;
|
||||
const c = window.BridgeSwarmProtomux.c;
|
||||
const ch = mux.createChannel({
|
||||
protocol: 'sdk-demo/v1',
|
||||
onopen: function () { log('Protomux channel opened', 'protomux'); protoStatusEl.style.display = ''; },
|
||||
onclose: function () { log('Protomux channel closed', 'protomux'); }
|
||||
});
|
||||
entry.stringMsg = ch.addMessage({
|
||||
encoding: c.string,
|
||||
onmessage: function (m) { log('Protomux (string): ' + m, 'protomux'); }
|
||||
});
|
||||
ch.addMessage({
|
||||
encoding: c.binary,
|
||||
onmessage: function (buf) {
|
||||
const str = new TextDecoder().decode(buf);
|
||||
log('Protomux (binary): ' + str, 'protomux');
|
||||
}
|
||||
});
|
||||
ch.open();
|
||||
entry.channel = ch;
|
||||
} else {
|
||||
log('Protomux not available (ensure protomux-bundle.js is loaded)', 'err');
|
||||
}
|
||||
});
|
||||
|
||||
await swarm.join(topic);
|
||||
setStatus('Joined "' + topic + '". Connect another tab to try raw and Protomux messages.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
log('Joined topic: ' + topic, 'sys');
|
||||
} catch (err) {
|
||||
setStatus('Error: ' + err.message, 'error');
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
btnLeave.addEventListener('click', async function () {
|
||||
if (!swarm) return;
|
||||
try {
|
||||
const topic = topicEl.value.trim() || 'sdk-demo';
|
||||
await swarm.leave(topic);
|
||||
await swarm.destroy();
|
||||
connectionEntries.length = 0;
|
||||
updatePeers();
|
||||
disableConnected();
|
||||
swarm = null;
|
||||
setStatus('Left topic. You can join again.');
|
||||
btnJoin.disabled = false;
|
||||
btnLeave.disabled = true;
|
||||
log('Left topic and destroyed swarm', 'sys');
|
||||
} catch (err) {
|
||||
log('Error: ' + err.message, 'err');
|
||||
}
|
||||
});
|
||||
|
||||
function sendRaw() {
|
||||
const text = rawMsgEl.value.trim();
|
||||
if (!text || !swarm || connectionEntries.length === 0) return;
|
||||
const data = new TextEncoder().encode(text);
|
||||
connectionEntries.forEach(function (e) {
|
||||
try { e.conn.write(data); } catch (_) {}
|
||||
});
|
||||
log('Sent raw: ' + text, 'raw');
|
||||
rawMsgEl.value = '';
|
||||
}
|
||||
|
||||
function sendProtomux() {
|
||||
const text = protoMsgEl.value.trim();
|
||||
if (!text || connectionEntries.length === 0) return;
|
||||
let sent = 0;
|
||||
connectionEntries.forEach(function (e) {
|
||||
if (e.stringMsg) {
|
||||
try {
|
||||
e.stringMsg.send(text);
|
||||
sent++;
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
if (sent > 0) {
|
||||
log('Sent via Protomux: ' + text, 'protomux');
|
||||
protoMsgEl.value = '';
|
||||
} else {
|
||||
log('No Protomux channel ready', 'err');
|
||||
}
|
||||
}
|
||||
|
||||
btnRawSend.addEventListener('click', sendRaw);
|
||||
rawMsgEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') sendRaw(); });
|
||||
btnProtoSend.addEventListener('click', sendProtomux);
|
||||
protoMsgEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') sendProtomux(); });
|
||||
})();
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – SDK Demo</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – SDK Demo</h1>
|
||||
<p>This page demonstrates all SDK features: swarm, connections, raw messages, and Protomux. Open in two tabs (or two devices) and join the same topic.</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Swarm <span class="badge">join / leave / destroy</span></h2>
|
||||
<p id="status" class="status">Waiting for extension…</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (32 bytes or string)" value="bridge-swarm-sdk-demo">
|
||||
<button class="primary" id="btnJoin">Join topic</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave & destroy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Connections <span class="badge">peerInfo, connection events</span></h2>
|
||||
<p id="peers">Peers: 0</p>
|
||||
<p class="status ok" id="connStatus" style="display:none;">On connection you get (conn, peerInfo). peerInfo: publicKey (hex), topics (hex[]).</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>3. Raw messages <span class="badge">conn.write / on('data')</span></h2>
|
||||
<div class="row">
|
||||
<input type="text" id="rawMsg" placeholder="Send raw bytes (text)…" disabled>
|
||||
<button class="secondary" id="btnRawSend" disabled>Send raw</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>4. Protomux <span class="badge">createProtomux, channel, c.string / c.binary</span></h2>
|
||||
<div class="row">
|
||||
<input type="text" id="protoMsg" placeholder="Send via Protomux (string)…" disabled>
|
||||
<button class="secondary" id="btnProtoSend" disabled>Send Protomux</button>
|
||||
</div>
|
||||
<p class="status ok" id="protoStatus" style="display:none;">Channel protocol: sdk-demo/v1. Binary messages supported too.</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Log</h2>
|
||||
<div class="log" id="log"></div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
|
||||
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
|
||||
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.status.ok { color: #9ece6a; }
|
||||
.status.error { color: #f7768e; }
|
||||
.status.warn { color: #e0af68; }
|
||||
.log {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.log .peer { color: #bb9af7; }
|
||||
.log .raw { color: #9ece6a; }
|
||||
.log .protomux { color: #7dcfff; }
|
||||
.log .sys { color: #7aa2f7; }
|
||||
.log .err { color: #f7768e; }
|
||||
.section { margin-bottom: 1rem; }
|
||||
.badge { font-size: 0.7rem; padding: 0.15rem 0.4rem; border-radius: 4px; background: #3b4261; color: #a9b1d6; }
|
||||
@@ -1,311 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Whiteboard</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 840px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; color: #7aa2f7; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; color: #9ece6a; margin-bottom: 0.5rem; }
|
||||
.status.error { color: #f7768e; }
|
||||
.peers { font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.5rem; }
|
||||
.canvas-wrap {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 500px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Whiteboard</h1>
|
||||
<p class="status" id="status">Load the extension, then join a topic. Open this page in another tab to draw together.</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (e.g. my-whiteboard)" value="bridge-swarm-whiteboard">
|
||||
<button class="primary" id="btnJoin">Join</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
<button class="secondary" id="btnClear" disabled>Clear board</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="canvas" width="800" height="500"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const btnClear = document.getElementById('btnClear');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const DEFAULT_COLOR = '#c0caf5';
|
||||
const DEFAULT_WIDTH = 3;
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'status' + (isError ? ' error' : '');
|
||||
}
|
||||
|
||||
(function initWhenReady(attempts) {
|
||||
if (attempts >= 50) {
|
||||
setStatus('BridgeSwarm extension not detected. Install the extension and reload.', true);
|
||||
return;
|
||||
}
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Waiting for extension…');
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
setStatus('Load the extension, then join a topic. Open this page in another tab to draw together.');
|
||||
}).catch(function () {
|
||||
setStatus('BridgeSwarm extension not detected.', true);
|
||||
});
|
||||
})(0);
|
||||
|
||||
let swarm = null;
|
||||
const connections = [];
|
||||
const strokes = [];
|
||||
const drafts = {}; // id -> { color, width, points } for peer strokes in progress
|
||||
let drawing = false;
|
||||
let currentPoints = [];
|
||||
let currentStrokeId = null;
|
||||
let currentColor = DEFAULT_COLOR;
|
||||
let currentWidth = DEFAULT_WIDTH;
|
||||
let lastDraftSend = 0;
|
||||
const DRAFT_THROTTLE_MS = 32;
|
||||
|
||||
function updatePeers() {
|
||||
peersEl.textContent = 'Peers: ' + connections.length;
|
||||
}
|
||||
|
||||
function getCanvasCoords(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
function drawStroke(s) {
|
||||
if (!s.points || s.points.length < 2) return;
|
||||
ctx.strokeStyle = s.color || DEFAULT_COLOR;
|
||||
ctx.lineWidth = s.width || DEFAULT_WIDTH;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s.points[0][0], s.points[0][1]);
|
||||
for (let j = 1; j < s.points.length; j++) {
|
||||
ctx.lineTo(s.points[j][0], s.points[j][1]);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for (let i = 0; i < strokes.length; i++) drawStroke(strokes[i]);
|
||||
for (const id in drafts) drawStroke(drafts[id]);
|
||||
if (drawing && currentPoints.length >= 2) {
|
||||
drawStroke({ points: currentPoints, color: currentColor, width: currentWidth });
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(msg) {
|
||||
if (!swarm || connections.length === 0) return;
|
||||
const data = new TextEncoder().encode(JSON.stringify(msg));
|
||||
connections.forEach(({ conn }) => {
|
||||
try { conn.write(data); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(data) {
|
||||
try {
|
||||
const msg = JSON.parse(new TextDecoder().decode(data));
|
||||
if (msg.type === 'strokeStart' && msg.id) {
|
||||
drafts[msg.id] = {
|
||||
color: msg.color || DEFAULT_COLOR,
|
||||
width: msg.width || DEFAULT_WIDTH,
|
||||
points: []
|
||||
};
|
||||
redraw();
|
||||
} else if (msg.type === 'strokeDraft' && msg.id && Array.isArray(msg.points)) {
|
||||
if (drafts[msg.id]) {
|
||||
drafts[msg.id].points = msg.points;
|
||||
redraw();
|
||||
}
|
||||
} else if (msg.type === 'stroke' && Array.isArray(msg.points) && msg.points.length >= 2) {
|
||||
delete drafts[msg.id];
|
||||
strokes.push({
|
||||
id: msg.id,
|
||||
points: msg.points,
|
||||
color: msg.color || DEFAULT_COLOR,
|
||||
width: msg.width || DEFAULT_WIDTH
|
||||
});
|
||||
redraw();
|
||||
} else if (msg.type === 'clear') {
|
||||
strokes.length = 0;
|
||||
for (const id in drafts) delete drafts[id];
|
||||
redraw();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (!swarm) return;
|
||||
e.preventDefault();
|
||||
drawing = true;
|
||||
currentStrokeId = Date.now() + '-' + Math.random();
|
||||
currentPoints = [getCanvasCoords(e)];
|
||||
broadcast({ type: 'strokeStart', id: currentStrokeId, color: currentColor, width: currentWidth });
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!drawing) return;
|
||||
e.preventDefault();
|
||||
currentPoints.push(getCanvasCoords(e));
|
||||
redraw();
|
||||
const now = Date.now();
|
||||
if (now - lastDraftSend >= DRAFT_THROTTLE_MS) {
|
||||
lastDraftSend = now;
|
||||
broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints });
|
||||
}
|
||||
});
|
||||
|
||||
function finishStroke() {
|
||||
if (!drawing || !currentStrokeId) return;
|
||||
drawing = false;
|
||||
if (currentPoints.length >= 2) {
|
||||
broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints });
|
||||
const stroke = {
|
||||
type: 'stroke',
|
||||
id: currentStrokeId,
|
||||
points: currentPoints,
|
||||
color: currentColor,
|
||||
width: currentWidth
|
||||
};
|
||||
strokes.push(stroke);
|
||||
broadcast(stroke);
|
||||
}
|
||||
currentStrokeId = null;
|
||||
currentPoints = [];
|
||||
redraw();
|
||||
}
|
||||
|
||||
canvas.addEventListener('mouseup', (e) => {
|
||||
if (!drawing) return;
|
||||
e.preventDefault();
|
||||
finishStroke();
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
if (drawing) finishStroke();
|
||||
});
|
||||
|
||||
btnClear.addEventListener('click', () => {
|
||||
if (!swarm) return;
|
||||
strokes.length = 0;
|
||||
redraw();
|
||||
broadcast({ type: 'clear' });
|
||||
});
|
||||
|
||||
btnJoin.addEventListener('click', async () => {
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Extension not ready yet. Wait a moment and try again.', true);
|
||||
return;
|
||||
}
|
||||
const topic = topicEl.value.trim() || 'whiteboard-topic';
|
||||
if (swarm) return;
|
||||
try {
|
||||
setStatus('Joining topic "' + topic + '"…');
|
||||
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-whiteboard' });
|
||||
swarm.on('connection', (conn, peerInfo) => {
|
||||
connections.push({ conn, peerInfo });
|
||||
updatePeers();
|
||||
setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.');
|
||||
conn.on('data', (data) => handleMessage(data));
|
||||
conn.on('end', () => {
|
||||
const i = connections.findIndex(c => c.conn === conn);
|
||||
if (i !== -1) connections.splice(i, 1);
|
||||
updatePeers();
|
||||
});
|
||||
conn.on('error', () => {});
|
||||
});
|
||||
await swarm.join(topic);
|
||||
setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
btnClear.disabled = false;
|
||||
} catch (err) {
|
||||
setStatus('Error: ' + err.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
btnLeave.addEventListener('click', async () => {
|
||||
if (!swarm) return;
|
||||
try {
|
||||
const topic = topicEl.value.trim() || 'whiteboard-topic';
|
||||
await swarm.leave(topic);
|
||||
await swarm.destroy();
|
||||
connections.length = 0;
|
||||
updatePeers();
|
||||
swarm = null;
|
||||
setStatus('Left topic. You can join again.');
|
||||
btnJoin.disabled = false;
|
||||
btnLeave.disabled = true;
|
||||
btnClear.disabled = true;
|
||||
} catch (err) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,229 @@
|
||||
(function () {
|
||||
const statusEl = document.getElementById('status');
|
||||
const topicEl = document.getElementById('topic');
|
||||
const btnJoin = document.getElementById('btnJoin');
|
||||
const btnLeave = document.getElementById('btnLeave');
|
||||
const btnClear = document.getElementById('btnClear');
|
||||
const peersEl = document.getElementById('peers');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const DEFAULT_COLOR = '#c0caf5';
|
||||
const DEFAULT_WIDTH = 3;
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = 'status' + (isError ? ' error' : '');
|
||||
}
|
||||
|
||||
(function initWhenReady(attempts) {
|
||||
if (attempts >= 50) {
|
||||
setStatus('BridgeSwarm extension not detected. Install the extension and reload.', true);
|
||||
return;
|
||||
}
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Waiting for extension…');
|
||||
setTimeout(function () { initWhenReady(attempts + 1); }, 100);
|
||||
return;
|
||||
}
|
||||
window.BridgeSwarm.ready().then(function () {
|
||||
setStatus('Load the extension, then join a topic. Open this page in another tab to draw together.');
|
||||
}).catch(function () {
|
||||
setStatus('BridgeSwarm extension not detected.', true);
|
||||
});
|
||||
})(0);
|
||||
|
||||
let swarm = null;
|
||||
const connections = [];
|
||||
const strokes = [];
|
||||
const drafts = {}; // id -> { color, width, points } for peer strokes in progress
|
||||
let drawing = false;
|
||||
let currentPoints = [];
|
||||
let currentStrokeId = null;
|
||||
let currentColor = DEFAULT_COLOR;
|
||||
let currentWidth = DEFAULT_WIDTH;
|
||||
let lastDraftSend = 0;
|
||||
const DRAFT_THROTTLE_MS = 32;
|
||||
|
||||
function updatePeers() {
|
||||
peersEl.textContent = 'Peers: ' + connections.length;
|
||||
}
|
||||
|
||||
function getCanvasCoords(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
function drawStroke(s) {
|
||||
if (!s.points || s.points.length < 2) return;
|
||||
ctx.strokeStyle = s.color || DEFAULT_COLOR;
|
||||
ctx.lineWidth = s.width || DEFAULT_WIDTH;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s.points[0][0], s.points[0][1]);
|
||||
for (let j = 1; j < s.points.length; j++) {
|
||||
ctx.lineTo(s.points[j][0], s.points[j][1]);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for (let i = 0; i < strokes.length; i++) drawStroke(strokes[i]);
|
||||
for (const id in drafts) drawStroke(drafts[id]);
|
||||
if (drawing && currentPoints.length >= 2) {
|
||||
drawStroke({ points: currentPoints, color: currentColor, width: currentWidth });
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(msg) {
|
||||
if (!swarm || connections.length === 0) return;
|
||||
const data = new TextEncoder().encode(JSON.stringify(msg));
|
||||
connections.forEach(({ conn }) => {
|
||||
try { conn.write(data); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(data) {
|
||||
try {
|
||||
const msg = JSON.parse(new TextDecoder().decode(data));
|
||||
if (msg.type === 'strokeStart' && msg.id) {
|
||||
drafts[msg.id] = {
|
||||
color: msg.color || DEFAULT_COLOR,
|
||||
width: msg.width || DEFAULT_WIDTH,
|
||||
points: []
|
||||
};
|
||||
redraw();
|
||||
} else if (msg.type === 'strokeDraft' && msg.id && Array.isArray(msg.points)) {
|
||||
if (drafts[msg.id]) {
|
||||
drafts[msg.id].points = msg.points;
|
||||
redraw();
|
||||
}
|
||||
} else if (msg.type === 'stroke' && Array.isArray(msg.points) && msg.points.length >= 2) {
|
||||
delete drafts[msg.id];
|
||||
strokes.push({
|
||||
id: msg.id,
|
||||
points: msg.points,
|
||||
color: msg.color || DEFAULT_COLOR,
|
||||
width: msg.width || DEFAULT_WIDTH
|
||||
});
|
||||
redraw();
|
||||
} else if (msg.type === 'clear') {
|
||||
strokes.length = 0;
|
||||
for (const id in drafts) delete drafts[id];
|
||||
redraw();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (!swarm) return;
|
||||
e.preventDefault();
|
||||
drawing = true;
|
||||
currentStrokeId = Date.now() + '-' + Math.random();
|
||||
currentPoints = [getCanvasCoords(e)];
|
||||
broadcast({ type: 'strokeStart', id: currentStrokeId, color: currentColor, width: currentWidth });
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!drawing) return;
|
||||
e.preventDefault();
|
||||
currentPoints.push(getCanvasCoords(e));
|
||||
redraw();
|
||||
const now = Date.now();
|
||||
if (now - lastDraftSend >= DRAFT_THROTTLE_MS) {
|
||||
lastDraftSend = now;
|
||||
broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints });
|
||||
}
|
||||
});
|
||||
|
||||
function finishStroke() {
|
||||
if (!drawing || !currentStrokeId) return;
|
||||
drawing = false;
|
||||
if (currentPoints.length >= 2) {
|
||||
broadcast({ type: 'strokeDraft', id: currentStrokeId, points: currentPoints });
|
||||
const stroke = {
|
||||
type: 'stroke',
|
||||
id: currentStrokeId,
|
||||
points: currentPoints,
|
||||
color: currentColor,
|
||||
width: currentWidth
|
||||
};
|
||||
strokes.push(stroke);
|
||||
broadcast(stroke);
|
||||
}
|
||||
currentStrokeId = null;
|
||||
currentPoints = [];
|
||||
redraw();
|
||||
}
|
||||
|
||||
canvas.addEventListener('mouseup', (e) => {
|
||||
if (!drawing) return;
|
||||
e.preventDefault();
|
||||
finishStroke();
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseleave', () => {
|
||||
if (drawing) finishStroke();
|
||||
});
|
||||
|
||||
btnClear.addEventListener('click', () => {
|
||||
if (!swarm) return;
|
||||
strokes.length = 0;
|
||||
redraw();
|
||||
broadcast({ type: 'clear' });
|
||||
});
|
||||
|
||||
btnJoin.addEventListener('click', async () => {
|
||||
if (typeof window.BridgeSwarm === 'undefined') {
|
||||
setStatus('Extension not ready yet. Wait a moment and try again.', true);
|
||||
return;
|
||||
}
|
||||
const topic = topicEl.value.trim() || 'whiteboard-topic';
|
||||
if (swarm) return;
|
||||
try {
|
||||
setStatus('Joining topic "' + topic + '"…');
|
||||
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-whiteboard' });
|
||||
swarm.on('connection', (conn, peerInfo) => {
|
||||
connections.push({ conn, peerInfo });
|
||||
updatePeers();
|
||||
setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.');
|
||||
conn.on('data', (data) => handleMessage(data));
|
||||
conn.on('end', () => {
|
||||
const i = connections.findIndex(c => c.conn === conn);
|
||||
if (i !== -1) connections.splice(i, 1);
|
||||
updatePeers();
|
||||
});
|
||||
conn.on('error', () => {});
|
||||
});
|
||||
await swarm.join(topic);
|
||||
setStatus('Joined "' + topic + '". Draw on the canvas; open in another tab to collaborate.');
|
||||
btnJoin.disabled = true;
|
||||
btnLeave.disabled = false;
|
||||
btnClear.disabled = false;
|
||||
} catch (err) {
|
||||
setStatus('Error: ' + err.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
btnLeave.addEventListener('click', async () => {
|
||||
if (!swarm) return;
|
||||
try {
|
||||
const topic = topicEl.value.trim() || 'whiteboard-topic';
|
||||
await swarm.leave(topic);
|
||||
await swarm.destroy();
|
||||
connections.length = 0;
|
||||
updatePeers();
|
||||
swarm = null;
|
||||
setStatus('Left topic. You can join again.');
|
||||
btnJoin.disabled = false;
|
||||
btnLeave.disabled = true;
|
||||
btnClear.disabled = true;
|
||||
} catch (err) {}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BridgeSwarm – Whiteboard</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>BridgeSwarm – Whiteboard</h1>
|
||||
<p class="status" id="status">Load the extension, then join a topic. Open this page in another tab to draw together.</p>
|
||||
<div class="row">
|
||||
<input type="text" id="topic" placeholder="Topic (e.g. my-whiteboard)" value="bridge-swarm-whiteboard">
|
||||
<button class="primary" id="btnJoin">Join</button>
|
||||
<button class="danger" id="btnLeave" disabled>Leave</button>
|
||||
<button class="secondary" id="btnClear" disabled>Clear board</button>
|
||||
</div>
|
||||
<div class="peers" id="peers">Peers: 0</div>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="canvas" width="800" height="500"></canvas>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 840px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #1a1b26;
|
||||
color: #c0caf5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; color: #7aa2f7; }
|
||||
.row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; align-items: center; flex-wrap: wrap; }
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
background: #24283b;
|
||||
color: #c0caf5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
input::placeholder { color: #565f89; }
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary { background: #7aa2f7; color: #1a1b26; }
|
||||
button.primary:hover { background: #89b4fa; }
|
||||
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.secondary { background: #3b4261; color: #c0caf5; }
|
||||
button.secondary:hover { background: #414868; }
|
||||
button.danger { background: #f7768e; color: #1a1b26; }
|
||||
button.danger:hover { background: #ff9db5; }
|
||||
.status { font-size: 0.85rem; color: #9ece6a; margin-bottom: 0.5rem; }
|
||||
.status.error { color: #f7768e; }
|
||||
.peers { font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.5rem; }
|
||||
.canvas-wrap {
|
||||
background: #24283b;
|
||||
border: 1px solid #3b4261;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
#canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 500px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
Reference in New Issue
Block a user