lan-send 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/LICENSE +21 -0
- package/README.md +47 -0
- package/index.js +94 -0
- package/package.json +35 -0
- package/public/client.js +91 -0
- package/public/style.css +171 -0
- package/views/desktop.html +31 -0
- package/views/mobile.html +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## 🛰️ lan-send
|
|
2
|
+
Instant, zero-disk file bridge for your local network.
|
|
3
|
+
|
|
4
|
+
## 🚀 Quick Start
|
|
5
|
+
No installation required. Run this in your terminal to start beaming:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx lan-send
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
1. **Scan** the QR code displayed in your terminal with your phone.
|
|
12
|
+
|
|
13
|
+
2. **Drop** files on your desktop or **Tap** "Select Files" on mobile.
|
|
14
|
+
|
|
15
|
+
3. **Download** on the other device by tapping the **READY** cards.
|
|
16
|
+
|
|
17
|
+
## 🛠️ Global Installation
|
|
18
|
+
If you want the command available anytime:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g lan-send
|
|
22
|
+
```
|
|
23
|
+
Then just run:
|
|
24
|
+
```bash
|
|
25
|
+
lan-send
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## ✨ Features
|
|
29
|
+
**Volatile by Design:** Files are stored strictly in RAM. Once the process is stopped, the data vanishes—leaving no trace on your hard drive.
|
|
30
|
+
|
|
31
|
+
**Dual-Sync History:** Tracks both sent and received history on both devices. Your history persists even if you refresh the browser page.
|
|
32
|
+
|
|
33
|
+
**WSL2 Optimized:** Automatically detects your host machine's real IP, bypassing the virtual WSL bridge so mobile devices can connect seamlessly.
|
|
34
|
+
|
|
35
|
+
**Mobile-First UI:** Large, tapable cards and a simplified mobile interface designed for one-handed use.
|
|
36
|
+
|
|
37
|
+
**Real-time Feedback:** Powered by Server-Sent Events (SSE) for instant notifications and per-file progress bars.
|
|
38
|
+
|
|
39
|
+
## 🔒 Privacy & Security
|
|
40
|
+
**Local Network Only:** Your data never touches the internet or third-party servers.
|
|
41
|
+
|
|
42
|
+
**Zero Footprint:** Ideal for temporary file sharing where you don't want to leave artifacts on the host machine's SSD/HDD.
|
|
43
|
+
|
|
44
|
+
**Open & Minimal:** Transparent logic with minimal dependencies and zero telemetry.
|
|
45
|
+
|
|
46
|
+
## 📜 License
|
|
47
|
+
MIT © 2026
|
package/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { serve } from '@hono/node-server';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { streamSSE } from 'hono/streaming';
|
|
5
|
+
import QRCode from 'qrcode';
|
|
6
|
+
import color from 'picocolors';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { networkInterfaces } from 'node:os';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
const PORT = 8000;
|
|
15
|
+
|
|
16
|
+
const IP = (() => {
|
|
17
|
+
const nets = networkInterfaces();
|
|
18
|
+
let backupIP = 'localhost';
|
|
19
|
+
|
|
20
|
+
for (const name of Object.keys(nets)) {
|
|
21
|
+
// Skip virtual adapters often used by WSL, Docker, and VPNs
|
|
22
|
+
if (name.includes('vEthernet') || name.includes('docker') || name.includes('wsl')) continue;
|
|
23
|
+
|
|
24
|
+
for (const net of nets[name]) {
|
|
25
|
+
// We want IPv4 and it must not be the internal loopback (127.0.0.1)
|
|
26
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
27
|
+
// Prioritize "Wi-Fi" or "Ethernet" naming conventions
|
|
28
|
+
if (name.toLowerCase().includes('wi-fi') || name.toLowerCase().includes('eth')) {
|
|
29
|
+
return net.address;
|
|
30
|
+
}
|
|
31
|
+
backupIP = net.address;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return backupIP;
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
const memoryFiles = new Map();
|
|
39
|
+
const history = [];
|
|
40
|
+
let clients = [];
|
|
41
|
+
|
|
42
|
+
app.get('/static/:file', (c) => {
|
|
43
|
+
const file = c.req.param('file');
|
|
44
|
+
const filePath = path.join(__dirname, 'public', file);
|
|
45
|
+
if (!fs.existsSync(filePath)) return c.text("404", 404);
|
|
46
|
+
const type = file.endsWith('.css') ? 'text/css' : 'application/javascript';
|
|
47
|
+
return c.body(fs.readFileSync(filePath), 200, { 'Content-Type': type });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const getTemplate = (name) => fs.readFileSync(path.join(__dirname, 'views', `${name}.html`), 'utf-8');
|
|
51
|
+
app.get('/', (c) => c.html(getTemplate('mobile')));
|
|
52
|
+
app.get('/desktop', (c) => c.html(getTemplate('desktop')));
|
|
53
|
+
|
|
54
|
+
app.get('/api/history', (c) => c.json(history));
|
|
55
|
+
|
|
56
|
+
app.post('/upload', async (c) => {
|
|
57
|
+
const from = c.req.query('from');
|
|
58
|
+
const body = await c.req.parseBody();
|
|
59
|
+
const file = body['file'];
|
|
60
|
+
if (file instanceof File) {
|
|
61
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
62
|
+
const id = 'h-' + Math.random().toString(36).substring(2, 9);
|
|
63
|
+
memoryFiles.set(file.name, { buffer, from });
|
|
64
|
+
history.push({ id, name: file.name, from });
|
|
65
|
+
if(history.length > 20) history.shift();
|
|
66
|
+
clients.forEach(s => s.writeSSE({ data: "u" }));
|
|
67
|
+
}
|
|
68
|
+
return c.json({ ok: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
app.get('/dl/:name', (c) => {
|
|
72
|
+
const name = decodeURIComponent(c.req.param('name'));
|
|
73
|
+
const file = memoryFiles.get(name);
|
|
74
|
+
if (!file) return c.text("Expired", 404);
|
|
75
|
+
return c.body(file.buffer, 200, { 'Content-Disposition': `attachment; filename="${name}"` });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
app.get('/sse', (c) => streamSSE(c, async (s) => {
|
|
79
|
+
clients.push(s);
|
|
80
|
+
while (true) { await s.sleep(30000); await s.writeSSE({ data: "p" }); }
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
serve({ fetch: app.fetch, port: PORT, hostname: '0.0.0.0' }, async () => {
|
|
84
|
+
console.clear();
|
|
85
|
+
const mobileUrl = `http://${IP}:${PORT}`;
|
|
86
|
+
console.log(color.cyan(color.bold("⚡ TELE-PORT ACTIVE")));
|
|
87
|
+
console.log(color.dim("------------------------------------------"));
|
|
88
|
+
console.log(color.white("Scan this code to teleport files to/from mobile:"));
|
|
89
|
+
console.log(await QRCode.toString(mobileUrl, { type: 'terminal', small: true }));
|
|
90
|
+
console.log(color.yellow("📱 MOBILE: ") + color.white(mobileUrl));
|
|
91
|
+
console.log(color.blue("💻 DESKTOP: ") + color.white(`http://localhost:${PORT}/desktop`));
|
|
92
|
+
console.log(color.dim("------------------------------------------"));
|
|
93
|
+
console.log(color.dim("Running in Volatile Mode (RAM only)."));
|
|
94
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lan-send",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-disk, RAM-only file bridge for instant local network sharing via QR code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"lan-send": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"file-share",
|
|
12
|
+
"lan",
|
|
13
|
+
"p2p",
|
|
14
|
+
"qr-code",
|
|
15
|
+
"transfer",
|
|
16
|
+
"volatile",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"public/",
|
|
24
|
+
"views/"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@hono/node-server": "^1.13.0",
|
|
28
|
+
"hono": "^4.6.0",
|
|
29
|
+
"picocolors": "^1.1.0",
|
|
30
|
+
"qrcode": "^1.5.4"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/public/client.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
let sse = new EventSource("/sse");
|
|
2
|
+
let knownIds = new Set();
|
|
3
|
+
const role = window.location.pathname === '/desktop' ? 'pc' : 'mobile';
|
|
4
|
+
|
|
5
|
+
async function syncAllHistory() {
|
|
6
|
+
const res = await fetch('/api/history');
|
|
7
|
+
const data = await res.json();
|
|
8
|
+
|
|
9
|
+
data.forEach(item => {
|
|
10
|
+
if (!knownIds.has(item.id)) {
|
|
11
|
+
knownIds.add(item.id);
|
|
12
|
+
|
|
13
|
+
// Determine if this device was the sender or receiver
|
|
14
|
+
const isSentByMe = item.from === role;
|
|
15
|
+
const targetFeedId = isSentByMe ? 'sent-feed' : 'inbox-feed';
|
|
16
|
+
const feed = document.getElementById(targetFeedId);
|
|
17
|
+
|
|
18
|
+
const el = document.createElement('div');
|
|
19
|
+
el.id = item.id;
|
|
20
|
+
|
|
21
|
+
if (!isSentByMe) {
|
|
22
|
+
// Incoming items get the clickable download logic
|
|
23
|
+
el.className = 'feed-item clickable';
|
|
24
|
+
el.onclick = () => window.location.href = `/dl/${encodeURIComponent(item.name)}`;
|
|
25
|
+
el.innerHTML = `
|
|
26
|
+
<div class="item-row">
|
|
27
|
+
<span class="file-name">📥 ${item.name}</span>
|
|
28
|
+
<div class="status-group">
|
|
29
|
+
<span class="status-tag status-ready">READY</span>
|
|
30
|
+
<span class="status-tag status-dl">DL</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>`;
|
|
33
|
+
} else {
|
|
34
|
+
// Sent items (persisted from server)
|
|
35
|
+
el.className = 'feed-item';
|
|
36
|
+
el.innerHTML = `
|
|
37
|
+
<div class="item-row">
|
|
38
|
+
<span class="file-name">📤 ${item.name}</span>
|
|
39
|
+
<span class="status-tag status-ready">SENT</span>
|
|
40
|
+
</div>`;
|
|
41
|
+
}
|
|
42
|
+
feed.prepend(el);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handles the active, real-time upload progress
|
|
48
|
+
async function doUpload(files) {
|
|
49
|
+
const queue = Array.from(files).map(f => ({ f, id: 'up-' + Math.random().toString(36).substr(2, 9) }));
|
|
50
|
+
const sentFeed = document.getElementById('sent-feed');
|
|
51
|
+
|
|
52
|
+
for (const item of queue) {
|
|
53
|
+
// Create a temporary progress card
|
|
54
|
+
let progEl = document.createElement('div');
|
|
55
|
+
progEl.id = item.id;
|
|
56
|
+
progEl.className = 'feed-item';
|
|
57
|
+
sentFeed.prepend(progEl);
|
|
58
|
+
|
|
59
|
+
await new Promise((resolve) => {
|
|
60
|
+
const xhr = new XMLHttpRequest();
|
|
61
|
+
const fd = new FormData();
|
|
62
|
+
fd.append('file', item.f);
|
|
63
|
+
|
|
64
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
65
|
+
const percent = Math.round((e.loaded / e.total) * 100);
|
|
66
|
+
progEl.innerHTML = `
|
|
67
|
+
<div class="item-row">
|
|
68
|
+
<span class="file-name">📤 ${item.f.name}</span>
|
|
69
|
+
<span class="status-tag status-sending">${percent}%</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="progress-container" style="display: block">
|
|
72
|
+
<div class="progress-fill" style="width: ${percent}%"></div>
|
|
73
|
+
</div>`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
xhr.onload = () => {
|
|
77
|
+
// Remove the temporary progress card
|
|
78
|
+
progEl.remove();
|
|
79
|
+
// The server will trigger an SSE update, and syncAllHistory()
|
|
80
|
+
// will add the permanent "SENT" card to this device
|
|
81
|
+
resolve();
|
|
82
|
+
};
|
|
83
|
+
xhr.open('POST', '/upload?from=' + role);
|
|
84
|
+
xhr.send(fd);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
window.onload = syncAllHistory;
|
|
90
|
+
// When the server says "u" (upload happened), sync everything
|
|
91
|
+
sse.onmessage = (e) => { if(e.data === "u") syncAllHistory(); };
|
package/public/style.css
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* Color Palette & Variables */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #0f172a;
|
|
4
|
+
--card: #1e293b;
|
|
5
|
+
--accent: #22d3ee;
|
|
6
|
+
--text: #f1f5f9;
|
|
7
|
+
--ready: #10b981;
|
|
8
|
+
--sending: #f59e0b;
|
|
9
|
+
--pending: #64748b;
|
|
10
|
+
--border: #334155;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Global Reset & Body */
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
16
|
+
background: var(--bg);
|
|
17
|
+
color: var(--text);
|
|
18
|
+
padding: 20px;
|
|
19
|
+
margin: 0;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
align-items: center;
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.container {
|
|
27
|
+
width: 100%;
|
|
28
|
+
max-width: 500px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Typography */
|
|
32
|
+
h2 {
|
|
33
|
+
text-align: center;
|
|
34
|
+
font-weight: 800;
|
|
35
|
+
letter-spacing: -0.5px;
|
|
36
|
+
margin-bottom: 40px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.history-title {
|
|
40
|
+
font-size: 11px;
|
|
41
|
+
color: #94a3b8;
|
|
42
|
+
margin: 30px 0 10px 0;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 1.5px;
|
|
45
|
+
font-weight: bold;
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Main Action Button (Mobile) */
|
|
50
|
+
.btn {
|
|
51
|
+
background: var(--accent);
|
|
52
|
+
color: #0f172a;
|
|
53
|
+
padding: 12px 24px;
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
text-decoration: none;
|
|
56
|
+
font-weight: bold;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
border: none;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: opacity 0.2s;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.mobile-btn-wrapper {
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.btn:active {
|
|
68
|
+
opacity: 0.8;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Desktop Drop Zone */
|
|
72
|
+
.drop-zone {
|
|
73
|
+
border: 2px dashed var(--border);
|
|
74
|
+
border-radius: 15px;
|
|
75
|
+
padding: 40px 20px;
|
|
76
|
+
text-align: center;
|
|
77
|
+
margin-bottom: 20px;
|
|
78
|
+
background: #1e293b55;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
transition: all 0.2s ease;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Feed Item Cards */
|
|
84
|
+
.feed-item {
|
|
85
|
+
background: var(--card);
|
|
86
|
+
border-radius: 12px;
|
|
87
|
+
padding: 14px 16px;
|
|
88
|
+
margin-bottom: 12px;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
border: 1px solid var(--border);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.feed-item.clickable {
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
transition: transform 0.1s, background 0.2s, border-color 0.2s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.feed-item.clickable:hover {
|
|
100
|
+
border-color: var(--accent);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.feed-item.clickable:active {
|
|
104
|
+
transform: scale(0.98);
|
|
105
|
+
background: #2d3748;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.item-row {
|
|
109
|
+
display: flex;
|
|
110
|
+
justify-content: space-between;
|
|
111
|
+
align-items: center;
|
|
112
|
+
width: 100%;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.file-name {
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
font-weight: 500;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
text-overflow: ellipsis;
|
|
120
|
+
white-space: nowrap;
|
|
121
|
+
max-width: 220px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Refined Status Tags */
|
|
125
|
+
.status-group {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 6px;
|
|
128
|
+
align-items: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.status-tag {
|
|
132
|
+
font-size: 10px;
|
|
133
|
+
padding: 6px 10px; /* Taller height, tighter width */
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
font-weight: 800;
|
|
136
|
+
text-transform: uppercase;
|
|
137
|
+
text-align: center;
|
|
138
|
+
line-height: 1;
|
|
139
|
+
display: inline-block;
|
|
140
|
+
min-width: 50px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.status-ready { background: var(--ready); color: #fff; }
|
|
144
|
+
.status-sending { background: var(--sending); color: #000; }
|
|
145
|
+
.status-pending { background: var(--pending); color: #fff; }
|
|
146
|
+
.status-dl { background: var(--accent); color: #0f172a; }
|
|
147
|
+
|
|
148
|
+
/* Progress Bar */
|
|
149
|
+
.progress-container {
|
|
150
|
+
width: 100%;
|
|
151
|
+
height: 6px;
|
|
152
|
+
background: var(--border);
|
|
153
|
+
border-radius: 3px;
|
|
154
|
+
margin-top: 12px;
|
|
155
|
+
display: none;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.progress-fill {
|
|
160
|
+
height: 100%;
|
|
161
|
+
background: var(--accent);
|
|
162
|
+
width: 0%;
|
|
163
|
+
transition: width 0.1s ease-out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Mobile responsive adjustments */
|
|
167
|
+
@media (max-width: 400px) {
|
|
168
|
+
.file-name {
|
|
169
|
+
max-width: 150px;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>lan-send | Desktop</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div class="container">
|
|
12
|
+
<h2>LAN send - Desktop</h2>
|
|
13
|
+
<div class="drop-zone" id="dz" onclick="document.getElementById('f').click()">
|
|
14
|
+
<span class="btn" style="padding: 10px 20px;">CLICK OR DROP FILES</span>
|
|
15
|
+
<input type="file" id="f" multiple hidden onchange="doUpload(this.files)">
|
|
16
|
+
</div>
|
|
17
|
+
<div class="history-title">Incoming</div>
|
|
18
|
+
<div id="inbox-feed"></div>
|
|
19
|
+
<div class="history-title">Sent</div>
|
|
20
|
+
<div id="sent-feed"></div>
|
|
21
|
+
</div>
|
|
22
|
+
<script src="/static/client.js"></script>
|
|
23
|
+
<script>
|
|
24
|
+
const dz = document.getElementById('dz');
|
|
25
|
+
window.addEventListener('dragover', (e) => { e.preventDefault(); dz.style.borderColor = 'var(--accent)'; dz.style.background = '#22d3ee11'; });
|
|
26
|
+
window.addEventListener('dragleave', () => { dz.style.borderColor = '#334155'; dz.style.background = '#1e293b55'; });
|
|
27
|
+
window.addEventListener('drop', (e) => { e.preventDefault(); dz.style.borderColor = '#334155'; dz.style.background = '#1e293b55'; if (e.dataTransfer.files.length > 0) doUpload(e.dataTransfer.files); });
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>lan-send</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div class="container">
|
|
12
|
+
<h2 style="text-align: center;">Lan Send</h2>
|
|
13
|
+
|
|
14
|
+
<div class="mobile-btn-wrapper">
|
|
15
|
+
<button class="btn" style=""
|
|
16
|
+
onclick="document.getElementById('f').click()">
|
|
17
|
+
SELECT FILES TO SEND
|
|
18
|
+
</button>
|
|
19
|
+
<input type="file" id="f" multiple hidden onchange="doUpload(this.files)">
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="history-title">Incoming</div>
|
|
23
|
+
<div id="inbox-feed"></div>
|
|
24
|
+
|
|
25
|
+
<div class="history-title">Sent</div>
|
|
26
|
+
<div id="sent-feed"></div>
|
|
27
|
+
</div>
|
|
28
|
+
<script src="/static/client.js"></script>
|
|
29
|
+
</body>
|
|
30
|
+
|
|
31
|
+
</html>
|