add html file hosting
This commit is contained in:
Binary file not shown.
8
media/qrcode.min.js
vendored
Normal file
8
media/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
package.json
18
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "hpl-toolbox",
|
"name": "hpl-toolbox",
|
||||||
"displayName": "HPL Toolbox",
|
"displayName": "HPL Toolbox",
|
||||||
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update.",
|
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update. Local HTML Host.",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"publisher": "hesukastro",
|
"publisher": "hesukastro",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.85.0"
|
"vscode": "^1.85.0"
|
||||||
@@ -28,6 +28,10 @@
|
|||||||
{
|
{
|
||||||
"command": "hplToolbox.openDailyUpdate",
|
"command": "hplToolbox.openDailyUpdate",
|
||||||
"title": "HPL Toolbox: Open Daily Update"
|
"title": "HPL Toolbox: Open Daily Update"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "hplToolbox.openSendToMobile",
|
||||||
|
"title": "HPL Toolbox: Open Send To Mobile"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"viewsContainers": {
|
"viewsContainers": {
|
||||||
@@ -83,6 +87,16 @@
|
|||||||
"description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)."
|
"description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "HPL Toolbox — Send To Mobile",
|
||||||
|
"properties": {
|
||||||
|
"hplToolbox.sendToMobile.port": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Port for the temporary LAN HTTP server. 0 (default) = OS-assigned ephemeral port."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { openPlecUpload } from './tools/plecUpload';
|
|||||||
import { openApplovinUpload } from './tools/applovinUpload';
|
import { openApplovinUpload } from './tools/applovinUpload';
|
||||||
import { openBase64Scanner } from './tools/base64Scanner';
|
import { openBase64Scanner } from './tools/base64Scanner';
|
||||||
import { openDailyUpdate } from './tools/dailyUpdate';
|
import { openDailyUpdate } from './tools/dailyUpdate';
|
||||||
|
import { openSendToMobile } from './tools/sendToMobile';
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
@@ -11,6 +12,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.commands.registerCommand('hplToolbox.openApplovinUpload', () => openApplovinUpload(context)),
|
vscode.commands.registerCommand('hplToolbox.openApplovinUpload', () => openApplovinUpload(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)),
|
vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)),
|
||||||
|
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
|
||||||
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider())
|
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ function getHtml(): string {
|
|||||||
Daily Update
|
Daily Update
|
||||||
<span class="tool-desc">Compose & copy a daily status</span>
|
<span class="tool-desc">Compose & copy a daily status</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tool-btn" data-cmd="hplToolbox.openSendToMobile">
|
||||||
|
Send To Mobile
|
||||||
|
<span class="tool-desc">Share HTML to a phone via LAN + QR</span>
|
||||||
|
</button>
|
||||||
<script>
|
<script>
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
document.querySelectorAll('.tool-btn').forEach((btn) => {
|
document.querySelectorAll('.tool-btn').forEach((btn) => {
|
||||||
|
|||||||
434
src/tools/sendToMobile.ts
Normal file
434
src/tools/sendToMobile.ts
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { handleClipboardAndOpen } from './shared';
|
||||||
|
|
||||||
|
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||||
|
|
||||||
|
interface ActiveShare {
|
||||||
|
server: http.Server;
|
||||||
|
port: number;
|
||||||
|
token: string;
|
||||||
|
fileBuf: Buffer;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openSendToMobile(context: vscode.ExtensionContext) {
|
||||||
|
if (store.panel) {
|
||||||
|
store.panel.reveal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'hplToolbox.sendToMobile',
|
||||||
|
'Send To Mobile',
|
||||||
|
vscode.ViewColumn.Active,
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
retainContextWhenHidden: true,
|
||||||
|
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
store.panel = panel;
|
||||||
|
|
||||||
|
let active: ActiveShare | null = null;
|
||||||
|
|
||||||
|
const stopActive = () => {
|
||||||
|
if (active) {
|
||||||
|
try { active.server.close(); } catch { /* noop */ }
|
||||||
|
active = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.onDidDispose(() => {
|
||||||
|
stopActive();
|
||||||
|
store.panel = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const qrUri = panel.webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, 'media', 'qrcode.min.js')
|
||||||
|
);
|
||||||
|
panel.webview.html = getHtml(qrUri.toString());
|
||||||
|
|
||||||
|
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||||
|
try {
|
||||||
|
if (handleClipboardAndOpen(msg)) return;
|
||||||
|
|
||||||
|
if (msg.type === 'pickFile') {
|
||||||
|
const picked = await vscode.window.showOpenDialog({
|
||||||
|
canSelectMany: false,
|
||||||
|
filters: { HTML: ['html', 'htm'] },
|
||||||
|
openLabel: 'Select',
|
||||||
|
defaultUri: getPickerDefaultUri(),
|
||||||
|
});
|
||||||
|
if (picked && picked[0]) {
|
||||||
|
const fp = picked[0].fsPath;
|
||||||
|
panel.webview.postMessage({
|
||||||
|
type: 'fileSelected',
|
||||||
|
path: fp,
|
||||||
|
name: path.basename(fp),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'startShare') {
|
||||||
|
stopActive();
|
||||||
|
const filePath: string = msg.path;
|
||||||
|
if (!filePath || !fs.existsSync(filePath)) {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: 'File missing.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fileBuf = fs.readFileSync(filePath);
|
||||||
|
const filename = path.basename(filePath);
|
||||||
|
const token = crypto.randomBytes(6).toString('base64url');
|
||||||
|
|
||||||
|
const cfg = vscode.workspace.getConfiguration('hplToolbox.sendToMobile');
|
||||||
|
const configuredPort = cfg.get<number>('port') ?? 0;
|
||||||
|
|
||||||
|
const share: ActiveShare = {
|
||||||
|
server: http.createServer(),
|
||||||
|
port: 0,
|
||||||
|
token,
|
||||||
|
fileBuf,
|
||||||
|
filename,
|
||||||
|
};
|
||||||
|
share.server.on('request', (req, res) => handleRequest(req, res, share));
|
||||||
|
share.server.on('error', (err) => {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: 'Server error: ' + err.message });
|
||||||
|
stopActive();
|
||||||
|
});
|
||||||
|
|
||||||
|
share.server.listen(configuredPort, '0.0.0.0', () => {
|
||||||
|
const addr = share.server.address();
|
||||||
|
if (!addr || typeof addr === 'string') {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: 'Failed to bind server.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
share.port = addr.port;
|
||||||
|
active = share;
|
||||||
|
|
||||||
|
const ips = getLanIps();
|
||||||
|
if (ips.length === 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: 'No non-loopback IPv4 interface found. Are you on a network?',
|
||||||
|
});
|
||||||
|
stopActive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const urls = ips.map((ip) => ({
|
||||||
|
ip: ip.address,
|
||||||
|
iface: ip.iface,
|
||||||
|
url: `http://${ip.address}:${share.port}/s/${token}/`,
|
||||||
|
}));
|
||||||
|
panel.webview.postMessage({ type: 'sharing', urls, filename });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'stopShare') {
|
||||||
|
stopActive();
|
||||||
|
panel.webview.postMessage({ type: 'stopped' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, share: ActiveShare) {
|
||||||
|
if (req.method !== 'GET' || !req.url) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prefix = `/s/${share.token}`;
|
||||||
|
if (!req.url.startsWith(prefix)) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rest = req.url.slice(prefix.length).replace(/\?.*$/, '');
|
||||||
|
|
||||||
|
if (rest === '' || rest === '/') {
|
||||||
|
const body = chooserPage(share.filename);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
res.end(body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rest === '/view') {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
res.end(share.fileBuf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rest === '/file') {
|
||||||
|
const safeName = share.filename.replace(/[^A-Za-z0-9._-]/g, '_');
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Disposition': `attachment; filename="${safeName}"`,
|
||||||
|
'Content-Length': String(share.fileBuf.length),
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
res.end(share.fileBuf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooserPage(filename: string): string {
|
||||||
|
const escName = filename.replace(/[&<>"]/g, (c) =>
|
||||||
|
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c] as string)
|
||||||
|
);
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Send To Mobile</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
margin: 0; padding: 24px; background: #111; color: #eee; }
|
||||||
|
h1 { font-size: 18px; font-weight: 500; margin: 0 0 4px; opacity: 0.85; }
|
||||||
|
.file { font-size: 13px; opacity: 0.6; margin-bottom: 28px; word-break: break-all; }
|
||||||
|
a.btn { display: block; padding: 18px; margin-bottom: 14px; border-radius: 10px;
|
||||||
|
font-size: 17px; font-weight: 500; text-align: center; text-decoration: none; }
|
||||||
|
a.view { background: #2d7dff; color: white; }
|
||||||
|
a.dl { background: #2a2a2a; color: #eee; border: 1px solid #3a3a3a; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<h1>HPL Toolbox</h1>
|
||||||
|
<div class="file">${escName}</div>
|
||||||
|
<a class="btn view" href="view">View in browser</a>
|
||||||
|
<a class="btn dl" href="file" download="${escName}">Download .html</a>
|
||||||
|
</body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanIp { address: string; iface: string; }
|
||||||
|
|
||||||
|
function getLanIps(): LanIp[] {
|
||||||
|
const out: LanIp[] = [];
|
||||||
|
const ifaces = os.networkInterfaces();
|
||||||
|
for (const [name, list] of Object.entries(ifaces)) {
|
||||||
|
if (!list) continue;
|
||||||
|
for (const info of list) {
|
||||||
|
if (info.family === 'IPv4' && !info.internal) {
|
||||||
|
out.push({ address: info.address, iface: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPickerDefaultUri(): vscode.Uri | undefined {
|
||||||
|
const folders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!folders || folders.length === 0) return undefined;
|
||||||
|
const root = folders[0].uri.fsPath;
|
||||||
|
const dist = path.join(root, 'dist');
|
||||||
|
if (fs.existsSync(dist) && fs.statSync(dist).isDirectory()) {
|
||||||
|
return vscode.Uri.file(dist);
|
||||||
|
}
|
||||||
|
return vscode.Uri.file(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtml(qrScriptUri: string): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<style>
|
||||||
|
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; }
|
||||||
|
h2 { margin-top: 0; }
|
||||||
|
.row { margin-bottom: 14px; }
|
||||||
|
.file-cell { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.file-name { opacity: 0.85; font-size: 12px; word-break: break-all; }
|
||||||
|
button {
|
||||||
|
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||||
|
border: none; padding: 6px 12px; border-radius: 2px; cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||||
|
button.secondary {
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
}
|
||||||
|
button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||||
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.iface-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
|
||||||
|
.iface-list label { font-size: 12px; cursor: pointer; }
|
||||||
|
.iface-list .meta { opacity: 0.6; margin-left: 6px; }
|
||||||
|
#qr { margin-top: 14px; background: white; padding: 12px; display: inline-block; border-radius: 4px; }
|
||||||
|
#qr svg, #qr img { display: block; }
|
||||||
|
.url {
|
||||||
|
margin-top: 10px; padding: 8px;
|
||||||
|
background: var(--vscode-textBlockQuote-background);
|
||||||
|
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; word-break: break-all;
|
||||||
|
}
|
||||||
|
.url-actions { margin-top: 6px; display: flex; gap: 6px; }
|
||||||
|
#status { margin-top: 12px; min-height: 18px; }
|
||||||
|
.err { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||||
|
.ok { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
|
||||||
|
.hint { font-size: 12px; opacity: 0.7; margin-top: 8px; }
|
||||||
|
</style>
|
||||||
|
<script src="${qrScriptUri}"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Send To Mobile</h2>
|
||||||
|
<p style="opacity:0.8;font-size:12px;margin-top:0;">
|
||||||
|
Pick an HTML file, then start sharing. Scan the QR with a phone on the same WiFi.
|
||||||
|
The phone gets a choice of <strong>View</strong> or <strong>Download</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="file-cell">
|
||||||
|
<button id="pick" class="secondary">Choose...</button>
|
||||||
|
<span class="file-name" id="fileName">(no file)</span>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="filePath" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button id="start">Start sharing</button>
|
||||||
|
<button id="stop" class="secondary" disabled>Stop</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ifaceWrap" class="row" style="display:none;">
|
||||||
|
<div style="font-size:12px;opacity:0.8;">Multiple network interfaces found. Pick the one your phone can reach:</div>
|
||||||
|
<div class="iface-list" id="ifaceList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="shareInfo" style="display:none;">
|
||||||
|
<div id="qr"></div>
|
||||||
|
<div class="url" id="urlText"></div>
|
||||||
|
<div class="url-actions">
|
||||||
|
<button id="copyUrl" class="secondary">Copy URL</button>
|
||||||
|
<button id="openUrl" class="secondary">Open in browser</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">
|
||||||
|
First run on Windows may show a firewall prompt — allow access for "Private networks".
|
||||||
|
Sharing stops automatically when this panel is closed.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
const pickBtn = document.getElementById('pick');
|
||||||
|
const startBtn = document.getElementById('start');
|
||||||
|
const stopBtn = document.getElementById('stop');
|
||||||
|
const fileNameEl = document.getElementById('fileName');
|
||||||
|
const filePathEl = document.getElementById('filePath');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const shareInfo = document.getElementById('shareInfo');
|
||||||
|
const ifaceWrap = document.getElementById('ifaceWrap');
|
||||||
|
const ifaceList = document.getElementById('ifaceList');
|
||||||
|
const qrEl = document.getElementById('qr');
|
||||||
|
const urlTextEl = document.getElementById('urlText');
|
||||||
|
|
||||||
|
let currentUrls = [];
|
||||||
|
let selectedIdx = 0;
|
||||||
|
|
||||||
|
pickBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFile' }));
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
statusEl.innerHTML = '';
|
||||||
|
const path = filePathEl.value;
|
||||||
|
if (!path) {
|
||||||
|
statusEl.innerHTML = '<span class="err">Pick an HTML file first.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startBtn.disabled = true;
|
||||||
|
statusEl.textContent = 'Starting server...';
|
||||||
|
vscode.postMessage({ type: 'startShare', path });
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'stopShare' });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('copyUrl').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'copy', text: currentUrls[selectedIdx]?.url || '' });
|
||||||
|
});
|
||||||
|
document.getElementById('openUrl').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'open', text: currentUrls[selectedIdx]?.url || '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderQr(url) {
|
||||||
|
qrEl.innerHTML = '';
|
||||||
|
urlTextEl.textContent = url;
|
||||||
|
try {
|
||||||
|
const qr = qrcode(0, 'M');
|
||||||
|
qr.addData(url);
|
||||||
|
qr.make();
|
||||||
|
qrEl.innerHTML = qr.createSvgTag({ cellSize: 5, margin: 2, scalable: true });
|
||||||
|
const svg = qrEl.querySelector('svg');
|
||||||
|
if (svg) { svg.setAttribute('width', '240'); svg.setAttribute('height', '240'); }
|
||||||
|
} catch (e) {
|
||||||
|
qrEl.textContent = 'QR render failed: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIfaces(urls) {
|
||||||
|
ifaceList.innerHTML = '';
|
||||||
|
if (urls.length <= 1) {
|
||||||
|
ifaceWrap.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ifaceWrap.style.display = '';
|
||||||
|
urls.forEach((u, i) => {
|
||||||
|
const id = 'iface_' + i;
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.innerHTML = '<input type="radio" name="iface" id="' + id + '"' +
|
||||||
|
(i === selectedIdx ? ' checked' : '') + ' /> ' +
|
||||||
|
u.ip + '<span class="meta">(' + u.iface + ')</span>';
|
||||||
|
label.querySelector('input').addEventListener('change', () => {
|
||||||
|
selectedIdx = i;
|
||||||
|
renderQr(urls[i].url);
|
||||||
|
});
|
||||||
|
ifaceList.appendChild(label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
const m = e.data;
|
||||||
|
if (m.type === 'fileSelected') {
|
||||||
|
filePathEl.value = m.path;
|
||||||
|
fileNameEl.textContent = m.name;
|
||||||
|
} else if (m.type === 'sharing') {
|
||||||
|
currentUrls = m.urls;
|
||||||
|
selectedIdx = 0;
|
||||||
|
statusEl.innerHTML = '<span class="ok">Sharing: ' + m.filename + '</span>';
|
||||||
|
shareInfo.style.display = '';
|
||||||
|
renderIfaces(m.urls);
|
||||||
|
renderQr(m.urls[0].url);
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
} else if (m.type === 'stopped') {
|
||||||
|
statusEl.textContent = 'Stopped.';
|
||||||
|
shareInfo.style.display = 'none';
|
||||||
|
currentUrls = [];
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
} else if (m.type === 'error') {
|
||||||
|
statusEl.innerHTML = '<span class="err">' + (m.message || 'Error').replace(/</g, '<') + '</span>';
|
||||||
|
startBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user