# aardio 范例: 扫码传文件 - v2.6 (增强版)

```aardio
//扫码传文件 - v2.6 (增强版)
import inet.url;
import string.html;
import fonts.fontAwesome;
import win.ui;
import win.clip;
import win.clip.viewer;
import fsys;
import fsys.info;
import fsys.config;
import fsys.dlg.dir;
import fsys.update.simpleMain;
import process;
import web.socket.server;
import qrencode.bitmap;
import win.ui.simpleWindow2;
import win.ui.menu;
/*DSG{{*/
var winform = win.form(text="qrfs - 扫码快传 v2.5";right=807;bottom=465;bgcolor=0xFFFFFF;border="none";max=false)
winform.add(
bk={cls="bk";left=-2;top=-5;right=810;bottom=29;bgcolor=0xF0F0F0;dl=1;dr=1;dt=1;forecolor=0xC0DCC0;linearGradient=0;z=10};
bkplus={cls="bkplus";text="qrfs - 扫码快传  v2.5";left=35;top=3;right=220;bottom=25;align="left";color=0x5A5A5A;dl=1;dt=1;z=11};
btnOpen={cls="plus";text='\uF115';left=441;top=36;right=476;bottom=61;dr=1;dt=1;font=LOGFONT(h=-16;name='FontAwesome');notify=1;z=6};
btnOpenUpload={cls="plus";text="打开上传目录";left=506;top=32;right=647;bottom=61;dl=1;dt=1;font=LOGFONT(h=-15);iconStyle={align="left";font=LOGFONT(h=-15;name='FontAwesome');padding={left=8;top=2}};iconText='\uF045';notify=1;textPadding={left=20};z=12};
btnStart={cls="plus";text="重启服务";left=627;top=72;right=760;bottom=118;align="left";bgcolor=0xE3E4DB;dr=1;dt=1;font=LOGFONT(h=-15);iconStyle={align="left";font=LOGFONT(h=-19;name='FontAwesome');padding={left=16}};iconText='\uF233';notify=1;textPadding={left=45;bottom=2};z=5};
editDocumentRoot={cls="plus";left=131;top=35;right=430;bottom=59;align="left";border={bottom=1;color=0xFF808080};dl=1;dr=1;dt=1;editable="edit";font=LOGFONT(h=-16);textPadding={bottom=2};z=8};
editHost={cls="plus";left=131;top=64;right=430;bottom=88;align="left";border={bottom=1;color=0xFF808080};dr=1;dt=1;editable="edit";font=LOGFONT(h=-16);textPadding={bottom=2};z=19};
editPassword={cls="plus";left=131;top=95;right=430;bottom=119;align="left";border={bottom=1;color=0xFF808080};dr=1;dt=1;editable="edit";font=LOGFONT(h=-16);password=1;textPadding={right=24;bottom=2};z=14};
editPort={cls="plus";left=501;top=67;right=587;bottom=91;align="left";border={bottom=1;color=0xFF808080};dr=1;dt=1;editable=1;font=LOGFONT(h=-16);z=9};
plus={cls="plus";text="密码：";left=71;top=98;right=129;bottom=122;align="right";dl=1;dr=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=13};
plus2={cls="plus";left=38;top=130;right=472;bottom=424;align="left";border={color=0xFF008000;radius=8;width=1};db=1;dl=1;dr=1;dt=1;font=LOGFONT(h=-14);textPadding={left=16};valign="top";z=1};
plus3={cls="plus";text="自定义网址：";left=9;top=68;right=129;bottom=92;align="right";dr=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=20};
qr={cls="plus";left=499;top=132;right=760;bottom=418;db=1;dr=1;dt=1;repeat="scale";z=7};
radioQrClipboard={cls="radiobutton";text="共享剪贴板";left=685;top=435;right=772;bottom=454;bgcolor=0xFFFFFF;db=1;dr=1;z=18};
radioQrRootDir={cls="radiobutton";text="根目录";left=502;top=435;right=589;bottom=454;bgcolor=0xFFFFFF;db=1;dr=1;z=16};
radioQrUploadDir={cls="radiobutton";text="上传目录";left=593;top=435;right=680;bottom=454;bgcolor=0xFFFFFF;db=1;dr=1;z=17};
static={cls="plus";text="端口：";left=439;top=68;right=502;bottom=92;align="right";dr=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=3};
static2={cls="plus";text="网站根目录：";left=15;top=38;right=129;bottom=62;align="right";dl=1;dt=1;font=LOGFONT(h=-15);transparent=1;z=4};
syslink={cls="syslink";text='<a href="https://github.com/aardio/qrfs">开源项目</a>';left=43;top=437;right=176;bottom=457;bgcolor=0xFFFFFF;db=1;dl=1;z=15};
txtMessage={cls="richedit";left=42;top=132;right=469;bottom=418;autohscroll=false;db=1;dl=1;dr=1;dt=1;link=1;multiline=1;vscroll=1;z=2}
)
/*}}*/

if( fsys.update.simpleMain(
	"qrfs - 扫码快传",
	"http://d.aardio.com/qrfs/update/",
	io.appData("/aardio/std/qrfs/app/update"),
	function(version,description,status){})){
	return 0;	
}

config = fsys.config(io.appData("aardio/std/qrfs")); 

import sessionHandler.default;
sessionHandler.default.root = io.appData("aardio/std/qrfs/session")

if( io.exist(config.winform.txtMessage) ){
	winform.txtMessage.text = config.winform.txtMessage;
}
else {
	winform.txtMessage.text = io.getSpecial(0x5/*_CSIDL_MYDOCUMENTS*/)
}

if(config.winform.qrDir=="upload"){
	winform.radioQrUploadDir.checked = true; 
}
elseif(config.winform.qrDir=="clipboard"){
	winform.radioQrClipboard.checked = true;
}
else {
	winform.radioQrRootDir.checked = true;
} 
winform.editHost.text = config.winform.editHost;

var wsrv = web.socket.server();
var srvHttp = wsrv.httpServer;
/*
wsrv.httpServer 是实现单线程异步 HTTP 服务端的 wsock.tcp.asynHttpServer 对象。
浏览器组件发起异步 HTTP 请求支持 wsock.tcp.asynHttpServer。请不要用 inet.http 等
阻塞请求同一线程创建的 asynHttpServer,这会导致 asynHttpServer 没有机会响应请求而导致死锁，
如果确有这样的需求，可创建线程发起请求，或改用基于多线程的 wsock.tcp.simpleHttpServer。
*/
srvHttp.documentRoot = winform.txtMessage.text;
srvHttp.userToken = string.random(18);
winform.editPassword.text = srvHttp.userToken;

var cacheSysIcons = {}
var getSysIconIndex = function(path){ 
	var sfi;
	sfi = fsys.info.get(path, 0x100/*_SHGFI_ICON*/ | 0x4000/*_SHGFI_SYSICONINDEX*/|0x10/*_SHGFI_USEFILEATTRIBUTES*/, 
		..fsys.isDir(path)?0x10/*_FILE_ATTRIBUTE_DIRECTORY*/?0x80/*_FILE_ATTRIBUTE_NORMAL*/);
	 
	if( !(sfi.returnValue ) ) return; 
	
	if(!cacheSysIcons[sfi.iIcon]){
		var bmp = ..gdip.bitmap(sfi.hIcon,1/*_IMAGE_ICON*/);
		if(bmp){
			cacheSysIcons[sfi.iIcon] = bmp.saveToBuffer(".png"); 
			bmp.dispose();
		}
	}
	if(sfi.hIcon)::DestroyIcon(sfi.hIcon);	
	return sfi.iIcon;
}

// 文件大小格式化
var formatSize = function(size){
	if(size===null) return "";
	if(size < 1024) return size + " B";
	if(size < 1024*1024) return string.format("%.1f KB", size/1024);
	if(size < 1024*1024*1024) return string.format("%.1f MB", size/(1024*1024));
	return string.format("%.1f GB", size/(1024*1024*1024));
}

var cacheClientIps = {}
srvHttp.run( 
	function(response,request,session){ 
		
		response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
		response.headers["Pragma"] = "no-cache";
		response.headers["Expires"] = "0";
		 
		var token = request.get["t"] : session["token"];
		if( #srvHttp.userToken && (token != srvHttp.userToken) ){
			winform.txtMessage.printf("客户端：%s 连接被拒绝",request.remoteAddr);	
			response.errorStatus(401)
			return;
		}
		session["token"] = token;
		
		if(!cacheClientIps[request.remoteAddr]){
			winform.txtMessage.printf("客户端：%s 已连接",request.remoteAddr);	
			cacheClientIps[request.remoteAddr] = true;
		}
		
		response.headers["Access-Control-Allow-Origin"] = "*";
		response.headers["Access-Control-Allow-Headers"] = "*"
	
		if(request.path=="/main.aardio" && request.get["icon"]){
			var iconIdx = tonumber(request.get["icon"]);
			if(iconIdx!==null && cacheSysIcons[iconIdx]){
				response.contentType = "image/png";
				response.write(cacheSysIcons[iconIdx])
				return;
			}
			response.errorStatus(404);
			return;
		}
		
		if(request.path=="/upload/main.aardio"){
			if(request.method=="DELETE"){
				var path = request.postData();
				if(path && string.startsWith(path,"/upload/")){
					if(string.find(path,"\.\.")) { response.errorStatus(403); return; }
					
					path = ..io.joinpath(srvHttp.documentRoot,path)
					if(io.exist(path)){
						io.remove(path);
						winform.txtMessage.print("已删除：" + path);
						response.close();
						return;	
					}
				}
				response.errorStatus(404);
				return;
			}
			
			fileData = request.postFileData()
			if(fileData){
				io.createDir(..io.joinpath(srvHttp.documentRoot,"upload"))
				winform.txtMessage.print(..io.joinpath(srvHttp.documentRoot,"upload"))
				
				var fileName = ..io.joinpath(srvHttp.documentRoot,"upload",fileData.filepond.filename) 
				var ok,err = fileData.filepond.save(fileName); 
				if(!ok){ response.error(err); }
				
				winform.txtMessage.text = 'HTTP 服务端已启动: \n'; 
				winform.txtMessage.print( srvHttp.getUrl(,true) + "/?t=" + srvHttp.userToken  );
				winform.txtMessage.print( "" ); 	
				winform.txtMessage.print( "上传成功：" + fileName ); 	
				
				response.contentType = "text/plain";
				response.write("/upload/",fileData.filepond.filename)
				return response.close() 
			} 		
		}
		
		if(!fsys.isDir(request.path) && request.path!="/<clipboard>" ) {
			if( ..io.exist(request.path) && (!_STUDIO_INVOKED || request.path!="/main.aardio") )
				return response.loadcode(request.path)
			else {
				request.path = fsys.getParentDir(request.path)
			}
		} 
		
		response.write(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>qrfs - 扫码快传</title>
<link href="https://lib.baomitu.com/filepond/4.28.2/filepond.min.css" rel="stylesheet">
<script src="https://lib.baomitu.com/filepond/4.28.2/filepond.min.js"></script> 

<style>
:root {
	--bg-color: #f7f9fc;
	--card-bg: #ffffff;
	--text-primary: #2c3e50;
	--text-secondary: #5e6d82;
	--border-color: #ebEEF5;
	--accent-color: #0366d6;
	--accent-hover: #024ea4;
	--danger-color: #ff4757;
	--upload-area-bg: #eef2f7;
	--shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}

@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #121212;
		--card-bg: #1e1e1e;
		--text-primary: #e0e0e0;
		--text-secondary: #a0a0a0;
		--border-color: #333;
		--accent-color: #58a6ff;
		--accent-hover: #79c0ff;
		--danger-color: #ff6b6b;
		--upload-area-bg: #2d2d2d;
		--shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
	}
}

*, *::before, *::after { box-sizing: border-box; }

body {
	margin: 0;
	padding: 0;
	padding-top: max(110px, env(safe-area-inset-top)); 
	padding-bottom: max(20px, env(safe-area-inset-bottom));
	background-color: var(--bg-color);
	color: var(--text-primary);
	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
	-webkit-font-smoothing: antialiased;
}

.upload-header {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 100;
	background: rgba(255, 255, 255, 0.85);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	padding: 12px 15px;
	border-bottom: 1px solid var(--border-color);
	box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
	transition: background 0.3s;
}

@media (prefers-color-scheme: dark) {
	.upload-header { background: rgba(30, 30, 30, 0.85); }
}

.filepond--root { margin-bottom: 0; min-height: 46px !important; }
.filepond--panel-root { background-color: var(--card-bg); border: 2px dashed var(--border-color); }
.filepond--drop-label { color: var(--text-secondary); }

.native-upload {
	display: block; width: 100%; padding: 10px;
	background: var(--card-bg); border: 1px solid var(--border-color);
	border-radius: 8px; color: var(--text-secondary);
}

h2 {
	font-size: 1.1rem;
	margin: 10px 15px 15px 15px;
	color: var(--text-secondary);
	display: flex;
	align-items: center;
	justify-content: space-between;
	font-weight: 600;
}

.back-btn {
	padding: 6px 12px; font-size: 0.85rem;
	background: var(--upload-area-bg); color: var(--accent-color);
	border-radius: 6px; text-decoration: none; font-weight: 500;
}

ul {
	list-style: none; padding: 0 15px; margin: 0;
	display: flex; flex-direction: column; gap: 10px;
}

li {
	background: var(--card-bg); border-radius: 10px;
	box-shadow: var(--shadow);
	display: flex; align-items: center; overflow: hidden;
	transition: transform 0.1s;
}
li:active { transform: scale(0.99); }

li a {
	flex: 1; display: flex; align-items: center;
	padding: 16px; text-decoration: none;
	color: var(--text-primary); font-size: 1rem; overflow: hidden;
}

li img { width: 24px; height: 24px; margin-right: 12px; object-fit: contain; flex-shrink: 0; }
li span.fname { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; margin-right: 10px; }
li span.fsize { font-size: 0.8rem; color: var(--text-secondary); white-space: nowrap; }

.del-btn {
	background-color: var(--danger-color); color: white;
	border: none; padding: 0 16px; height: auto;
	font-weight: 600; font-size: 0.9rem; cursor: pointer;
	align-self: stretch; display: flex; align-items: center;
	margin-left: 1px; /* 分隔线 */
}

.clipboard-container {
	padding: 0 15px; display: flex; flex-direction: column; height: calc(100vh - 100px);
}
.clipboard-content {
	flex: 1; width: 100%; padding: 15px;
	border: 1px solid var(--border-color); border-radius: 12px;
	font-size: 1.1em; background-color: var(--card-bg); color: var(--text-primary);
	resize: none; line-height: 1.6; outline: none; margin-bottom: 15px;
}
.clipboard-content:focus { border-color: var(--accent-color); box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1); }

.btn-group { display: flex; gap: 10px; }
.btn-main {
	flex: 1; padding: 12px; border: none; border-radius: 10px;
	font-size: 1rem; font-weight: 600; cursor: pointer;
	color: white; text-align: center;
}
.btn-copy { background-color: var(--accent-color); }
.btn-paste { background-color: #2ecc71; }

.toast {
	position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
	background: rgba(0,0,0,0.8); color: #fff; padding: 10px 20px;
	border-radius: 20px; font-size: 0.9em; z-index: 9999;
	opacity: 0; transition: opacity 0.3s; pointer-events: none;
}
.toast.show { opacity: 1; }
.empty-tip { text-align: center; padding: 40px; color: var(--text-secondary); }
</style>
</head>
<body>

<div id="toast" class="toast"></div>

<script>
let websocket;
let wsReconnectAttempts = 0;
const wsUrl = "`+wsrv.getUrl("ws",true)+`";

function connectWebSocket() {
	websocket = new WebSocket(wsUrl);
	websocket.onopen = function() { wsReconnectAttempts = 0; };
	websocket.onmessage = function(evt) {
		if (evt.data === "reload") {
			if(window.location.pathname === '/' || window.location.pathname.startsWith('/upload')) {
				window.location.reload();
			}
		} else {
			const clipboardContent = document.getElementById('clipboardContent');
			if (clipboardContent && document.activeElement !== clipboardContent) {
				clipboardContent.value = evt.data;
			}
		}
	};
	websocket.onclose = function() {
		if (wsReconnectAttempts < 5) {
			const delay = Math.min(1000 * Math.pow(1.5, wsReconnectAttempts), 10000);
			setTimeout(connectWebSocket, delay);
			wsReconnectAttempts++;
		}
	};
}
connectWebSocket();

function showToast(msg) {
	const toast = document.getElementById('toast');
	toast.textContent = msg;
	toast.classList.add('show');
	setTimeout(() => toast.classList.remove('show'), 2000);
}

function deleteFile(path, btn) {
	if(event) { event.preventDefault(); event.stopPropagation(); }
	if (!confirm('确定要删除此文件吗？')) return;
	
	fetch('/upload/?t=`+srvHttp.userToken+`', { method: 'DELETE', body: path })
	.then(res => {
		if (res.ok) {
			const li = btn.closest('li');
			li.style.opacity = '0';
			setTimeout(() => {
				li.remove();
				if(document.querySelectorAll('ul li').length === 0) location.reload();
			}, 300);
			showToast('已删除');
		} else { showToast('删除失败'); }
	}).catch(() => showToast('请求失败'));
}
</script>
`)

