# aardio 范例: 扫码传文件 - v2.8 (导航优化版)



```aardio
//扫码传文件 - v2.8 (导航优化版)
import fsys;
import fsys.info;
import fsys.config;
import fsys.dlg.dir;
import fsys.update.simpleMain;
import process;
import web.socket.server;
import win.ui.simpleWindow2;
import qrencode.bitmap;
import win.ui.menu;
import inet.url;
import string.html;
import fonts.fontAwesome;
import win.ui;
import win.clip;
import win.clip.viewer;
/*DSG{{*/
var winform = win.form(text="qrfs - 扫码快传 v2.8";right=798;bottom=491;bgcolor=0xFAFAFA;border="none";max=false)
winform.add(
bk={cls="bk";left=0;top=-5;right=799;bottom=29;bgcolor=0xF0F0F0;dl=1;dr=1;dt=1;forecolor=0xEAEAEA;linearGradient=0;z=10};
bkplus={cls="bkplus";text="qrfs - 扫码快传  v2.8 - 共享文件、电脑剪贴板、手机输入法（ AI 自动优化 ）";left=35;top=3;right=509;bottom=25;align="left";color=0x5A5A5A;dl=1;dt=1;z=11};
btnOpen={cls="plus";text='\uF115';left=430;top=44;right=456;bottom=68;align="right";border={bottom=1;color=0xFF808080};dr=1;dt=1;font=LOGFONT(h=-16;name='FontAwesome');notify=1;z=1};
btnOpenUpload={cls="plus";text="上传目录";left=473;top=40;right=614;bottom=69;align="left";dl=1;dt=1;font=LOGFONT(h=-14);iconStyle={align="left";font=LOGFONT(h=-15;name='FontAwesome');padding={left=8;top=2}};iconText='\uF07C';notify=1;textPadding={left=27;bottom=1};z=12};
btnSetting={cls="plus";left=737;top=455;right=768;bottom=484;bgcolor=0xFAFAFA;db=1;dl=1;font=LOGFONT(h=-16;name='FontAwesome');iconStyle={align="left";font=LOGFONT(h=-16;name='FontAwesome');padding={left=7;top=3}};iconText='\uF013';notify=1;z=23};
btnStart={cls="plus";text="重启服务";left=635;top=69;right=768;bottom=106;align="left";bgcolor=0xD9A100;border={radius=8};color=0xFFFFFF;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=6};
chkAiOpt={cls="checkbox";text="AI 优化";left=642;top=461;right=712;bottom=483;bgcolor=0xFAFAFA;z=22};
chkPublicAddress={cls="checkbox";text="公网 IP";left=486;top=109;right=556;bottom=131;bgcolor=0xFFFFFF;z=24};
editDocumentRoot={cls="plus";left=131;top=46;right=430;bottom=68;align="left";border={bottom=1;color=0xFFC0C0C0};clip=1;dl=1;dr=1;dt=1;editable="edit";textPadding={bottom=1};z=8};
editHost={cls="plus";left=131;top=75;right=456;bottom=97;align="left";border={bottom=1;color=0xFFC0C0C0};dr=1;dt=1;editable="edit";textPadding={bottom=1};z=19};
editPassword={cls="plus";left=131;top=107;right=456;bottom=129;align="left";border={bottom=1;color=0xFFC0C0C0};dr=1;dt=1;editable="edit";password=1;textPadding={right=24;bottom=1};z=14};
editPort={cls="plus";left=529;top=78;right=615;bottom=100;align="left";border={bottom=1;color=0xFFC0C0C0};dr=1;dt=1;editable=1;notify=1;textPadding={bottom=1};z=9};
plus={cls="plus";text="密码：";left=71;top=108;right=129;bottom=132;align="right";dl=1;dr=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=13};
plus2={cls="plus";left=38;top=148;right=455;bottom=442;align="left";border={color=0xFFC0C0C0;radius=8;width=1};db=1;dl=1;dr=1;dt=1;font=LOGFONT(h=-14);textPadding={left=16};valign="top";z=2};
plus3={cls="plus";text="自定义网址：";left=9;top=77;right=129;bottom=101;align="right";dr=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=20};
qr={cls="plus";left=483;top=148;right=768;bottom=433;border={radius=8};db=1;dr=1;dt=1;foreRepeat="scale";repeat="scale";z=7};
radioQrClipboard={cls="radiobutton";text="共享剪贴板";left=442;top=461;right=529;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=18};
radioQrInputMethod={cls="radiobutton";text="共享输入法";left=541;top=461;right=637;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=21};
radioQrRootDir={cls="radiobutton";text="根目录";left=259;top=461;right=346;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=16};
radioQrUploadDir={cls="radiobutton";text="上传文件";left=350;top=461;right=437;bottom=483;bgcolor=0xFAFAFA;db=1;dr=1;z=17};
static={cls="plus";text="端口：";left=467;top=77;right=530;bottom=101;align="right";dr=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=4};
static2={cls="plus";text="网站根目录：";left=15;top=45;right=129;bottom=69;align="right";dl=1;dt=1;font=LOGFONT(h=-14);transparent=1;z=5};
syslink={cls="syslink";text='<a href="https://github.com/aardio/qrfs">开源项目</a>';left=43;top=463;right=176;bottom=483;bgcolor=0xFAFAFA;db=1;dl=1;z=15};
txtMessage={cls="richedit";left=43;top=151;right=451;bottom=436;autohscroll=false;bgcolor=0xFAFAFA;db=1;dl=1;dr=1;dt=1;link=1;multiline=1;vscroll=1;z=3}
)
/*}}*/

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")); 

table.mix(config.aiOpt,{
	key =   '\0\1\96';
	url = "https://ai.aardio.com/api/v1/";
	model = "prompt";	
	systemPrompt = /*你是语音输入法优化助手。

# 任务

用户输入的文本是使用语音输入法输入的的语音识别结果。
你的任务是在评估与尊重用户原意的基础上对识别结果进行评估与修复，纠正语音识别错误。
在保持与尊重用户原意的基础上修正错误的文法与不通顺之处，并合理使用标点符号。
然后在在保持与尊重用户原意的基础上，你应当评估输入并根据相关的文化与背景知识对识别结果的格式进行整理与优化，使文本更为通顺自然。
输出格式为纯文本，不使用 Markdown 标记（除非识别结果本身包含 Markdown 标记）。
*/
});

import sessionHandler.default;
sessionHandler.default.root = io.appData("aardio/std/qrfs/session")

if( #config.winform.documentRoot && io.exist(config.winform.documentRoot) ){
	winform.editDocumentRoot.text = config.winform.documentRoot;
}
else {
	winform.editDocumentRoot.text = io.getSpecial(0x5/*_CSIDL_MYDOCUMENTS*/)
}

if(config.winform.qrDir=="upload"){
	winform.radioQrUploadDir.checked = true; 
}
elseif(config.winform.qrDir=="clipboard"){
	winform.radioQrClipboard.checked = true;
}
elseif(config.winform.qrDir=="input"){
	winform.radioQrInputMethod.checked = true;
}
else {
	winform.radioQrRootDir.checked = true;
} 
winform.editHost.text = config.winform.editHost;

var wsrv = web.socket.server();
var srvHttp = wsrv.httpServer;
srvHttp.documentRoot = winform.editDocumentRoot.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 = {}

// 生成通用 HTML 头部
var getHtmlHead = function(activeTab){
	return `
