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 +125 -0
- package/package.json +52 -0
- package/public/main.js +271 -0
- package/public/style.css +193 -0
- package/server.js +133 -0
- package/views/index.ejs +50 -0
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;
|
package/public/style.css
ADDED
|
@@ -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
|
+
});
|
package/views/index.ejs
ADDED
|
@@ -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>
|