if(request.path!="/<clipboard>/"){
	response.write(`
<div class="upload-header">
	<input type="file" class="filepond" name="filepond" multiple>
</div>
<script>
if (typeof FilePond !== 'undefined') {
	FilePond.create(document.querySelector('input[type="file"]'), {
		server: '/upload/?t=`+srvHttp.userToken+`',
		labelIdle: '点击或拖拽上传文件',
		labelFileProcessingComplete: '成功',
		credits: false,
		onprocessfiles: function() {
			if (window.location.pathname === '/' || window.location.pathname.startsWith('/upload')) {
				setTimeout(() => window.location.reload(), 500);
			}
		}
	});
} else {
	const input = document.querySelector('input[type="file"]');
	input.className = 'native-upload';
}
</script>`)
}
		
var pathDisplay = string.html.escape(request.path);
var backButton = "";
var isUploadDir = (request.path=="/upload/");

if(request.path=="/<clipboard>/"){
	pathDisplay = "共享剪贴板";
	backButton = `<a class="back-btn" href="/?t=`+srvHttp.userToken+`">返回首页</a>`;
}
elseif(request.path=="/upload/"){
	pathDisplay = "上传目录";
	backButton = `<a class="back-btn" href="/?t=`+srvHttp.userToken+`">返回首页</a>`;
}
elseif(request.path=="/") {
	pathDisplay = "文件列表";
	backButton = ""; 
}
else {
	pathDisplay = string.html.escape(inet.url.decode(request.path));
	var parentPath = fsys.getParentDir(request.path);
	if(!#parentPath) parentPath = "/";
	backButton = `<a class="back-btn" href="`+parentPath+`?t=`+srvHttp.userToken+`">返回上一级</a>`;
}

response.write(`<h2><span>` + pathDisplay + `</span>` + backButton + `</h2>`);

if(request.path=="/<clipboard>/"){
	var clipText = win.clip.read() : "";
	response.write(`
<div class="clipboard-container">
	<textarea id="clipboardContent" class="clipboard-content" placeholder="在此输入文字，将同步到电脑...">`
	+string.html.escape(clipText)+`</textarea>
	<div class="btn-group">
		<button id="copyBtn" class="btn-main btn-copy">复制</button> 
		<button id="pasteBtn" class="btn-main btn-paste">粘贴</button>
	</div>
</div>

<script>
document.body.style.paddingTop = '20px';
const header = document.querySelector('.upload-header');
if(header) header.style.display = 'none';

const clipboardContent = document.getElementById('clipboardContent');
clipboardContent.addEventListener('input', (e) => {
	if (websocket && websocket.readyState === WebSocket.OPEN) {
		websocket.send(e.target.value);
	}
});

document.getElementById('copyBtn').addEventListener('click', async function() {
	try {
		await navigator.clipboard.writeText(clipboardContent.value);
		showToast('已复制');
	} catch (e) {
		clipboardContent.select();
		document.execCommand('copy');
		showToast('已尝试复制');
	}
});

document.getElementById('pasteBtn').addEventListener('click', async function() {
	try {
		const text = await navigator.clipboard.readText();
		clipboardContent.value = text;
		if (websocket && websocket.readyState === WebSocket.OPEN) websocket.send(text);
		showToast('已粘贴');
	} catch (e) {
		const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
		const tip = isMobile ? '请在输入框内长按粘贴 📋' : '请使用快捷键 Ctrl+V 粘贴 ⌨️';
		showToast(tip);
		clipboardContent.focus();
	}
});
</script>
</body></html>`)
	return;
}

response.write(`<ul>`);

if(request.path=="/" && ..io.exist("/upload/")){
	response.write('<li><a href="/upload/?t='+srvHttp.userToken+'"><img src="/?icon='++getSysIconIndex("/upload/")
		++'"><span class="fname">上传目录 (可删除)</span></a></li>\r\n');	
}

if(request.path=="/") {
	response.write('<li><a href="/%3Cclipboard%3E/?t='+srvHttp.userToken+'"><img src="/?icon='++getSysIconIndex("*.txt")
		++'"><span class="fname">共享剪贴板</span></a></li>\r\n');	
}

var file,dir = fsys.list(request.path,,"*.*");
var hasContent = false;

for(i=1;#dir;1){
	if(dir[i]==="upload" && request.path=="/") continue;
	hasContent = true;
	var iconIdx = getSysIconIndex(dir[dir[i]]);
	response.write('<li><a href="'
		,inet.url.append(request.path, inet.url.encode(dir[i]))
		,'?t='+srvHttp.userToken+'"><img src="/?icon='++(iconIdx)++'"><span class="fname">',string.html.escape(dir[i]),'</span></a></li>\r\n');
}

for(i=1;#file;1){
	hasContent = true;
	var iconIdx = getSysIconIndex(file[file[i]]);
	var encodedPath = inet.url.append(request.path, inet.url.encode(file[i]));
	var filePath = string.replace(request.path + file[i], "//", "/");
	// 获取文件大小
	var fileSize = io.getSize( io.joinpath(srvHttp.documentRoot, request.path, file[i]) );
	
	response.write('<li><a href="'
		,encodedPath,'?t='+srvHttp.userToken+'"><img src="/?icon='++(iconIdx)++'"><span class="fname">',string.html.escape(file[i]),'</span>');
	
	// 显示文件大小
	response.write('<span class="fsize">' + formatSize(fileSize) + '</span></a>');
	
	if(isUploadDir){
		var jsPath = string.replace(filePath,"'","\\'"); 
		response.write('<button class="del-btn" onclick="deleteFile(\''
			,jsPath,'\', this)">删除</button>');
	}
	response.write('</li>\r\n');
}

if(!hasContent && request.path!="/"){
	response.write('<li class="empty-tip">📭 目录为空</li>');
}

response.write("</ul></body></html>")
	} 	
);