<!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);
	--nav-height: 50px;
}

@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: calc(var(--nav-height) + max(10px, 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;
}

/* 顶部导航栏 */
.nav-header {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 100;
	background: rgba(255, 255, 255, 0.95);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border-bottom: 1px solid var(--border-color);
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
	padding-top: max(0px, env(safe-area-inset-top));
}

@media (prefers-color-scheme: dark) {
	.nav-header { background: rgba(30, 30, 30, 0.95); }
}

.nav-tabs {
	display: flex;
	height: var(--nav-height);
	padding: 0;
	margin: 0;
	list-style: none;
}

.nav-tabs li {
	flex: 1;
	display: flex;
	align-items: center;
	justify-content: center;
}

.nav-tabs a {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 100%;
	text-decoration: none;
	color: var(--text-secondary);
	font-size: 0.85rem;
	font-weight: 500;
	transition: all 0.2s;
	border-bottom: 2px solid transparent;
	gap: 2px;
}

.nav-tabs a .nav-icon {
	font-size: 1.2rem;
}

.nav-tabs a:hover {
	color: var(--accent-color);
	background: rgba(3, 102, 214, 0.05);
}

.nav-tabs a.active {
	color: var(--accent-color);
	border-bottom-color: var(--accent-color);
	background: rgba(3, 102, 214, 0.08);
}

/* 上传区域样式 */
.upload-section {
	padding: 15px;
	margin-bottom: 10px;
}

.filepond--root { margin-bottom: 0; min-height: 120px !important; }
.filepond--panel-root { background-color: var(--card-bg); border: 2px dashed var(--border-color); border-radius: 12px; }
.filepond--drop-label { color: var(--text-secondary); font-size: 1rem; }

.native-upload {
	display: block; width: 100%; padding: 15px;
	background: var(--card-bg); border: 2px dashed var(--border-color);
	border-radius: 12px; color: var(--text-secondary); text-align: center;
}

.page-title {
	font-size: 1.1rem;
	margin: 15px 15px 15px 15px;
	color: var(--text-secondary);
	font-weight: 600;
}

ul.file-list {
	list-style: none; padding: 0 15px; margin: 0;
	display: flex; flex-direction: column; gap: 10px;
}

ul.file-list li {
	background: var(--card-bg); border-radius: 10px;
	box-shadow: var(--shadow);
	display: flex; align-items: center; overflow: hidden;
	transition: transform 0.1s;
}
ul.file-list li:active { transform: scale(0.99); }

ul.file-list li a {
	flex: 1; display: flex; align-items: center;
	padding: 16px; text-decoration: none;
	color: var(--text-primary); font-size: 1rem; overflow: hidden;
}

ul.file-list li img { width: 24px; height: 24px; margin-right: 12px; object-fit: contain; flex-shrink: 0; }
ul.file-list li span.fname { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; margin-right: 10px; }
ul.file-list 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;
}

