lancast 1.0.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 ADDED
@@ -0,0 +1,125 @@
1
+ # 🚀 LanCast
2
+
3
+ <p align="center">
4
+ ⚡ Instant LAN File Sharing from your Terminal
5
+ <br/>
6
+ <b>No setup. No cloud. No accounts.</b>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <img src="https://img.shields.io/npm/v/lancast?color=green" />
11
+ <img src="https://img.shields.io/npm/dt/lancast?color=blue" />
12
+ <img src="https://img.shields.io/npm/l/lancast" />
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## ✨ What is LanCast?
18
+
19
+ **LanCast** is a LAN-based peer-to-peer file sharing tool that runs directly from your terminal.
20
+
21
+ It starts a local server and allows devices on the same network to discover each other and transfer files instantly.
22
+
23
+ 🌐 Official Website:
24
+ 👉 [https://lancast.zoherdev.xyz](https://lancast.zoherdev.xyz)
25
+
26
+ ---
27
+
28
+ # ⚡ Installation
29
+
30
+ Install globally:
31
+
32
+ ```bash
33
+ npm install -g lancast
34
+ ```
35
+
36
+ ---
37
+
38
+ # 🚀 Start LanCast
39
+
40
+ ```bash
41
+ npx lancast start
42
+ ```
43
+
44
+ or
45
+
46
+ ```bash
47
+ lancast start
48
+ ```
49
+
50
+ LanCast will automatically:
51
+
52
+ * Start server on port **3150**
53
+ * Detect your LAN IP
54
+ * Display access URLs
55
+ * Enable device discovery
56
+
57
+ Example output:
58
+
59
+ ```
60
+ 🌐 Local: http://localhost:3150
61
+ 📡 Network: http://192.168.1.25:3150
62
+ ```
63
+
64
+ Open the network URL on any device connected to the same WiFi.
65
+
66
+ ---
67
+
68
+ # 🧠 How It Works
69
+
70
+ ```
71
+ Device A joins LAN
72
+
73
+ Device B joins LAN
74
+
75
+ LanCast shares connected devices
76
+
77
+ Select device
78
+
79
+ Peer connection established
80
+
81
+ File transfers directly device → device
82
+ ```
83
+
84
+ No cloud. No external storage.
85
+ Everything happens inside your local network.
86
+
87
+ ---
88
+
89
+ # 🔥 Features
90
+
91
+ * ⚡ Instant device discovery
92
+ * 🔗 Direct peer-to-peer transfer
93
+ * 📁 Send large files
94
+ * 📡 Works on mobile & desktop
95
+ * 🚀 Zero configuration
96
+ * 🔐 Fully private (LAN only)
97
+
98
+ ---
99
+
100
+ # 🏗 Tech Stack
101
+
102
+ * Node.js
103
+ * Express
104
+ * Socket.io
105
+ * WebRTC
106
+ * EJS
107
+
108
+ ---
109
+
110
+ # 🛣 Roadmap
111
+
112
+ * [ ] Transfer progress indicator
113
+ * [ ] Drag & Drop support
114
+ * [ ] QR code quick connect
115
+ * [ ] PWA support
116
+ * [ ] Desktop version
117
+
118
+ ---
119
+
120
+ # 👨‍💻 Author
121
+
122
+ Created by **Zoher Rangwala**
123
+
124
+ 🌐 [https://zoherdev.xyz](https://zoherdev.xyz)
125
+ 🌐 [https://lancast.zoherdev.xyz](https://lancast.zoherdev.xyz)
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "lancast",
3
+ "version": "1.0.0",
4
+ "description": "LanCast is a LAN-based peer-to-peer file sharing CLI tool. Instantly share files between devices on the same WiFi network with zero configuration.",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "bin": {
8
+ "lancast": "./server.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node server.js",
12
+ "dev": "nodemon server.js"
13
+ },
14
+ "keywords": [
15
+ "lan",
16
+ "file-sharing",
17
+ "peer-to-peer",
18
+ "p2p",
19
+ "webrtc",
20
+ "socket.io",
21
+ "local-network",
22
+ "wifi-file-transfer",
23
+ "cli-tool",
24
+ "xender-alternative",
25
+ "shareit-alternative",
26
+ "nodejs"
27
+ ],
28
+ "author": "Zoher Rangwala",
29
+ "license": "MIT",
30
+ "homepage": "https://lancast.zoherdev.xyz",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/zoherr/lancast.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/zoherr/lancast/issues"
37
+ },
38
+ "dependencies": {
39
+ "boxen": "^8.0.1",
40
+ "chalk": "^5.6.2",
41
+ "ejs": "^3.1.9",
42
+ "express": "^4.18.2",
43
+ "figlet": "^1.10.0",
44
+ "socket.io": "^4.7.4"
45
+ },
46
+ "devDependencies": {
47
+ "nodemon": "^3.0.3"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ }
52
+ }
package/public/main.js ADDED
@@ -0,0 +1,271 @@
1
+ const socket = io();
2
+ let peerConnection;
3
+ let dataChannel;
4
+ let currentActivePeer = null;
5
+
6
+ const config = {
7
+ iceServers: [
8
+
9
+ ]
10
+ };
11
+
12
+ const statusEl = document.getElementById('status');
13
+ const usersListEl = document.getElementById('users-list');
14
+ const networkZone = document.getElementById('network-zone');
15
+ const transferZone = document.getElementById('transfer-zone');
16
+ const fileInput = document.getElementById('file-input');
17
+ const progressContainer = document.getElementById('progress-container');
18
+ const fileProgress = document.getElementById('file-progress');
19
+ const progressText = document.getElementById('progress-text');
20
+ const receivedFilesList = document.getElementById('received-files');
21
+
22
+ const requestModal = document.getElementById('request-modal');
23
+ const requesterIdEl = document.getElementById('requester-id');
24
+ const acceptBtn = document.getElementById('accept-btn');
25
+ const rejectBtn = document.getElementById('reject-btn');
26
+ const disconnectBtn = document.getElementById('disconnect-btn');
27
+
28
+ let receiveBuffer = [];
29
+ let receivedSize = 0;
30
+ let expectedFileSize = 0;
31
+ let expectedFileName = '';
32
+ let pendingRequester = null;
33
+
34
+ function updateStatus(message, isError = false) {
35
+ statusEl.textContent = message;
36
+ statusEl.style.color = isError ? 'var(--danger)' : 'var(--primary)';
37
+ statusEl.style.background = isError ? '#ffebee' : '#eef2ff';
38
+ }
39
+
40
+ socket.on('connect', () => {
41
+ updateStatus(`My ID: ${socket.id.substring(0, 5)}`);
42
+ });
43
+
44
+ socket.on('update-user-list', (users) => {
45
+ usersListEl.innerHTML = '';
46
+ const otherUsers = users.filter(u => u.id !== socket.id);
47
+
48
+ if (otherUsers.length === 0) {
49
+ usersListEl.innerHTML = '<p style="color: #666; font-size: 0.9rem;">No other devices on network.</p>';
50
+ return;
51
+ }
52
+
53
+ otherUsers.forEach(user => {
54
+ const btn = document.createElement('button');
55
+ btn.className = 'user-btn';
56
+ btn.innerHTML = `<span>📱 Device_${user.id.substring(0, 5)}</span> <span>Connect →</span>`;
57
+ btn.onclick = () => sendConnectionRequest(user.id);
58
+ usersListEl.appendChild(btn);
59
+ });
60
+ });
61
+
62
+ function sendConnectionRequest(targetId) {
63
+ updateStatus(`Sending request to Device_${targetId.substring(0, 5)}...`);
64
+ socket.emit('connection-request', { to: targetId });
65
+ }
66
+
67
+ socket.on('connection-request', (data) => {
68
+ if (currentActivePeer) {
69
+ socket.emit('connection-response', { to: data.from, accepted: false });
70
+ return;
71
+ }
72
+ pendingRequester = data.from;
73
+ requesterIdEl.textContent = `Device_${data.from.substring(0, 5)}`;
74
+ requestModal.style.display = 'flex';
75
+ });
76
+
77
+ acceptBtn.onclick = () => {
78
+ requestModal.style.display = 'none';
79
+ socket.emit('connection-response', { to: pendingRequester, accepted: true });
80
+ updateStatus(`Accepted request. Establishing connection...`);
81
+ };
82
+
83
+ rejectBtn.onclick = () => {
84
+ requestModal.style.display = 'none';
85
+ socket.emit('connection-response', { to: pendingRequester, accepted: false });
86
+ pendingRequester = null;
87
+ updateStatus(`My ID: ${socket.id.substring(0, 5)}`);
88
+ };
89
+
90
+ socket.on('connection-response', async (data) => {
91
+ if (data.accepted) {
92
+ updateStatus(`Request accepted! Connecting to Device_${data.from.substring(0, 5)}...`);
93
+ await initiateWebRTCConnection(data.from);
94
+ } else {
95
+ updateStatus(`Device_${data.from.substring(0, 5)} rejected your request.`, true);
96
+ setTimeout(() => updateStatus(`My ID: ${socket.id.substring(0, 5)}`), 3000);
97
+ }
98
+ });
99
+
100
+ function setupPeerConnection(targetId) {
101
+ peerConnection = new RTCPeerConnection(config);
102
+ currentActivePeer = targetId;
103
+
104
+ peerConnection.onicecandidate = (event) => {
105
+ if (event.candidate) {
106
+ socket.emit('ice-candidate', { to: targetId, candidate: event.candidate });
107
+ }
108
+ };
109
+
110
+ peerConnection.onconnectionstatechange = () => {
111
+ if (peerConnection.connectionState === 'connected') {
112
+ updateStatus(`🟢 Connected to Device_${targetId.substring(0, 5)}`);
113
+ networkZone.style.display = 'none';
114
+ transferZone.style.display = 'block';
115
+ } else if (peerConnection.connectionState === 'disconnected' || peerConnection.connectionState === 'failed') {
116
+ handleDisconnection(false);
117
+ }
118
+ };
119
+
120
+ peerConnection.ondatachannel = (event) => {
121
+ dataChannel = event.channel;
122
+ setupDataChannel();
123
+ };
124
+ }
125
+
126
+ function handleDisconnection(notifyPeer = true) {
127
+ if (notifyPeer && currentActivePeer) {
128
+ socket.emit('peer-disconnected', { to: currentActivePeer });
129
+ }
130
+
131
+ if (peerConnection) {
132
+ peerConnection.close();
133
+ peerConnection = null;
134
+ }
135
+
136
+ currentActivePeer = null;
137
+ dataChannel = null;
138
+ networkZone.style.display = 'block';
139
+ transferZone.style.display = 'none';
140
+ updateStatus(`My ID: ${socket.id.substring(0, 5)}`);
141
+ }
142
+
143
+ disconnectBtn.onclick = () => handleDisconnection(true);
144
+
145
+ function setupDataChannel() {
146
+ dataChannel.binaryType = 'arraybuffer';
147
+
148
+ dataChannel.onmessage = (event) => {
149
+ if (typeof event.data === 'string') {
150
+ const metadata = JSON.parse(event.data);
151
+ expectedFileName = metadata.name;
152
+ expectedFileSize = metadata.size;
153
+ receiveBuffer = [];
154
+ receivedSize = 0;
155
+ progressContainer.style.display = 'block';
156
+ } else {
157
+ receiveBuffer.push(event.data);
158
+ receivedSize += event.data.byteLength;
159
+
160
+ const percentage = Math.round((receivedSize / expectedFileSize) * 100);
161
+ fileProgress.value = percentage;
162
+ progressText.textContent = `Receiving: ${percentage}%`;
163
+
164
+ if (receivedSize === expectedFileSize) {
165
+ const blob = new Blob(receiveBuffer);
166
+ const downloadUrl = URL.createObjectURL(blob);
167
+ const li = document.createElement('li');
168
+ const a = document.createElement('a');
169
+ a.href = downloadUrl;
170
+ a.download = expectedFileName;
171
+ a.textContent = `💾 ${expectedFileName}`;
172
+ li.appendChild(a);
173
+ receivedFilesList.appendChild(li);
174
+
175
+ progressText.textContent = 'Complete!';
176
+ setTimeout(() => progressContainer.style.display = 'none', 2000);
177
+ }
178
+ }
179
+ };
180
+ }
181
+
182
+ async function initiateWebRTCConnection(targetId) {
183
+ setupPeerConnection(targetId);
184
+ dataChannel = peerConnection.createDataChannel('fileTransfer');
185
+ setupDataChannel();
186
+
187
+ const offer = await peerConnection.createOffer();
188
+ await peerConnection.setLocalDescription(offer);
189
+
190
+ socket.emit('offer', { to: targetId, offer: offer });
191
+ }
192
+
193
+ socket.on('offer', async (data) => {
194
+ setupPeerConnection(data.sender);
195
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
196
+ const answer = await peerConnection.createAnswer();
197
+ await peerConnection.setLocalDescription(answer);
198
+ socket.emit('answer', { to: data.sender, answer: answer });
199
+ });
200
+
201
+ socket.on('answer', async (data) => {
202
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
203
+ });
204
+
205
+ socket.on('ice-candidate', async (data) => {
206
+ try {
207
+ await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
208
+ } catch (e) {
209
+ console.error(e);
210
+ }
211
+ });
212
+
213
+ fileInput.addEventListener('change', () => {
214
+ const file = fileInput.files[0];
215
+ if (!file || !dataChannel || dataChannel.readyState !== 'open') return;
216
+
217
+ dataChannel.send(JSON.stringify({ name: file.name, size: file.size }));
218
+
219
+ const chunkSize = 16384;
220
+ let offset = 0;
221
+
222
+ progressContainer.style.display = 'block';
223
+
224
+ const readSlice = (o) => {
225
+ const slice = file.slice(offset, o + chunkSize);
226
+ const reader = new FileReader();
227
+
228
+ reader.onload = (e) => {
229
+ dataChannel.send(e.target.result);
230
+ offset += chunkSize;
231
+
232
+ const percentage = Math.round((offset / file.size) * 100);
233
+ fileProgress.value = Math.min(percentage, 100);
234
+ progressText.textContent = `Sending: ${Math.min(percentage, 100)}%`;
235
+
236
+ if (offset < file.size) {
237
+ if (dataChannel.bufferedAmount > 65535) {
238
+ dataChannel.onbufferedamountlow = () => {
239
+ dataChannel.onbufferedamountlow = null;
240
+ readSlice(offset);
241
+ };
242
+ } else {
243
+ readSlice(offset);
244
+ }
245
+ } else {
246
+ progressText.textContent = 'Sent!';
247
+ setTimeout(() => {
248
+ progressContainer.style.display = 'none';
249
+ fileInput.value = '';
250
+ }, 2000);
251
+ }
252
+ };
253
+ reader.readAsArrayBuffer(slice);
254
+ };
255
+
256
+ readSlice(0);
257
+ });
258
+
259
+ function handleDisconnection() {
260
+ if (peerConnection) {
261
+ peerConnection.close();
262
+ peerConnection = null;
263
+ }
264
+ currentActivePeer = null;
265
+ dataChannel = null;
266
+ networkZone.style.display = 'block';
267
+ transferZone.style.display = 'none';
268
+ updateStatus(`My ID: ${socket.id.substring(0, 5)}`);
269
+ }
270
+
271
+ disconnectBtn.onclick = handleDisconnection;
@@ -0,0 +1,193 @@
1
+ :root {
2
+ --bg: #f4f7f6;
3
+ --primary: #4361ee;
4
+ --primary-hover: #3a53d0;
5
+ --text: #333;
6
+ --card-bg: #fff;
7
+ --success: #2ecc71;
8
+ --danger: #e74c3c;
9
+ --border: #e0e0e0;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
17
+ }
18
+
19
+ body {
20
+ background-color: var(--bg);
21
+ color: var(--text);
22
+ display: flex;
23
+ justify-content: center;
24
+ padding: 20px;
25
+ }
26
+
27
+ .container {
28
+ background: var(--card-bg);
29
+ width: 100%;
30
+ max-width: 500px;
31
+ border-radius: 12px;
32
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
33
+ padding: 24px;
34
+ }
35
+
36
+ h1 {
37
+ text-align: center;
38
+ font-size: 1.5rem;
39
+ margin-bottom: 8px;
40
+ color: var(--primary);
41
+ }
42
+
43
+ .status-badge {
44
+ display: block;
45
+ text-align: center;
46
+ font-size: 0.85rem;
47
+ padding: 6px 12px;
48
+ border-radius: 20px;
49
+ background: #eef2ff;
50
+ color: var(--primary);
51
+ margin-bottom: 24px;
52
+ font-weight: 500;
53
+ }
54
+
55
+ h3 {
56
+ font-size: 1.1rem;
57
+ margin: 20px 0 12px;
58
+ border-bottom: 1px solid var(--border);
59
+ padding-bottom: 8px;
60
+ }
61
+
62
+ .user-btn {
63
+ width: 100%;
64
+ background: var(--card-bg);
65
+ border: 1px solid var(--border);
66
+ padding: 12px;
67
+ border-radius: 8px;
68
+ font-size: 1rem;
69
+ cursor: pointer;
70
+ display: flex;
71
+ justify-content: space-between;
72
+ align-items: center;
73
+ margin-bottom: 8px;
74
+ transition: all 0.2s;
75
+ }
76
+
77
+ .user-btn:hover {
78
+ border-color: var(--primary);
79
+ background: #f8faff;
80
+ }
81
+
82
+ .modal-overlay {
83
+ position: fixed;
84
+ top: 0;
85
+ left: 0;
86
+ right: 0;
87
+ bottom: 0;
88
+ background: rgba(0, 0, 0, 0.5);
89
+ display: none;
90
+ justify-content: center;
91
+ align-items: center;
92
+ z-index: 1000;
93
+ }
94
+
95
+ .modal {
96
+ background: var(--card-bg);
97
+ padding: 24px;
98
+ border-radius: 12px;
99
+ width: 90%;
100
+ max-width: 350px;
101
+ text-align: center;
102
+ }
103
+
104
+ .modal p {
105
+ margin-bottom: 20px;
106
+ font-size: 1.1rem;
107
+ }
108
+
109
+ .modal-actions {
110
+ display: flex;
111
+ gap: 12px;
112
+ }
113
+
114
+ .btn {
115
+ flex: 1;
116
+ padding: 10px;
117
+ border: none;
118
+ border-radius: 6px;
119
+ font-size: 1rem;
120
+ cursor: pointer;
121
+ color: white;
122
+ font-weight: 500;
123
+ }
124
+
125
+ .btn-accept {
126
+ background: var(--success);
127
+ }
128
+
129
+ .btn-reject {
130
+ background: var(--danger);
131
+ }
132
+
133
+ .btn-primary {
134
+ background: var(--primary);
135
+ width: 100%;
136
+ margin-top: 10px;
137
+ }
138
+
139
+ input[type="file"] {
140
+ width: 100%;
141
+ padding: 10px;
142
+ border: 1px dashed var(--primary);
143
+ border-radius: 8px;
144
+ background: #f8faff;
145
+ cursor: pointer;
146
+ }
147
+
148
+ .progress-wrapper {
149
+ margin-top: 15px;
150
+ display: none;
151
+ }
152
+
153
+ progress {
154
+ width: 100%;
155
+ height: 8px;
156
+ border-radius: 4px;
157
+ }
158
+
159
+ progress::-webkit-progress-bar {
160
+ background-color: var(--border);
161
+ border-radius: 4px;
162
+ }
163
+
164
+ progress::-webkit-progress-value {
165
+ background-color: var(--primary);
166
+ border-radius: 4px;
167
+ }
168
+
169
+ .progress-text {
170
+ display: block;
171
+ text-align: right;
172
+ font-size: 0.85rem;
173
+ margin-top: 4px;
174
+ font-weight: 500;
175
+ }
176
+
177
+ #received-files {
178
+ list-style: none;
179
+ }
180
+
181
+ #received-files li {
182
+ background: #f8faff;
183
+ border: 1px solid var(--border);
184
+ padding: 12px;
185
+ border-radius: 8px;
186
+ margin-bottom: 8px;
187
+ }
188
+
189
+ #received-files a {
190
+ text-decoration: none;
191
+ color: var(--primary);
192
+ font-weight: 500;
193
+ }
package/server.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import express from 'express';
3
+ import { createServer } from 'http';
4
+ import { Server } from 'socket.io';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import chalk from 'chalk';
8
+ import boxen from 'boxen';
9
+ import figlet from 'figlet';
10
+ import os from 'os';
11
+
12
+ const args = process.argv.slice(2);
13
+ if (args[0] !== 'start') {
14
+ console.log(chalk.red('\n❌ Invalid command. Please run:\n👉 npx laracast start\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ const app = express();
22
+ const server = createServer(app);
23
+ const io = new Server(server, {
24
+ cors: {
25
+ origin: "*",
26
+ methods: ["GET", "POST"]
27
+ }
28
+ });
29
+
30
+ app.set('view engine', 'ejs');
31
+ app.set('views', path.join(__dirname, 'views'));
32
+ app.use(express.static(path.join(__dirname, 'public')));
33
+
34
+ app.get('/', (req, res) => {
35
+ res.render('index');
36
+ });
37
+
38
+ const connectedUsers = new Map();
39
+
40
+ io.on('connection', (socket) => {
41
+ connectedUsers.set(socket.id, { id: socket.id });
42
+
43
+ io.emit('update-user-list', Array.from(connectedUsers.values()));
44
+
45
+ socket.on('connection-request', (data) => {
46
+ socket.to(data.to).emit('connection-request', {
47
+ from: socket.id
48
+ });
49
+ });
50
+
51
+ socket.on('connection-response', (data) => {
52
+ socket.to(data.to).emit('connection-response', {
53
+ from: socket.id,
54
+ accepted: data.accepted
55
+ });
56
+ });
57
+
58
+ socket.on('offer', (data) => {
59
+ socket.to(data.to).emit('offer', {
60
+ offer: data.offer,
61
+ sender: socket.id
62
+ });
63
+ });
64
+
65
+ socket.on('peer-disconnected', (data) => {
66
+ socket.to(data.to).emit('peer-disconnected');
67
+ });
68
+
69
+ socket.on('answer', (data) => {
70
+ socket.to(data.to).emit('answer', {
71
+ answer: data.answer,
72
+ sender: socket.id
73
+ });
74
+ });
75
+
76
+ socket.on('ice-candidate', (data) => {
77
+ socket.to(data.to).emit('ice-candidate', {
78
+ candidate: data.candidate,
79
+ sender: socket.id
80
+ });
81
+ });
82
+
83
+ socket.on('disconnect', () => {
84
+ connectedUsers.delete(socket.id);
85
+ io.emit('update-user-list', Array.from(connectedUsers.values()));
86
+ });
87
+ });
88
+
89
+ const PORT = process.env.PORT || 3150;
90
+
91
+ function getLocalIP() {
92
+ const interfaces = os.networkInterfaces();
93
+ for (const name of Object.keys(interfaces)) {
94
+ for (const iface of interfaces[name]) {
95
+ if (iface.family === 'IPv4' && !iface.internal) {
96
+ return iface.address;
97
+ }
98
+ }
99
+ }
100
+ return '127.0.0.1';
101
+ }
102
+
103
+ server.listen(PORT, '0.0.0.0', () => {
104
+ console.clear();
105
+
106
+ const title = figlet.textSync('LARACAST', {
107
+ font: 'Slant',
108
+ horizontalLayout: 'fitted',
109
+ verticalLayout: 'default'
110
+ });
111
+
112
+ console.log(chalk.cyan(title));
113
+
114
+ const lanIP = getLocalIP();
115
+
116
+ const message = `
117
+ 🚀 Server Running Successfully
118
+
119
+ 🌐 Local: http://localhost:${PORT}
120
+ 📡 Network: http://${lanIP}:${PORT}
121
+
122
+ Use port ${PORT}
123
+ `;
124
+
125
+ console.log(
126
+ boxen(chalk.green(message), {
127
+ padding: 1,
128
+ margin: 1,
129
+ borderStyle: 'round',
130
+ borderColor: 'cyan'
131
+ })
132
+ );
133
+ });
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>LANCast</title>
8
+ <link rel="stylesheet" href="/style.css">
9
+ </head>
10
+
11
+ <body>
12
+ <div class="container">
13
+ <h1>LANCast P2P</h1>
14
+ <span id="status" class="status-badge">Connecting to signaling server...</span>
15
+
16
+ <div id="network-zone">
17
+ <h3>Available Devices</h3>
18
+ <div id="users-list"></div>
19
+ </div>
20
+
21
+ <div id="transfer-zone" style="display: none;">
22
+ <h3>Transfer Files</h3>
23
+ <input type="file" id="file-input" />
24
+ <div id="progress-container" class="progress-wrapper">
25
+ <progress id="file-progress" value="0" max="100"></progress>
26
+ <span id="progress-text" class="progress-text">0%</span>
27
+ </div>
28
+ <button id="disconnect-btn" class="btn btn-primary"
29
+ style="background: var(--danger); margin-top: 15px;">Disconnect</button>
30
+ </div>
31
+
32
+ <h3>Received Files</h3>
33
+ <ul id="received-files"></ul>
34
+ </div>
35
+
36
+ <div id="request-modal" class="modal-overlay">
37
+ <div class="modal">
38
+ <p><strong id="requester-id"></strong> wants to connect and share files.</p>
39
+ <div class="modal-actions">
40
+ <button id="accept-btn" class="btn btn-accept">Accept</button>
41
+ <button id="reject-btn" class="btn btn-reject">Reject</button>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <script src="/socket.io/socket.io.js"></script>
47
+ <script src="/main.js"></script>
48
+ </body>
49
+
50
+ </html>