var serverInfo = function(){
	var ip,port = srvHttp.getLocalIp();
	winform.editPort.text = port;
	winform.editDocumentRoot.text = io.fullpath(srvHttp.documentRoot)

	var home = "/";
	if(winform.radioQrUploadDir.checked){
		home = "/upload/";
	}
	elseif(winform.radioQrClipboard.checked){
		home = "/%3Cclipboard%3E/";
	}
	
	var url = srvHttp.getUrl(home,true);
	
	var customUrl = string.trim(winform.editHost.text);
	if(#customUrl){ 
		if( inet.url.is(customUrl,-1) ){
			url = customUrl;
		}
		elseif(string.startsWith(customUrl,"natapp:",true)){
		 	url = winform.natAppTunnelUrl;
		} 
		else{
			url = "http://"+customUrl;
		} 
	} 
	
	var tUrl = inet.url.split(url);
	if(!tUrl){
		return winform.msgboxErr("指定了错误的网址");
	}
	
	url = tostring( tUrl ); //移除 80 端口号
	
	if(#srvHttp.userToken){
		url = inet.url.appendExtraInfo(url,{
			t = srvHttp.userToken;
		})
	} 
	
	winform.txtMessage.text = 'HTTP 服务端已启动: \n'; 
	winform.txtMessage.print(  url );
	
	var qrBmp = qrencode.bitmap( url );
    winform.qr.setBackground(qrBmp.copyBitmap(winform.qr.width)); 
		
	winform.txtMessage.print( 
		"
手机无线连接电脑局域网。
扫码打开网页，可上传下载文件、共享电脑剪贴板。
拖动文件或目录到窗口网页自动刷新。

aardio 实现的开源单线程异步 HTTP 服务端，体积仅数十 KB。
支持高速上传下载、断点续传、304 缓存、分块传输、Keep Alive。
支持 WebSocket / HTTP 双服务端（共享端口）。
可运行 aardio 开发的网站。
"
	); 	
}
serverInfo();