/* 功能容器 */
.func-container {
	padding: 0 15px; 
	display: flex; 
	flex-direction: column; 
	height: calc(100vh - var(--nav-height) - max(10px, env(safe-area-inset-top)) - 50px - max(20px, env(safe-area-inset-bottom)));
	height: calc(100dvh - var(--nav-height) - max(10px, env(safe-area-inset-top)) - 50px - max(20px, env(safe-area-inset-bottom)));
}

.func-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;
	overflow: auto;
}
.func-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; flex-wrap: wrap; }
.btn-main {
	flex: 1; min-width: 120px; padding: 14px; 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-send { background-color: #2ecc71; }
.btn-clear { background-color: #95a5a6; }

/* 提示卡片 */
.tip-card {
	background: var(--upload-area-bg);
	border-radius: 8px;
	padding: 10px 12px;
	margin-bottom: 10px;
	color: var(--text-secondary);
	font-size: 0.85rem;
	line-height: 1.5;
}

.tip-card .tip-icon {
	font-size: 1rem;
	margin-right: 6px;
	vertical-align: middle;
}

/* 链接卡片 */
.link-card {
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--card-bg);
	border: 1px solid var(--border-color);
	border-radius: 12px;
	padding: 20px;
	margin: 15px;
	text-decoration: none;
	color: var(--accent-color);
	font-weight: 600;
	box-shadow: var(--shadow);
	transition: all 0.2s;
}

.link-card:hover {
	background: var(--accent-color);
	color: white;
	transform: translateY(-2px);
}

.link-card .link-icon {
	margin-right: 10px;
	font-size: 1.2rem;
}

.toast {
	position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
	background: rgba(0,0,0,0.8); color: #fff; padding: 12px 24px;
	border-radius: 25px; 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); }

.success-msg {
	text-align: center;
	padding: 30px;
	color: #2ecc71;
	font-size: 1.1rem;
}

.success-msg .check-icon {
	font-size: 3rem;
	margin-bottom: 15px;
	display: block;
}
</style>
</head>
<body>

<div id="toast" class="toast"></div>

<nav class="nav-header">
	<ul class="nav-tabs">
		<li><a href="/?t=`+srvHttp.userToken+`" class="`+(activeTab=="files"?"active":"")+`"><span class="nav-icon">📁</span><span>文件</span></a></li>
		<li><a href="/%3Cupload%3E/?t=`+srvHttp.userToken+`" class="`+(activeTab=="upload"?"active":"")+`"><span class="nav-icon">📤</span><span>上传</span></a></li>
		<li><a href="/%3Cclipboard%3E/?t=`+srvHttp.userToken+`" class="`+(activeTab=="clipboard"?"active":"")+`"><span class="nav-icon">📋</span><span>剪贴板</span></a></li>
		<li><a href="/%3Cinput%3E/?t=`+srvHttp.userToken+`" class="`+(activeTab=="input"?"active":"")+`"><span class="nav-icon">⌨️</span><span>输入法</span></a></li>
	</ul>
</nav>

<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") {
			`+(activeTab=="files"?"window.location.reload();":"")+`
		} else {
			const clipboardContent = document.getElementById('clipboardContent');
			if (clipboardContent /*&& document.activeElement !== clipboardContent*/) {
				if(evt.data!=clipboardContent.value ){
					clipboardContent.value = evt.data;
					clipboardContent.select();
				} 
			}
		}
	};
	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('/%3Cupload%3E/?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.file-list li').length === 0) {
					document.querySelector('ul.file-list').innerHTML = '<li class="empty-tip">📭 目录为空</li>';
				}
			}, 300);
			showToast('已删除');
		} else { showToast('删除失败'); }
	}).catch(() => showToast('请求失败'));
}
</script>
`;
}

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"){ //注意请求目录会自动附加默认文档路径 /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"))
				
				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() 
			} 		
		}
		
		// 确定当前页面类型
		var pageType = "files";
		if(request.path=="/<clipboard>/main.aardio") pageType = "clipboard";
		elseif(request.path=="/<input>/main.aardio") pageType = "input";
		elseif(request.path=="/<upload>/main.aardio") pageType = "upload";
		elseif(string.startsWith(request.path,"/upload/")) pageType = "files";
		
		// 文件浏览处理
		if(pageType=="files"){
			if(!fsys.isDir(request.path)) {
				if( ..io.exist(request.path) && (!_STUDIO_INVOKED || request.path!="/main.aardio") )
					return response.loadcode(request.path)
				else {
					request.path = fsys.getParentDir(request.path)
				}
			}
		}
		
		// 输出 HTML 头部
		response.write(getHtmlHead(pageType));
		
		// 上传页面
		if(pageType=="upload"){
			response.write(`
