# aardio 范例: BT 下载 / WebView2 纯 HTML 界面

```aardio
//BT 下载 / WebView2 纯 HTML 界面
//win.form 下载界面: https://www.aardio.com/zh-cn/doc/example/Network/Transfer/bt.win.html
//如果 BT 下载没速度，先找个热门种子下载就可以了
import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="aria2 下载 —— 纯 HTML 界面";right=921;bottom=537)
winform.add()
/*}}*/

import web.view;
var wb = web.view(winform);

//启动 aria2 下载服务进程
import process.aria2;
var aria2 = process.aria2();  
aria2.startServer( maxConcurrentDownloads = 10 );

import com.interface.ITaskbarList3;
var taskbar = com.interface.ITaskbarList3.Create();

//导出到网页的接口
wb.external = {
	//添加下载任务
	addTask = function(url){ 
		if(!#url) return {success=false; error="请输入下载地址或种子文件路径"};
		
		aria2.taskAdd(url,function(gid,err){
			if(err){
				wb.invoke("onTaskAdded",null,url,err[["message"]]); 
			} 
			else {
				wb.invoke("onTaskAdded",gid,url,null); 
			} 	 
		});
		
		return {success=true};
	};
	
	//获取任务名称
	getTaskName = function(gid){
		return aria2.taskName(gid);
	};
	
	//获取任务路径
	getTaskPath = function(gid){
		return aria2.taskFilePath(gid);
	};
	
	//获取任务URL
	getTaskUrl = function(gid){
		return aria2.taskUrl(gid);
	};
	
	//移除任务
	removeTask = function(gid){
		aria2.taskRemove(gid);
		return true;
	};
	
	//暂停任务
	pauseTask = function(gid){
		aria2.taskPause(gid);
		return true;
	};
	
	//恢复任务
	unpauseTask = function(gid){
		aria2.taskUnpause(gid);
		return true;
	};
	
	//浏览文件
	exploreFile = function(path){
		if(#path) raw.explore(path,"/select");
		return true;
	};
	
	//复制链接
	copyLink = function(url){
		import win.clip;
		win.clip.write(url);
		return true;
	};
	
	//格式化文件大小
	formatSize = function(size){
		import math.size64;
		return math.size64(size).format();
	};
	
	//获取任务状态
	getTaskStatus = function(gid){
		return aria2.tellStatus(gid,"status");
	};
};

//监听 aria2 事件
aria2.onDownloadStart = function(task){
	wb.invoke("onDownloadStart",task.gid);	
}

aria2.onDownloadPause = function(task){ 
	wb.invoke("onDownloadPause",task.gid);	
}
	
aria2.onDownloadStop = function(task){
	wb.invoke("onDownloadStop",task.gid);	
} 

aria2.onDownloadComplete = function(task){ 
	if(aria2.taskIsTorrent(task.gid)){
		wb.invoke("onTorrentComplete",task.gid);
	} 
	else{
		wb.invoke("onDownloadComplete",task.gid);
		
		/*
		//aria2 会话不会自动保存已完成任务（重启不恢复），可用以下代码保存：
		var downloadedFiles = table.append(downloadedFiles,{
			gid = task.gid;//重启后 gid 无效
			name = aria2.taskName(task.gid);
			url = aria2.taskUrl(task.gid);
			path = aria2.taskFilePath(task.gid);
		});
		
		var path = io.joinpath(aria2.saveSessionPath,"downloadedFiles.table");
		JSON.save(path,downloadedFiles)
		*/
	} 
} 

aria2.onDownloadError = function(task){
	var errMsg = aria2.taskErrorMessage(task.gid); 
	wb.invoke("onDownloadError",task.gid,errMsg);	
} 

//更新下载进度
updateDownloadStatus = function(init){
	aria2.tellActive(function(result,err){
		if(result) { 
			var totalLength,completedLength = math.size64(),math.size64();
			var progressData = {};
			
			for(k,v in result){ 
				totalLength.add(v.totalLength);
				completedLength.add(v.completedLength);
				
				progressData[v.gid] = {
					downloadSpeed = tonumber(v.downloadSpeed) : 0;
					totalLength = tonumber(v.totalLength) : 0;
					completedLength = tonumber(v.completedLength) : 0;
					connections = v.connections;
					status = v.status;
				};
			} 
			
			wb.invoke("updateProgress",progressData);
			
			//显示所有下载任务的总进度
			var pos = (completedLength / totalLength) * 100;
			if(pos >= 0 && pos <= 100){
				taskbar.SetProgressValue(winform.hwnd,pos,100);
			}
		}  
	},"gid","status","connections","downloadSpeed","totalLength","completedLength"); 
	
	if(init){
		aria2.tellWaiting(0,20,function(result,err){
			if(result) {
				var progressData = {};
				for(i,v in result){
					progressData[v.gid] = {
						downloadSpeed = tonumber(v.downloadSpeed) : 0;
						totalLength = tonumber(v.totalLength) : 0;
						completedLength = tonumber(v.completedLength) : 0;
						connections = v.connections;
						status = v.status;
					};
				} 
				wb.invoke("updateProgress",progressData);
			}  
		},"gid","status","connections","downloadSpeed","totalLength","completedLength");
	}
}

//错误处理
aria2.onError = function(errMsg,rpcErr){
	wb.invoke("showError",errMsg);
}

wb.html = /**
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}

.container {
    max-width: 900px;
    margin: 0 auto;
    background: white;
    border-radius: 12px;
    box-shadow: 0 20px 40px rgba(0,0,0,0.1);
    overflow: hidden;
}

.input-section {
    padding: 30px;
    background: #f8f9fa;
    border-bottom: 1px solid #e0e0e0;
}

.input-wrapper {
    position: relative;
    display: flex;
    align-items: center;
    background: white;
    border-radius: 30px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.05);
    transition: box-shadow 0.3s;
}

.input-wrapper:hover {
    box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}

.input-wrapper input {
    flex: 1;
    border: none;
    outline: none;
    padding: 14px 20px;
    font-size: 14px;
    background: transparent;
}

.download-btn {
    background: linear-gradient(135deg, #667eea, #764ba2);
    border: none;
    color: white;
    padding: 12px 20px;
    margin: 4px;
    border-radius: 26px;
    cursor: pointer;
    font-size: 16px;
    transition: all 0.3s;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 48px;
}

.download-btn:hover {
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.download-btn:active {
    transform: scale(0.95);
}

.download-list {
    padding: 20px;
    max-height: 400px;
    overflow-y: auto;
}

.download-list::-webkit-scrollbar {
    width: 8px;
}

.download-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
}

.download-list::-webkit-scrollbar-thumb {
    background: #888;
    border-radius: 4px;
}

.download-list::-webkit-scrollbar-thumb:hover {
    background: #555;
}

.download-item {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 12px;
    transition: all 0.3s;
    cursor: pointer;
}

.download-item:hover {
    background: #e9ecef;
    transform: translateX(4px);
}

.download-item.error {
    background: #fee;
    border-left: 4px solid #f44336;
}

.download-item.complete {
    background: #e8f5e9;
    border-left: 4px solid #4caf50;
}

.download-item.paused {
    background: #fff3e0;
    border-left: 4px solid #ff9800;
}

.file-name {
    font-weight: 600;
    color: #333;
    margin-bottom: 8px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.progress-bar-wrapper {
    height: 6px;
    background: #e0e0e0;
    border-radius: 3px;
    overflow: hidden;
    margin: 8px 0;
}

.progress-bar {
    height: 100%;
    background: linear-gradient(90deg, #667eea, #764ba2);
    transition: width 0.3s;
    border-radius: 3px;
}

.download-info {
    display: flex;
    justify-content: space-between;
    font-size: 12px;
    color: #666;
    margin-top: 8px;
}

.download-speed {
    color: #4caf50;
    font-weight: 500;
}

.error-message {
    color: #f44336;
    font-size: 12px;
    margin-top: 5px;
}

.empty-state {
    text-align: center;
    padding: 60px 20px;
    color: #999;
}

.empty-state svg {
    width: 80px;
    height: 80px;
    margin-bottom: 20px;
    opacity: 0.3;
}

.context-menu {
    position: fixed;
    background: white;
    border-radius: 8px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.15);
    z-index: 1000;
    display: none;
    overflow: hidden;
}

.context-menu-item {
    padding: 10px 20px;
    cursor: pointer;
    font-size: 14px;
    color: #333;
    transition: background 0.2s;
}

.context-menu-item:hover {
    background: #f0f0f0;
}

.context-menu-item.disabled {
    color: #999;
    cursor: not-allowed;
}

.context-menu-item.disabled:hover {
    background: transparent;
}
</style>
</head>
<body>
<div class="container">
    <div class="input-section">
        <div class="input-wrapper">
            <input type="text" id="urlInput" placeholder="输入下载链接、磁力链接或种子文件路径...">
            <button class="download-btn" id="downloadBtn" title="开始下载">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                    <polyline points="7 10 12 15 17 10"/>
                    <line x1="12" y1="15" x2="12" y2="3"/>
                </svg>
            </button>
        </div>
    </div>
    
    <div class="download-list" id="downloadList">
        <div class="empty-state">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                <polyline points="7 10 12 15 17 10"/>
                <line x1="12" y1="15" x2="12" y2="3"/>
            </svg>
            <p>暂无下载任务</p>
        </div>
    </div>
</div>

<div class="context-menu" id="contextMenu">
    <div class="context-menu-item" data-action="pause">暂停</div>
    <div class="context-menu-item" data-action="resume">继续</div>
    <div class="context-menu-item" data-action="remove">移除</div>
    <div class="context-menu-item" data-action="explore">浏览文件</div>
    <div class="context-menu-item" data-action="copy">复制链接</div>
</div>

<script>
const downloads = {};
let currentGid = null;

// 添加下载任务
document.getElementById('downloadBtn').onclick = async function() {
    const url = document.getElementById('urlInput').value.trim();
    const result = await aardio.addTask(url);
    
    if (!result.success) {
        showError(result.error);
    } else {
        document.getElementById('urlInput').value = '';
    }
};

// 回车键下载
document.getElementById('urlInput').onkeypress = function(e) {
    if (e.key === 'Enter') {
        document.getElementById('downloadBtn').click();
    }
};

// 任务添加回调
async function onTaskAdded(gid, url, error) {
    if (error) {
        addDownloadItem(null, url, error);
    } else {
        const name = await aardio.getTaskName(gid) || '获取文件名...';
        addDownloadItem(gid, name);
    }
}

// 添加下载项到列表
function addDownloadItem(gid, name, error) {
    const list = document.getElementById('downloadList');
    
    // 移除空状态提示
    const emptyState = list.querySelector('.empty-state');
    if (emptyState) emptyState.remove();
    
    const item = document.createElement('div');
    item.className = 'download-item' + (error ? ' error' : '');
    item.dataset.gid = gid || '';
    
    if (error) {
        item.innerHTML = `
            <div class="file-name">${name}</div>
            <div class="error-message">${error}</div>
        `;
    } else {
        item.innerHTML = `
            <div class="file-name">${name}</div>
            <div class="progress-bar-wrapper">
                <div class="progress-bar" style="width: 0%"></div>
            </div>
            <div class="download-info">
                <span class="download-speed">准备中...</span>
                <span class="download-size">-</span>
                <span class="connections">-</span>
            </div>
        `;
        
        // 右键菜单
        item.oncontextmenu = function(e) {
            e.preventDefault();
            showContextMenu(e, gid);
        };
    }
    
    list.appendChild(item);
    
    if (gid) {
        downloads[gid] = item;
    }
}

// 更新进度
function updateProgress(progressData) {
    for (const gid in progressData) {
        const data = progressData[gid];
        const item = downloads[gid];
    
        // 添加：如果任务不存在，先创建
        if (!item) {
            aardio.getTaskName(gid).then(name => {
                if (name) {
                    addDownloadItem(gid, name || '未知文件');
                    // 创建后再更新一次进度
                    updateProgress({[gid]: data});
                }
            });
            continue;
        }
        
        const speed = data.downloadSpeed || 0;
        const total = data.totalLength || 0;
        const completed = data.completedLength || 0;
        const progress = total > 0 ? (completed / total * 100) : 0;
        
        const progressBar = item.querySelector('.progress-bar');
        if (progressBar) {
            progressBar.style.width = progress + '%';
        }
        
        const speedEl = item.querySelector('.download-speed');
        if (speedEl) {
            if (speed > 0) {
                speedEl.textContent = formatSpeed(speed);
            } else if (data.status === 'complete') {
                speedEl.textContent = '已完成';
            } else if (data.status === 'paused') {
                speedEl.textContent = '已暂停';
            } else {
                speedEl.textContent = '等待中...';
            }
        }
        
        const sizeEl = item.querySelector('.download-size');
        if (sizeEl) {
            const completedStr = formatSize(completed);
            const totalStr = formatSize(total);
            sizeEl.textContent = `${completedStr} / ${totalStr}`;
        }
        
        const connEl = item.querySelector('.connections');
        if (connEl && data.connections) {
            connEl.textContent = `连接数: ${data.connections}`;
        }
    }
}

// 右键菜单
function showContextMenu(e, gid) {
    currentGid = gid;
    const menu = document.getElementById('contextMenu');
    
    // 获取任务状态并更新菜单项
    aardio.getTaskStatus(gid).then(status => {
        const pauseItem = menu.querySelector('[data-action="pause"]');
        const resumeItem = menu.querySelector('[data-action="resume"]');
        
        if (status && status.status === 'active') {
            pauseItem.style.display = 'block';
            resumeItem.style.display = 'none';
        } else if (status && status.status === 'paused') {
            pauseItem.style.display = 'none';
            resumeItem.style.display = 'block';
        } else {
            pauseItem.style.display = 'none';
            resumeItem.style.display = 'none';
        }
    });
    
    menu.style.display = 'block';
    menu.style.left = e.pageX + 'px';
    menu.style.top = e.pageY + 'px';
    
    // 检查边界
    const rect = menu.getBoundingClientRect();
    if (rect.right > window.innerWidth) {
        menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
    }
    if (rect.bottom > window.innerHeight) {
        menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
    }
}

// 隐藏右键菜单
document.onclick = function() {
    document.getElementById('contextMenu').style.display = 'none';
};

// 右键菜单操作
document.getElementById('contextMenu').onclick = async function(e) {
    const action = e.target.dataset.action;
    if (!action || !currentGid) return;
    
    switch(action) {
        case 'pause':
            await aardio.pauseTask(currentGid);
            break;
        case 'resume':
            await aardio.unpauseTask(currentGid);
            break;
        case 'remove':
            await aardio.removeTask(currentGid);
            if (downloads[currentGid]) {
                downloads[currentGid].remove();
                delete downloads[currentGid];
            }
            checkEmptyState();
            break;
        case 'explore':
            const path = await aardio.getTaskPath(currentGid);
            if (path) await aardio.exploreFile(path);
            break;
        case 'copy':
            const url = await aardio.getTaskUrl(currentGid);
            if (url) await aardio.copyLink(url);
            break;
    }
    
    this.style.display = 'none';
};

// 检查空状态
function checkEmptyState() {
    const list = document.getElementById('downloadList');
    if (list.children.length === 0) {
        list.innerHTML = `
            <div class="empty-state">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                    <polyline points="7 10 12 15 17 10"/>
                    <line x1="12" y1="15" x2="12" y2="3"/>
                </svg>
                <p>暂无下载任务</p>
            </div>
        `;
    }
}

// 工具函数
function formatSize(bytes) {
    if (!bytes || bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

function formatSpeed(bytesPerSec) {
    if (!bytesPerSec || bytesPerSec === 0) return '0 B/s';
    return formatSize(bytesPerSec) + '/s';
}

// 事件处理函数
function onDownloadStart(gid) {
    const item = downloads[gid];
    if (item) {
        item.classList.remove('error', 'complete', 'paused');
    }
}

function onDownloadPause(gid) {
    const item = downloads[gid];
    if (item) {
        item.classList.add('paused');
    }
}

function onDownloadStop(gid) {
    const item = downloads[gid];
    if (item) {
        const speedEl = item.querySelector('.download-speed');
        if (speedEl) speedEl.textContent = '已停止';
    }
}

function onDownloadComplete(gid) {
    const item = downloads[gid];
    if (item) {
        item.classList.add('complete');
        const speedEl = item.querySelector('.download-speed');
        if (speedEl) speedEl.textContent = '已完成';
        const progressBar = item.querySelector('.progress-bar');
        if (progressBar) progressBar.style.width = '100%';
    }
}

function onTorrentComplete(gid) {
    const item = downloads[gid];
    if (item) {
        setTimeout(
        	()=>{
        	  	item.remove();
        		delete downloads[gid];
        		//checkEmptyState();	
        	},2000
        )
      
    }
}

function onDownloadError(gid, errMsg) {
    const item = downloads[gid];
    if (item) {
        item.classList.add('error');
        const speedEl = item.querySelector('.download-speed');
        if (speedEl) speedEl.textContent = '错误: ' + errMsg;
    }
}

function showError(msg) {
    console.error(msg);
    // 可以添加更友好的错误提示UI
}
</script>
</body>
</html>
**/

winform.show();

//启动就绪执行
aria2.ready(
	function(){
		updateDownloadStatus(true);
		winform.setInterval(updateDownloadStatus,1500);  
	}
)

winform.onDestroy = function(){
	//退出前务必关闭 aria2 并保存会话，下次启动才能恢复状态
    aria2.stop();
}

win.loopMessage();
```