winform.btnStart.oncommand = function(id,event){
	winform.txtMessage.text = "";
	winform.btnStart.disabledText = {'\uF254';'\uF251';'\uF252';'\uF253';'\uF250'}
	thread.delay(500);
 
	var customUrl = string.trim(winform.editHost.text);
	if(#customUrl){ 
		if(string.startsWith(customUrl,"natapp:",true)){
			if(..natapp){
				..natapp.terminate()
			}
			
		 	var token = string.right(customUrl,-8); 
		 	
		 	import process.natapp;
		 	..natapp = process.natapp(token,winform.edi,"INFO")
		 	if(natapp[["tunnelUrl"]]){
		 		winform.natAppTunnelUrl = natapp.tunnelUrl;
		 		winform.editPort.text = 80;
		 	}
		 	else{
		 		winform.btnStart.disabledText = null;
		 		return winform.msgboxErr("错误的 NATAPP 令牌");
		 	}
		}
		
		config.winform.editHost = customUrl; 
	}
	else{
		config.winform.editHost = null;
	}
	
	config.saveAll();
	
	var port = tonumber(winform.editPort.text);
	
	srvHttp.documentRoot = fsys.isDir(winform.editDocumentRoot.text) ? winform.editDocumentRoot.text;
	srvHttp.userToken = winform.editPassword.text;
	srvHttp.start("0.0.0.0",port);
	serverInfo();
	
	winform.btnStart.disabledText = null;
}