<h3 class="page-title">📤 上传文件</h3>

<div class="upload-section">
	<input type="file" class="filepond" name="filepond" multiple>
</div>

<script>
if (typeof FilePond !== 'undefined') {
	FilePond.create(document.querySelector('input[type="file"]'), {
		//这里不能写 %3Eupload%3E，因为 FilePond 会再次进行 URL 编码
		server: '/<upload>/?t=`+srvHttp.userToken+`',
		labelIdle: '点击或拖拽文件到此处上传',
		labelFileProcessingComplete: '上传成功',
		credits: false,
		onprocessfiles: function() {
			showToast('上传完成');
		}
	});
} else {
	const input = document.querySelector('input[type="file"]');
	input.className = 'native-upload';
	input.insertAdjacentHTML('afterend', '<p style="text-align:center;color:var(--text-secondary)">选择文件后自动上传</p>');
}
</script>

<a class="link-card" href="/upload/?t=`+srvHttp.userToken+`">
	<span class="link-icon">📂</span>
	<span>查看已上传的文件</span>
</a>

</body></html>`);
			return;
		}
		
		// 剪贴板页面
		if(pageType=="clipboard"){
			var clipText = win.clip.read() : "";
			response.write(`
<h3 class="page-title">📋 共享剪贴板</h3>

<div class="func-container">
	<textarea id="clipboardContent" class="func-content" placeholder="修改这里的文本，将同步修改电脑剪贴板...">`
	+string.html.escape(clipText)+`</textarea>
	
	<div class="tip-card">
		<span class="tip-icon">💡</span> 
		<span id="pasteTip"></span>
	</div>
	
	<div class="btn-group">
		<button id="copyBtn" class="btn-main btn-copy">📄 复制到手机</button>
	</div>
</div>

<script>
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isAndroid = /Android/i.test(navigator.userAgent);

const copyBtn = document.getElementById('copyBtn');

let pasteTip = '使用 Ctrl+V 粘贴内容到输入框';
if (isIOS) {
	pasteTip = '长按输入框 → 选择"粘贴"';
} else if (isAndroid) {
	pasteTip = '长按输入框 → 点击"粘贴"';
} else if (isMobile) {
	pasteTip = '长按输入框进行粘贴';
}
else{
	copyBtn.textContent = "📄 复制到本机"
}

document.getElementById('pasteTip').textContent = pasteTip;

const clipboardContent = document.getElementById('clipboardContent');
clipboardContent.addEventListener('input', (e) => {
	if (websocket && websocket.readyState === WebSocket.OPEN) {
		var data = JSON.stringify({
			event:"clipboard",
			content:e.target.value
		});
		websocket.send(data);
	}
});

copyBtn.addEventListener('click', async function() {
	try {
		await navigator.clipboard.writeText(clipboardContent.value);
		showToast('✓ 已复制到剪贴板');
	} catch (e) {
		clipboardContent.select();
		document.execCommand('copy');
		showToast('✓ 已尝试复制');
	}
});

