instbyte 1.9.4 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -164
- package/bin/instbyte.js +15 -6
- package/client/css/app.css +366 -0
- package/client/index.html +31 -0
- package/client/js/app.js +498 -3
- package/package.json +2 -1
- package/server/config.js +15 -6
- package/server/server.js +271 -19
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
npx instbyte
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
You're three feet from your teammate. *A quick share* still leaves your building before it reaches them.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Your chat app is for conversation. Your cloud storage is for files. Your email is for async.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
None of them are for *right now, on this network, gone tomorrow.*
|
|
29
|
+
|
|
30
|
+
That's what **Instbyte** is for.
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
@@ -102,147 +104,19 @@ For teams who want auth, custom retention, or branding — create `instbyte.conf
|
|
|
102
104
|
|
|
103
105
|
Then run `npx instbyte` in the same directory. The config is picked up automatically.
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
For persistent team use, run it as a background process:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
# using pm2
|
|
110
|
-
npm install -g pm2
|
|
111
|
-
pm2 start "npx instbyte" --name instbyte
|
|
112
|
-
pm2 save
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Or use any process manager you already have — systemd, screen, tmux.
|
|
107
|
+
For persistent deployment options including pm2, systemd, and Docker, see the [Deployment Guide](docs/deployment.md).
|
|
116
108
|
|
|
117
109
|
---
|
|
118
110
|
|
|
119
111
|
## Docker
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
docker compose up -d
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Or with plain Docker:
|
|
127
|
-
```bash
|
|
128
|
-
docker run -d \
|
|
129
|
-
-p 3000:3000 \
|
|
130
|
-
-v $(pwd)/instbyte-data:/data \
|
|
131
|
-
-e INSTBYTE_DATA=/data \
|
|
132
|
-
-e INSTBYTE_UPLOADS=/data/uploads \
|
|
133
|
-
--name instbyte \
|
|
134
|
-
mohitgauniyal/instbyte
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Data persists in `./instbyte-data` on your host. The same folder used by `npx instbyte` — so switching between the two preserves all your data.
|
|
138
|
-
|
|
139
|
-
### With a config file
|
|
140
|
-
|
|
141
|
-
Mount your config file into the container:
|
|
142
|
-
```yaml
|
|
143
|
-
services:
|
|
144
|
-
instbyte:
|
|
145
|
-
image: mohitgauniyal/instbyte
|
|
146
|
-
ports:
|
|
147
|
-
- "3000:3000"
|
|
148
|
-
volumes:
|
|
149
|
-
- ./instbyte-data:/data
|
|
150
|
-
- ./instbyte.config.json:/app/instbyte.config.json
|
|
151
|
-
environment:
|
|
152
|
-
- INSTBYTE_DATA=/data
|
|
153
|
-
- INSTBYTE_UPLOADS=/data/uploads
|
|
154
|
-
restart: unless-stopped
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Changing the port
|
|
158
|
-
|
|
159
|
-
Edit the host port in `docker-compose.yml`:
|
|
160
|
-
```yaml
|
|
161
|
-
ports:
|
|
162
|
-
- "8080:3000" # now runs on port 8080
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
> **Note:** File uploads may not work correctly on Windows Docker Desktop due to network limitations. For Windows, use `npx instbyte` directly or deploy on a Linux server.
|
|
113
|
+
For Docker setup, persistent data, and config file mounting, see [Deployment Guide](docs/deployment.md).
|
|
166
114
|
|
|
167
115
|
---
|
|
168
116
|
|
|
169
117
|
## Reverse Proxy
|
|
170
118
|
|
|
171
|
-
For
|
|
172
|
-
|
|
173
|
-
> **Important:** Instbyte uses WebSockets for real-time sync. Your proxy must be configured to forward WebSocket connections — otherwise the app will load but live updates will stop working.
|
|
174
|
-
|
|
175
|
-
### Nginx
|
|
176
|
-
```nginx
|
|
177
|
-
server {
|
|
178
|
-
listen 80;
|
|
179
|
-
server_name instbyte.yourdomain.com;
|
|
180
|
-
|
|
181
|
-
# Redirect HTTP to HTTPS
|
|
182
|
-
return 301 https://$host$request_uri;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
server {
|
|
186
|
-
listen 443 ssl;
|
|
187
|
-
server_name instbyte.yourdomain.com;
|
|
188
|
-
|
|
189
|
-
ssl_certificate /etc/letsencrypt/live/instbyte.yourdomain.com/fullchain.pem;
|
|
190
|
-
ssl_certificate_key /etc/letsencrypt/live/instbyte.yourdomain.com/privkey.pem;
|
|
191
|
-
|
|
192
|
-
# Increase max upload size to match Instbyte's limit
|
|
193
|
-
client_max_body_size 2G;
|
|
194
|
-
|
|
195
|
-
location / {
|
|
196
|
-
proxy_pass http://localhost:3000;
|
|
197
|
-
proxy_http_version 1.1;
|
|
198
|
-
|
|
199
|
-
# Required for WebSocket support
|
|
200
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
201
|
-
proxy_set_header Connection "upgrade";
|
|
202
|
-
|
|
203
|
-
proxy_set_header Host $host;
|
|
204
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
205
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
206
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
207
|
-
|
|
208
|
-
# Prevent proxy timeouts on large uploads
|
|
209
|
-
proxy_read_timeout 300s;
|
|
210
|
-
proxy_send_timeout 300s;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Get a free SSL certificate with Certbot:
|
|
216
|
-
```bash
|
|
217
|
-
certbot --nginx -d instbyte.yourdomain.com
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Caddy
|
|
221
|
-
|
|
222
|
-
Caddy automatically handles HTTPS certificates — no Certbot needed.
|
|
223
|
-
```caddy
|
|
224
|
-
instbyte.yourdomain.com {
|
|
225
|
-
reverse_proxy localhost:3000
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Caddy handles WebSocket forwarding and HTTPS automatically. That's all you need.
|
|
230
|
-
|
|
231
|
-
### With Docker
|
|
232
|
-
|
|
233
|
-
If running Instbyte via Docker, proxy to the mapped host port:
|
|
234
|
-
```nginx
|
|
235
|
-
proxy_pass http://localhost:3000;
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
Or use Docker's internal network — replace `localhost` with the container name:
|
|
239
|
-
```nginx
|
|
240
|
-
proxy_pass http://instbyte:3000;
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Keeping it LAN-only
|
|
244
|
-
|
|
245
|
-
If you don't want external access but still want HTTPS on your local network, tools like [Tailscale](https://tailscale.com) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) are good options that require no open ports.
|
|
119
|
+
For Nginx, Caddy, and HTTPS setup, see [Deployment Guide](docs/deployment.md).
|
|
246
120
|
|
|
247
121
|
---
|
|
248
122
|
|
|
@@ -305,6 +179,8 @@ The difference between *a tool you use* and *a tool you own.*
|
|
|
305
179
|
|
|
306
180
|
**Presence awareness** — see how many people are currently connected in real time.
|
|
307
181
|
|
|
182
|
+
**Live broadcast** — share your screen in real time with everyone on the network. Viewers join instantly from their browser, no plugins or installs needed. Built on WebRTC for smooth, low-latency video. Includes mic audio, viewer mute/unmute, raise hand, and screen capture to channel.
|
|
183
|
+
|
|
308
184
|
**Read receipts** — see how many devices have viewed each shared item. Updates live as teammates open the page.
|
|
309
185
|
|
|
310
186
|
**Item management** — add optional titles to label any item for future reference. Edit text items inline without deleting and re-pasting. Pinned items are protected from both manual deletion and auto-cleanup.
|
|
@@ -315,8 +191,19 @@ The difference between *a tool you use* and *a tool you own.*
|
|
|
315
191
|
|
|
316
192
|
---
|
|
317
193
|
|
|
194
|
+
## Broadcasting
|
|
195
|
+
|
|
196
|
+
One person shares their screen — everyone else on the network watches live in their browser. No plugins, no accounts, no external services. Built on WebRTC.
|
|
197
|
+
|
|
198
|
+
Viewers can save the current frame to a channel, raise a hand to notify the broadcaster, and toggle audio from the panel.
|
|
199
|
+
|
|
200
|
+
For HTTPS setup and advanced network configuration, see the [Deployment Guide](docs/deployment.md#broadcasting).
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
318
204
|
## Keyboard Shortcuts
|
|
319
205
|
|
|
206
|
+
```
|
|
320
207
|
| Key | Action |
|
|
321
208
|
|---|---|
|
|
322
209
|
| `/` | Focus search |
|
|
@@ -346,40 +233,13 @@ node server/server.js
|
|
|
346
233
|
- Home lab file sharing without setting up NAS or cloud sync
|
|
347
234
|
- Piping build logs or stack traces from CI or terminal directly into a shared channel
|
|
348
235
|
- Sharing sensitive credentials or config files over LAN without leaving a cloud trail
|
|
236
|
+
- Live screen sharing during standups, design reviews, or debugging sessions — no Zoom link needed
|
|
349
237
|
|
|
350
238
|
---
|
|
351
239
|
|
|
352
240
|
## Terminal Usage
|
|
353
241
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
**Send a log file:**
|
|
357
|
-
```bash
|
|
358
|
-
curl -X POST http://192.168.x.x:3000/text \
|
|
359
|
-
-H "Content-Type: application/json" \
|
|
360
|
-
-d "{\"content\": \"$(cat error.log)\", \"channel\": \"general\", \"uploader\": \"terminal\"}"
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**Pipe command output directly:**
|
|
364
|
-
```bash
|
|
365
|
-
npm run build 2>&1 | curl -X POST http://192.168.x.x:3000/text \
|
|
366
|
-
-H "Content-Type: application/json" \
|
|
367
|
-
--data-binary @- \
|
|
368
|
-
-H "X-Channel: general" \
|
|
369
|
-
-H "X-Uploader: CI"
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
**Upload a file from the terminal:**
|
|
373
|
-
```bash
|
|
374
|
-
curl -X POST http://192.168.x.x:3000/upload \
|
|
375
|
-
-F "file=@./build.log" \
|
|
376
|
-
-F "channel=general" \
|
|
377
|
-
-F "uploader=terminal"
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Replace `192.168.x.x:3000` with the URL shown when Instbyte starts. If auth is enabled, add `-b "instbyte_auth=your-token"` to each request.
|
|
381
|
-
|
|
382
|
-
Useful for piping stack traces, build logs, or environment dumps straight into a channel your whole team can see instantly.
|
|
242
|
+
Push content from your terminal using curl — no browser needed. See [Terminal Usage Guide](docs/terminal-usage.md).
|
|
383
243
|
|
|
384
244
|
---
|
|
385
245
|
|
|
@@ -393,7 +253,7 @@ Instbyte follows [Semantic Versioning](https://semver.org). See [Releases](https
|
|
|
393
253
|
|
|
394
254
|
Instbyte is intentionally lightweight and LAN-first. If you want to extend it — CLI tools, themes, integrations — open an issue or submit a pull request.
|
|
395
255
|
|
|
396
|
-
The codebase has a full test suite (
|
|
256
|
+
The codebase has a full test suite (195 tests across unit and integration). Run `npm test` before submitting anything. Issues tagged **good first issue** are a good starting point.
|
|
397
257
|
|
|
398
258
|
---
|
|
399
259
|
|
package/bin/instbyte.js
CHANGED
|
@@ -11,18 +11,27 @@ const fs = require("fs");
|
|
|
11
11
|
// When run via npx or global install, we want data to live
|
|
12
12
|
// in the user's current working directory, not inside the
|
|
13
13
|
// npm cache or global node_modules.
|
|
14
|
+
//
|
|
15
|
+
// When run via Docker, INSTBYTE_DATA and INSTBYTE_UPLOADS may
|
|
16
|
+
// already be set via environment variables — respect those and
|
|
17
|
+
// don't override them.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
if (!process.env.INSTBYTE_DATA) {
|
|
20
|
+
process.env.INSTBYTE_DATA = path.join(process.cwd(), "instbyte-data");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!process.env.INSTBYTE_UPLOADS) {
|
|
24
|
+
process.env.INSTBYTE_UPLOADS = path.join(process.env.INSTBYTE_DATA, "uploads");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dataDir = process.env.INSTBYTE_DATA;
|
|
28
|
+
const uploadsDir = process.env.INSTBYTE_UPLOADS;
|
|
17
29
|
|
|
18
30
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
19
31
|
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
|
|
20
32
|
|
|
21
|
-
// Pass locations to the rest of the app via env vars
|
|
22
|
-
process.env.INSTBYTE_DATA = dataDir;
|
|
23
|
-
process.env.INSTBYTE_UPLOADS = uploadsDir;
|
|
24
|
-
|
|
25
33
|
// ========================
|
|
26
34
|
// BOOT
|
|
27
35
|
// ========================
|
|
36
|
+
process.env.INSTBYTE_BOOT = '1';
|
|
28
37
|
require("../server/server.js");
|
package/client/css/app.css
CHANGED
|
@@ -1079,6 +1079,264 @@ input[type=text]:focus {
|
|
|
1079
1079
|
transition: background 1.5s ease, border-color 1.5s ease;
|
|
1080
1080
|
}
|
|
1081
1081
|
|
|
1082
|
+
/* ============================================================
|
|
1083
|
+
BROADCAST
|
|
1084
|
+
============================================================ */
|
|
1085
|
+
|
|
1086
|
+
/* Bar — sits below header, visible to all when live */
|
|
1087
|
+
.broadcast-bar {
|
|
1088
|
+
background: #111827;
|
|
1089
|
+
color: #fff;
|
|
1090
|
+
padding: 8px 20px;
|
|
1091
|
+
position: sticky;
|
|
1092
|
+
top: 0;
|
|
1093
|
+
z-index: 100;
|
|
1094
|
+
border-bottom: 1px solid #1f2937;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.broadcast-bar-inner {
|
|
1098
|
+
max-width: 900px;
|
|
1099
|
+
margin: 0 auto;
|
|
1100
|
+
display: flex;
|
|
1101
|
+
align-items: center;
|
|
1102
|
+
gap: 10px;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.broadcast-pulse {
|
|
1106
|
+
display: inline-block;
|
|
1107
|
+
width: 8px;
|
|
1108
|
+
height: 8px;
|
|
1109
|
+
background: #ef4444;
|
|
1110
|
+
border-radius: 50%;
|
|
1111
|
+
flex-shrink: 0;
|
|
1112
|
+
animation: broadcastPulse 1.5s ease-in-out infinite;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
@keyframes broadcastPulse {
|
|
1116
|
+
|
|
1117
|
+
0%,
|
|
1118
|
+
100% {
|
|
1119
|
+
opacity: 1;
|
|
1120
|
+
transform: scale(1);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
50% {
|
|
1124
|
+
opacity: 0.4;
|
|
1125
|
+
transform: scale(0.85);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
#broadcastLabel {
|
|
1130
|
+
font-size: 13px;
|
|
1131
|
+
font-weight: 500;
|
|
1132
|
+
flex: 1;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.broadcast-viewers {
|
|
1136
|
+
font-size: 12px;
|
|
1137
|
+
color: #9ca3af;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.broadcast-bar-actions {
|
|
1141
|
+
display: flex;
|
|
1142
|
+
gap: 8px;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.broadcast-join-btn {
|
|
1146
|
+
background: #2563eb;
|
|
1147
|
+
color: #fff;
|
|
1148
|
+
border: none;
|
|
1149
|
+
border-radius: 6px;
|
|
1150
|
+
padding: 5px 14px;
|
|
1151
|
+
font-size: 13px;
|
|
1152
|
+
cursor: pointer;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.broadcast-join-btn:hover {
|
|
1156
|
+
background: #1d4ed8;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.broadcast-end-btn {
|
|
1160
|
+
background: #ef4444;
|
|
1161
|
+
color: #fff;
|
|
1162
|
+
border: none;
|
|
1163
|
+
border-radius: 6px;
|
|
1164
|
+
padding: 5px 14px;
|
|
1165
|
+
font-size: 13px;
|
|
1166
|
+
cursor: pointer;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.broadcast-end-btn:hover {
|
|
1170
|
+
background: #dc2626;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/* Start button in composer */
|
|
1174
|
+
.broadcast-start-btn {
|
|
1175
|
+
background: #374151;
|
|
1176
|
+
color: #fff;
|
|
1177
|
+
border: none;
|
|
1178
|
+
border-radius: 6px;
|
|
1179
|
+
padding: 10px 14px;
|
|
1180
|
+
font-size: 13px;
|
|
1181
|
+
cursor: pointer;
|
|
1182
|
+
white-space: nowrap;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.broadcast-start-btn:hover {
|
|
1186
|
+
background: #4b5563;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.broadcast-start-btn.is-live {
|
|
1190
|
+
background: #ef4444;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.broadcast-start-btn.is-live:hover {
|
|
1194
|
+
background: #dc2626;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* ─── VIEWER PANEL ─────────────────────────────────────────── */
|
|
1198
|
+
|
|
1199
|
+
.viewer-panel {
|
|
1200
|
+
position: fixed;
|
|
1201
|
+
top: 60px;
|
|
1202
|
+
right: 20px;
|
|
1203
|
+
width: 480px;
|
|
1204
|
+
max-width: calc(100vw - 40px);
|
|
1205
|
+
background: #fff;
|
|
1206
|
+
border: 1px solid #e5e7eb;
|
|
1207
|
+
border-radius: 12px;
|
|
1208
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
1209
|
+
z-index: 500;
|
|
1210
|
+
display: flex;
|
|
1211
|
+
flex-direction: column;
|
|
1212
|
+
overflow: auto;
|
|
1213
|
+
resize: both;
|
|
1214
|
+
min-width: 280px;
|
|
1215
|
+
min-height: 200px;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.viewer-panel-header {
|
|
1219
|
+
display: flex;
|
|
1220
|
+
align-items: center;
|
|
1221
|
+
justify-content: space-between;
|
|
1222
|
+
padding: 10px 14px;
|
|
1223
|
+
background: #f9fafb;
|
|
1224
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1225
|
+
flex-shrink: 0;
|
|
1226
|
+
cursor: grab;
|
|
1227
|
+
user-select: none;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.viewer-panel-header:active {
|
|
1231
|
+
cursor: grabbing;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
.viewer-minimize-btn {
|
|
1235
|
+
background: #f3f4f6;
|
|
1236
|
+
border: 1px solid #e5e7eb;
|
|
1237
|
+
border-radius: 6px;
|
|
1238
|
+
padding: 4px 10px;
|
|
1239
|
+
font-size: 12px;
|
|
1240
|
+
cursor: pointer;
|
|
1241
|
+
color: #374151;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.viewer-minimize-btn:hover {
|
|
1245
|
+
background: #fef9c3;
|
|
1246
|
+
border-color: #fde68a;
|
|
1247
|
+
color: #92400e;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.viewer-panel.minimized .viewer-frame,
|
|
1251
|
+
.viewer-panel.minimized .viewer-panel-actions .viewer-capture-btn,
|
|
1252
|
+
.viewer-panel.minimized .viewer-panel-actions .viewer-raise-btn,
|
|
1253
|
+
.viewer-panel.minimized .viewer-panel-actions .viewer-close-btn {
|
|
1254
|
+
display: none;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.viewer-panel.minimized {
|
|
1258
|
+
min-height: unset;
|
|
1259
|
+
resize: none;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
#viewerLabel {
|
|
1263
|
+
font-size: 13px;
|
|
1264
|
+
font-weight: 600;
|
|
1265
|
+
color: #111827;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.viewer-panel-actions {
|
|
1269
|
+
display: flex;
|
|
1270
|
+
gap: 6px;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.viewer-capture-btn,
|
|
1274
|
+
.viewer-raise-btn,
|
|
1275
|
+
.viewer-close-btn {
|
|
1276
|
+
background: #f3f4f6;
|
|
1277
|
+
border: 1px solid #e5e7eb;
|
|
1278
|
+
border-radius: 6px;
|
|
1279
|
+
padding: 4px 10px;
|
|
1280
|
+
font-size: 12px;
|
|
1281
|
+
cursor: pointer;
|
|
1282
|
+
color: #374151;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.viewer-capture-btn:hover {
|
|
1286
|
+
background: #dbeafe;
|
|
1287
|
+
border-color: #bfdbfe;
|
|
1288
|
+
color: #1d4ed8;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.viewer-raise-btn:hover {
|
|
1292
|
+
background: #fef9c3;
|
|
1293
|
+
border-color: #fde68a;
|
|
1294
|
+
color: #92400e;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.viewer-close-btn:hover {
|
|
1298
|
+
background: #fee2e2;
|
|
1299
|
+
border-color: #fecaca;
|
|
1300
|
+
color: #b91c1c;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.viewer-frame {
|
|
1304
|
+
width: 100%;
|
|
1305
|
+
display: block;
|
|
1306
|
+
background: #0f1117;
|
|
1307
|
+
min-height: 180px;
|
|
1308
|
+
object-fit: contain;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/* ─── RAISE HAND TOAST ─────────────────────────────────────── */
|
|
1312
|
+
|
|
1313
|
+
.raise-hand-toast {
|
|
1314
|
+
position: fixed;
|
|
1315
|
+
bottom: 80px;
|
|
1316
|
+
left: 50%;
|
|
1317
|
+
transform: translateX(-50%);
|
|
1318
|
+
background: #111827;
|
|
1319
|
+
color: #fff;
|
|
1320
|
+
padding: 8px 16px;
|
|
1321
|
+
border-radius: 20px;
|
|
1322
|
+
font-size: 13px;
|
|
1323
|
+
z-index: 9999;
|
|
1324
|
+
pointer-events: none;
|
|
1325
|
+
animation: raiseHandIn 0.2s ease;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
@keyframes raiseHandIn {
|
|
1329
|
+
from {
|
|
1330
|
+
opacity: 0;
|
|
1331
|
+
transform: translateX(-50%) translateY(8px);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
to {
|
|
1335
|
+
opacity: 1;
|
|
1336
|
+
transform: translateX(-50%) translateY(0);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1082
1340
|
/* ============================================================
|
|
1083
1341
|
MOBILE
|
|
1084
1342
|
============================================================ */
|
|
@@ -1322,6 +1580,36 @@ input[type=text]:focus {
|
|
|
1322
1580
|
transform: translateY(0);
|
|
1323
1581
|
}
|
|
1324
1582
|
}
|
|
1583
|
+
|
|
1584
|
+
.broadcast-bar {
|
|
1585
|
+
padding: 8px 12px;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.broadcast-bar-inner {
|
|
1589
|
+
gap: 8px;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
#broadcastLabel {
|
|
1593
|
+
font-size: 12px;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
.broadcast-start-btn {
|
|
1597
|
+
display: none;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
.viewer-panel {
|
|
1601
|
+
position: fixed;
|
|
1602
|
+
top: auto;
|
|
1603
|
+
bottom: 0;
|
|
1604
|
+
left: 0;
|
|
1605
|
+
right: 0;
|
|
1606
|
+
width: 100%;
|
|
1607
|
+
max-width: 100%;
|
|
1608
|
+
border-radius: 16px 16px 0 0;
|
|
1609
|
+
resize: none;
|
|
1610
|
+
max-height: 85vh;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1325
1613
|
}
|
|
1326
1614
|
|
|
1327
1615
|
|
|
@@ -1728,6 +2016,45 @@ input[type=text]:focus {
|
|
|
1728
2016
|
background: #1a1d27 !important;
|
|
1729
2017
|
color: #e5e7eb !important;
|
|
1730
2018
|
}
|
|
2019
|
+
|
|
2020
|
+
.broadcast-bar {
|
|
2021
|
+
background: #0f1117;
|
|
2022
|
+
border-bottom-color: #1f2937;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
.broadcast-start-btn {
|
|
2026
|
+
background: #1f2937;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
.broadcast-start-btn:hover {
|
|
2030
|
+
background: #374151;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
.viewer-panel {
|
|
2034
|
+
background: #1e2433;
|
|
2035
|
+
border-color: #2d3748;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
.viewer-panel-header {
|
|
2039
|
+
background: #161b27;
|
|
2040
|
+
border-bottom-color: #2d3748;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
#viewerLabel {
|
|
2044
|
+
color: #f9fafb;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
.viewer-capture-btn,
|
|
2048
|
+
.viewer-raise-btn,
|
|
2049
|
+
.viewer-close-btn {
|
|
2050
|
+
background: #2d3748;
|
|
2051
|
+
border-color: #374151;
|
|
2052
|
+
color: #d1d5db;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.raise-hand-toast {
|
|
2056
|
+
background: #1f2937;
|
|
2057
|
+
}
|
|
1731
2058
|
}
|
|
1732
2059
|
}
|
|
1733
2060
|
|
|
@@ -2126,4 +2453,43 @@ input[type=text]:focus {
|
|
|
2126
2453
|
background: #1a1d27 !important;
|
|
2127
2454
|
color: #e5e7eb !important;
|
|
2128
2455
|
}
|
|
2456
|
+
|
|
2457
|
+
.broadcast-bar {
|
|
2458
|
+
background: #0f1117;
|
|
2459
|
+
border-bottom-color: #1f2937;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.broadcast-start-btn {
|
|
2463
|
+
background: #1f2937;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.broadcast-start-btn:hover {
|
|
2467
|
+
background: #374151;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
.viewer-panel {
|
|
2471
|
+
background: #1e2433;
|
|
2472
|
+
border-color: #2d3748;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
.viewer-panel-header {
|
|
2476
|
+
background: #161b27;
|
|
2477
|
+
border-bottom-color: #2d3748;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
#viewerLabel {
|
|
2481
|
+
color: #f9fafb;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
.viewer-capture-btn,
|
|
2485
|
+
.viewer-raise-btn,
|
|
2486
|
+
.viewer-close-btn {
|
|
2487
|
+
background: #2d3748;
|
|
2488
|
+
border-color: #374151;
|
|
2489
|
+
color: #d1d5db;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
.raise-hand-toast {
|
|
2493
|
+
background: #1f2937;
|
|
2494
|
+
}
|
|
2129
2495
|
}
|
package/client/index.html
CHANGED
|
@@ -37,6 +37,34 @@
|
|
|
37
37
|
</div>
|
|
38
38
|
</header>
|
|
39
39
|
|
|
40
|
+
<div id="broadcastBar" class="broadcast-bar" style="display:none">
|
|
41
|
+
<div class="broadcast-bar-inner">
|
|
42
|
+
<span class="broadcast-pulse"></span>
|
|
43
|
+
<span id="broadcastLabel">Someone is broadcasting</span>
|
|
44
|
+
<span id="broadcastViewers" class="broadcast-viewers"></span>
|
|
45
|
+
<div class="broadcast-bar-actions">
|
|
46
|
+
<button id="broadcastJoinBtn" class="broadcast-join-btn" onclick="joinBroadcast()">Join</button>
|
|
47
|
+
<button id="broadcastEndBtn" class="broadcast-end-btn" onclick="stopBroadcast()" style="display:none">End
|
|
48
|
+
broadcast</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div id="viewerPanel" class="viewer-panel" style="display:none">
|
|
54
|
+
<div class="viewer-panel-header">
|
|
55
|
+
<span id="viewerLabel">Broadcast</span>
|
|
56
|
+
<div class="viewer-panel-actions">
|
|
57
|
+
<button onclick="captureToChannel()" class="viewer-capture-btn" title="Save frame to channel">📸
|
|
58
|
+
Capture</button>
|
|
59
|
+
<button onclick="raiseHand()" class="viewer-raise-btn" title="Raise hand">✋</button>
|
|
60
|
+
<button onclick="toggleAudio()" class="viewer-audio-btn" id="audioToggleBtn" title="Unmute audio">🔇</button>
|
|
61
|
+
<button onclick="toggleMinimize()" class="viewer-minimize-btn" title="Minimize">─</button>
|
|
62
|
+
<button onclick="leaveBroadcast()" class="viewer-close-btn" title="Leave broadcast">✕</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<video id="viewerFrame" class="viewer-frame" autoplay playsinline muted></video>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
40
68
|
<div class="container">
|
|
41
69
|
<div class="channel-bar">
|
|
42
70
|
<div class="channels" id="channels"></div>
|
|
@@ -49,6 +77,9 @@
|
|
|
49
77
|
<button onclick="sendText()">Send</button>
|
|
50
78
|
<input type="file" id="fileInput" hidden multiple>
|
|
51
79
|
<button onclick="fileInput.click()">Upload</button>
|
|
80
|
+
<button id="startBroadcastBtn" onclick="startBroadcast()" class="broadcast-start-btn"
|
|
81
|
+
title="Start broadcasting your screen">📡 Broadcast</button>
|
|
82
|
+
|
|
52
83
|
</div>
|
|
53
84
|
<div class="drop" id="drop">Drag files anywhere to upload</div>
|
|
54
85
|
<div id="uploadStatus"
|