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:
Raven Scott
2026-02-12 07:35:18 -05:00
parent 1ea4deac6e
commit e801bc0041
24 changed files with 1080 additions and 1217 deletions
+14 -12
View File
@@ -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 directorys **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 hosts **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 extensions **Options** and ensure “Do not inject on file:// URLs” is *off*, then in `chrome://extensions` (or the browsers 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 extensions **Options** and ensure “Do not inject on file:// URLs” is *off*, then in `chrome://extensions` (or the browsers 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/`.
-206
View File
@@ -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>
+120
View File
@@ -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(); });
})();
+26
View File
@@ -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>
+58
View File
@@ -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>
+74
View File
@@ -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>
+18
View File
@@ -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 &quot;Ping peer&quot; 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 &amp; destroy</button>
</div>
</div>
<div class="section">
<h2>2. Connection &amp; 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>
+48
View File
@@ -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 &quot;Ping peer&quot; 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 &amp; destroy</button>
</div>
</div>
<div class="section">
<h2>2. Connection &amp; 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>
+27
View File
@@ -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 tabs 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>
+40
View File
@@ -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>
+25
View File
@@ -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; }
-312
View File
@@ -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>
+192
View File
@@ -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(); });
})();
+53
View File
@@ -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>
+63
View File
@@ -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; }
-311
View File
@@ -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>
+229
View File
@@ -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) {}
});
})();
+25
View File
@@ -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>
+55
View File
@@ -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;
}