clipboardContent.select();
</script>
</body></html>`);
			return;
		}
		
		// 输入法页面
		if(pageType=="input"){
			response.write(`
<h3 class="page-title">⌨️ 共享输入法</h3>

<div class="func-container">
	<textarea id="inputContent" class="func-content" placeholder="在此输入文字，点击发送后将输入到电脑当前焦点位置..."></textarea>
	  
	<div class="btn-group">
		<button id="clearBtn" class="btn-main btn-clear">🗑️ 清空</button>
		<button id="sendBtn" class="btn-main btn-send">📨 发送到电脑</button>
	</div>
</div>

<script>
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (!isMobile) {
	document.getElementById('sendBtn').textContent = '📨 发送到服务端';
}

const inputContent = document.getElementById('inputContent');
 
document.getElementById('sendBtn').addEventListener('click', async function() {
	if(!inputContent.value.trim()) {
		showToast('请先输入内容');
		return;
	}
	try {
		var data = JSON.stringify({
			event:"input",
			content:inputContent.value 
		});
		websocket.send(data);
		showToast('✓ 已发送到电脑');
		inputContent.value = "";
		inputContent.focus();
	} catch (e) { 
		showToast('发送失败: ' + e.message);
	}
});

document.getElementById('clearBtn').addEventListener('click', function() {
	inputContent.value = "";
	inputContent.focus();
	showToast('已清空');
});

inputContent.focus();