winform.txtMessage.enablePopMenu();
winform.txtMessage.onHyperlink = function(message,href){
	if( message = 0x202/*_WM_LBUTTONUP*/ ) {
		process.openUrl(href);
	}
}

winform.onDropFiles = function(files){
	var path = files[1]
	if(!fsys.isDir(path)){
		path = fsys.getParentDir(path)
	}
	
	winform.editDocumentRoot.text = path;
	srvHttp.documentRoot = path;
	config.winform.txtMessage = path;
	config.winform.save();
	
	wsrv.publish("reload");
}

winform.btnOpen.oncommand = function(id,event){
	var dir = fsys.dlg.dir(winform.editDocumentRoot.text,winform)
	if(dir){
		winform.editDocumentRoot.text = dir;
		srvHttp.documentRoot = dir;
		
		config.winform.txtMessage = dir;
		config.winform.save();
		wsrv.publish("reload");
	}
}

winform.btnOpenUpload.oncommand = function(id,event){
	var path = io.joinpath(winform.editDocumentRoot.text,"upload")
	if(io.createDir(path)){
		process.explore(path)
	}
}

var clipViewer = win.clip.viewer(winform);
clipViewer.onDrawClipboard=function(){
	var str = win.clip.read();	
	if(str!=wsrv.lastReceivedClipboardData ){
		wsrv.publish(#str?str:"") 
		wsrv.lastReceivedClipboardData = null;   
	}
}

wsrv.onMessage = function(hSocket,msg){
	wsrv.lastReceivedClipboardData = msg.data;
    win.clip.write(msg.data);
}

var updateHomeDir = function(){
	var home = "/";
	if(winform.radioQrUploadDir.checked){
		home = "/upload/";
	}
	elseif(winform.radioQrClipboard.checked){
		home = "/%3Cclipboard%3E/";
	}
	
	var url = srvHttp.getUrl(home,true);
	if(#srvHttp.userToken){
		url = url ++ "?t=" + srvHttp.userToken;
	}  
	
	var qrBmp = qrencode.bitmap( url );
    winform.qr.setBackground(qrBmp.copyBitmap(winform.qr.width)); 	
}

winform.radioQrRootDir.oncommand = function(id,event){
	config.winform.qrDir = "root";
	updateHomeDir();
}

winform.radioQrUploadDir.oncommand = function(id,event){
	config.winform.qrDir = "upload";
	updateHomeDir();	
}

winform.radioQrClipboard.oncommand = function(id,event){
	config.winform.qrDir = "clipboard";	
	updateHomeDir();
}

win.ui.simpleWindow2(winform);
winform.show();  

var revealIcon = winform.editPassword.addCtrl(
	cls="plus";
	marginRight=0;marginBottom=2;
	width=24; 
	iconText = '\uF023';
	iconStyle={
		align="right";font=LOGFONT(h=-15;name='FontAwesome');padding={top=3}
	}
)

revealIcon.skin({
	color = {
		default = 0xC0000000;
		hover = 0xFFFF0000;
		active = 0xFF00FF00;
	};
	checked = {
		iconText = '\uF06E';
	}
})

revealIcon.onMouseClick = function(){
    winform.editPassword.editBox.passwordChar = !owner.checked ? "*" : null;
}

winform.btnStart.skin( {
	background={
		default=0x668FB2B0;
		hover=0xFF928BB3;
		disabled=0xFFCCCCCC; 
	}
})

winform.btnOpen.skin( {
	background={
		default=0;
		hover=0xFF928BB3;
		disabled=0xFFCCCCCC; 
	}
})

winform.btnOpenUpload.skin( {
	background={
		default=0;
		hover=0xFF928BB3;
		disabled=0xFFCCCCCC; 
	}
})

win.loopMessage();
```