</script>
</body></html>`);
			return;
		}
		
		// 文件列表页面
		var pathDisplay = string.html.escape(request.path);
		var isUploadDir = string.startsWith(request.path,"/upload");
		
		if(request.path=="/") {
			pathDisplay = "📁 全部文件";
		}
		elseif(request.path=="/upload/" || request.path=="/upload") {
			pathDisplay = "📤 已上传的文件";
		}
		
		response.write(`<h3 class="page-title">` + pathDisplay + `</h3>`);
		
		// 显示返回上级按钮（非根目录时）
		if(request.path!="/") {
			var parentPath = fsys.getParentDir(request.path);
			if(!#parentPath) parentPath = "/";
			response.write(`<a class="link-card" href="`+parentPath+`?t=`+srvHttp.userToken+`" style="margin-top:0;">
				<span class="link-icon">⬆️</span>
				<span>返回上一级</span>
			</a>`);
		}
		
		response.write(`<ul class="file-list">`);
		
		// 根目录显示上传目录入口
		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(!..fsys.isDir(request.path)){
			return response.errorStatus(404); 
		}
		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>');
		}
		elseif(!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 = "/%3Cupload%3E/";
	}
	elseif(winform.radioQrClipboard.checked){
		home = "/%3Cclipboard%3E/";
	}
	elseif(winform.radioQrInputMethod.checked){
		home = "/%3Cinput%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;
		}  
	} 
	elseif(winform.chkPublicAddress.checked){
		var tUrl = inet.url.split(url);
		
		winform.chkPublicAddress.disabled = true;
		winform.btnStart.disabledText = ['\uF254','\uF251','\uF252','\uF253','\uF250',text='获取 IP']
		var resp = thread.invokeAndWait(
			function(){
				import wsock.udp.stunClient;
        		var sutn = wsock.udp.stunClient();
        		return sutn.getPublicAddress();
			} 
		)
		
		winform.chkPublicAddress.disabled = false;
		winform.btnStart.disabledText = null;
		 
        if(resp){
            tUrl.host = resp.ip;
            url = tostring(tUrl);
        }
	}
	
	var tUrl = inet.url.split(url);
	if(!tUrl){
		return winform.msgboxErr("指定了错误的网址");
	}
	
	url = tostring( tUrl );
	
	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.setForeground(qrBmp.copyBitmap(winform.qr.width)); 
		
	if(winform.chkPublicAddress.checked){
		winform.txtMessage.print( 
			"
手机无线连接电脑局域网。
扫码打开网页即可互传文件，共享电脑剪贴板、手机输入法。
拖动文件或目录到窗口网页自动刷新。

基于纯 aardio 代码实现的开源单线程异步 HTTP / WebSocket 双服务端，体积仅数十 KB。"
		); 			
	}
	else{
		winform.txtMessage.print( 
			"
手机无线连接电脑局域网。
扫码打开网页即可互传文件，共享电脑剪贴板、手机输入法。
拖动文件或目录到窗口网页自动刷新。

本工具只走内网，不走公网！
端口与密钥动态改变，双重安全保障。

基于纯 aardio 代码实现的开源单线程异步 HTTP / WebSocket 双服务端，体积仅数十 KB。"
		); 		
	} 
}

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.documentRoot = 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.documentRoot = 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 ){
		if(#str) wsrv.publish(str) 
		//wsrv.lastReceivedClipboardData = null;   
	//}
}

import key;
wsrv.onMessage = function(hSocket,msg){
	var data = JSON.tryParse(msg.data)
	if(!data) return;
	
	if(data.event=="clipboard"){
		wsrv.lastReceivedClipboardData = data.content; 
		win.clip.write(data.content);	
	}
	elseif(data.event=="input"){
		if(!winform.chkAiOpt.checked){
			key.sendString(data.content)
		}
		else{
			thread.invoke( 
				function(winform,aiOpt,text){
					import key;
					import web.rest.aiChat;
					var aiClient = web.rest.aiChat(aiOpt)  
					
					var msg = web.rest.aiChat.messages();
					msg.system(aiOpt.systemPrompt) 
					msg.prompt( "请修复语音识别结果: " + text );
			 		 
			 		import winex.loading;
					var loading = winex.loading("正在优化",hFocus)
				
					var resp,err = aiClient.messages(msg,
						function(deltaText,reasoning){
					 		if(reasoning){ 
								return loading.thinking(reasoning);
							}
							
							if(loading.isCanceled() && deltaText){
								return false;
							}
							
							key.sendString(deltaText) 
						}
					);
					 
				},winform,config.aiOpt,data.content
			)
		}
	}
}

var updateHomeDir = function(){
	var home = "/";
	if(winform.radioQrUploadDir.checked){
		home = "/%3Cupload%3E/";
	}
	elseif(winform.radioQrClipboard.checked){
		home = "/%3Cclipboard%3E/";
	}
	elseif(winform.radioQrInputMethod.checked){
		home = "/%3Cinput%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();
}

winform.radioQrInputMethod.oncommand = function(id,event){
	config.winform.qrDir = "input";
	updateHomeDir();
}

winform.show();  
win.ui.simpleWindow2(winform);

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.btnSetting.oncommand = function(id,event){
	..table.assign(config.aiOpt,{
		title = "扫码快传 - 设置 AI 优化助手";
	});
		
	import web.rest.aiChat.settingForm;
	var frmSetting = web.rest.aiChat.settingForm(this,config.aiOpt);
	
	if( frmSetting.doModal(this) ){
		..table.assign(config.aiOpt,config.itemData[config.selItem] );
	}
}

winform.chkAiOpt.checked = config.winform.checkAiOpt;
winform.onDestroy = function(){
    config.winform.checkAiOpt = winform.chkAiOpt.checked;
}

winform.editHost.editBox.setCueBannerText("自定义访问域名")

winform.chkPublicAddress.oncommand = function(id,event){

	if(winform.chkPublicAddress.checked){
		import win.version;
		
		if(win.version.isServer){
			import win.ts; 
			if( win.ts.session.isRemote() ){
    			import wsock.tcp.server;
    			if( wsock.tcp.server.isFreePort(80)) winform.editPort.text = 80;
			} 
		}	
	}

	winform.btnStart.oncommand();
}

import gdip.imageAttributes;
var imgAttr = gdip.imageAttributes()

//替换前景图像（二维码）颜色
imgAttr.setRemapTable({ 
	0xFF000000; 0xFF00A1D9; // 仅将二维码中的黑色（0xFF000000）替换为绿色（0xFF008000），不替换白色
	0xFFFFFFFF; 0xFFFAFAFA;
}); 
winform.qr.imageAttributes = imgAttr

winform.btnStart.skin( {
	background={
		default=0xFF00A1D9;
		hover=0xFFFF5C65;
		active=0xFF1295C7;
		disabled=0xFFCCCCCC; 
	}
})

winform.btnOpen.skin( {
	background={
		default=0;
		hover=0xFF928BB3;
		disabled=0xFFCCCCCC; 
	}
})

winform.btnOpenUpload.skin( {
	background={
		default=0;
		hover=0xFF928BB3;
		disabled=0xFFCCCCCC; 
	}
})

winform.btnSetting.skin({
	color={
		active=0xFF00FF00;
		default=0xFF3C3C3C;
		disabled=0xFF999999;
		hover=0xFFFF0000		
	}
})

serverInfo();
win.loopMessage();
```

