aardio 文档

aardio 范例: BT 下载 - 自绘卡片风格

BT 下载 -自绘 listview 风格

// BT 下载 - 自绘卡片风格
// BT 下载 -自绘 listview 风格: https://www.aardio.com/zh-cn/docs/examples/Network/Transfer/bt.draw.listview.html

import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="BT 下载 - 自绘卡片风格";right=720;bottom=560;bgcolor=0xF0F0F0)
winform.add(
btnNext={cls="plus";text='\uF054';left=200;top=510;right=240;bottom=540;color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-14;name='FontAwesome');notify=1;z=6};
btnPrev={cls="plus";text='\uF053';left=90;top=510;right=130;bottom=540;color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-14;name='FontAwesome');notify=1;z=4};
pie={cls="plus";text="0%";left=20;top=505;right=68;bottom=553;bgcolor=0x4A68E8;color=0xFFFFFF;db=1;dl=1;forecolor=0x4AD4A9;z=2};
plusList={cls="plus";left=0;top=0;right=720;bottom=495;db=1;dl=1;dr=1;dt=1;notify=1;z=1};
txtPage={cls="plus";text="1/1";left=135;top=510;right=195;bottom=540;db=1;dl=1;z=5};
txtUrl={cls="plus";left=258;top=517;right=700;bottom=543;align="left";border={bottom=1;color=0x909090};db=1;dl=1;dr=1;editable=1;notify=1;paddingTop=5;textPadding={left=4;right=28;bottom=1};z=3}
)
/*}}*/

// ========== 下载任务列表数据管理 ==========
var taskList = {
    items = {};
    gidToIndex = {};
    selectedIndex = 0;
    hoverIndex = 0;
    currentPage = 1;
    pageSize = 4;

    getTotalPages = function(){
        var count = 0;
        for(i,item in owner.items){
            if(!item.isRemoving) count++;
        }
        return math.max(1, math.ceil(count / owner.pageSize));
    };

    getPageItems = function(){
        var visibleItems = {};
        for(i,item in owner.items){
            if(!item.isRemoving){
                table.push(visibleItems, item);
            }
        }
        var startIdx = (owner.currentPage - 1) * owner.pageSize + 1;
        var endIdx = math.min(startIdx + owner.pageSize - 1, #visibleItems);
        var result = {};
        for(i=startIdx; endIdx){
            table.push(result, visibleItems[i]);
        }
        return result, startIdx;
    };

    addTask = function(gid, name){
        var item = {
            gid = gid;
            name = name || "未知文件";
            speed = "";
            downloaded = "";
            total = "";
            status = "等待中";
            connections = 0;
            percent = 0;
            isComplete = false;
            isError = false;
            isRemoving = false;
        };
        table.push(owner.items, item);
        owner.gidToIndex[gid] = #owner.items;
        return #owner.items;
    };

    getItemByGid = function(gid){
        var idx = owner.gidToIndex[gid];
        if(idx) return owner.items[idx], idx;
    };

    markRemoving = function(gid){
        var item = owner.getItemByGid(gid);
        if(item){
            item.isRemoving = true;
            item.status = "正在移除...";
            item.speed = "";
        }
    };

    removeByGid = function(gid){
        var idx = owner.gidToIndex[gid];
        if(!idx) return;
        table.remove(owner.items, idx);
        owner.gidToIndex[gid] = null;
        for(i=idx; #owner.items){
            owner.gidToIndex[owner.items[i].gid] = i;
        }
        if(owner.currentPage > owner.getTotalPages()){
            owner.currentPage = owner.getTotalPages();
        }
    };

    updateTask = function(gid, data){
        var item = owner.getItemByGid(gid);
        if(!item) return;
        if(item.isRemoving && !data.isRemoving) return;
        for(k,v in data){
            item[k] = v;
        }
    };

    hitTest = function(x, y, rcHeight, dpiScaleY){
        var cardMargin = 10 * dpiScaleY;
        var availHeight = rcHeight - cardMargin;
        var cardHeight = (availHeight / owner.pageSize) - cardMargin;
        var pageItems = owner.getPageItems();

        for(i=1; #pageItems){
            var yTop = cardMargin + (i - 1) * (cardHeight + cardMargin);
            var yBottom = yTop + cardHeight;
            if(y >= yTop && y <= yBottom){
                return i, pageItems[i];
            }
        }
        return 0, null;
    };
};

var updatePageText = function(){
    winform.txtPage.text = string.format("%d/%d", taskList.currentPage, taskList.getTotalPages());
};

// 翻页按钮
winform.btnPrev.skin({
    background = { hover=0x15000000; active=0x25000000 };
    color = { default=0xFF505050; hover=0xFF1976D2; active=0xFF1565C0; disabled=0xFFBBBBBB }
});
winform.btnNext.skin({
    background = { hover=0x15000000; active=0x25000000 };
    color = { default=0xFF505050; hover=0xFF1976D2; active=0xFF1565C0; disabled=0xFFBBBBBB }
});

winform.btnPrev.oncommand = function(){
    if(taskList.currentPage > 1){
        taskList.currentPage--;
        taskList.selectedIndex = 0;
        updatePageText();
        winform.plusList.redraw();
    }
};

winform.btnNext.oncommand = function(){
    if(taskList.currentPage < taskList.getTotalPages()){
        taskList.currentPage++;
        taskList.selectedIndex = 0;
        updatePageText();
        winform.plusList.redraw();
    }
};

// ========== 卡片式自绘 ==========
winform.plusList.onDrawBackground = function(graphics,rc,backgroundColor,foregroundColor){
    var brush = gdip.solidBrush(0xFFF5F5F5);
    graphics.fillRectangle(brush, rc);
    brush.delete();
};

winform.plusList.onDrawForeground = function(graphics,rc,foregroundRect,foregroundColor,color,font){

    graphics.smoothingMode = 4;
    graphics.textRenderingHint = 5;

    var scaleX, scaleY, dpiScaleX, dpiScaleY = winform.getScale();
    var rcWidth = rc.right - rc.left;
    var rcHeight = rc.bottom - rc.top;

    // 动态计算卡片高度,充分利用空间
    var cardMargin = 10 * dpiScaleY;
    var cardPadding = 14 * dpiScaleX;
    var availHeight = rcHeight - cardMargin;
    var cardHeight = (availHeight / taskList.pageSize) - cardMargin;
    var cardRadius = 8;

    var pageItems, startIdx = taskList.getPageItems();

    // 创建字体
    var fontFamily = gdip.family("Tahoma");//注意 XP 以后的系统 GDI+ 会自动将 Tahoma 修改为 "Segoe UI";
    var titleFont = fontFamily.createFont(10, 1, 3);
    var infoFont = fontFamily.createFont(9, 0, 3);
    var percentFont = fontFamily.createFont(11, 1, 3);
    var iconFamily = gdip.family("FontAwesome");
    var iconFont = iconFamily.createFont(16, 0, 3);
    var strFormat = gdip.stringFormat();
    strFormat.trimming = 3;
    strFormat.flags = 0x1000/*_StringFormatFlagsNoWrap*/;

    for(i, item in pageItems){
        var globalIdx = startIdx + i - 1;
        var yPos = cardMargin + (i - 1) * (cardHeight + cardMargin);
        var cardLeft = cardMargin;
        var cardWidth = rcWidth - cardMargin * 2;

        // 绘制卡片阴影
        var shadowPath = gdip.path();
        shadowPath.addRoundRect(::RECTF(cardLeft + 2, yPos + 3, cardWidth, cardHeight), cardRadius);
        var shadowBrush = gdip.solidBrush(0x15000000);
        graphics.fillPath(shadowBrush, shadowPath);
        shadowBrush.delete();
        shadowPath.delete();

        // 卡片背景
        var cardPath = gdip.path();
        cardPath.addRoundRect(::RECTF(cardLeft, yPos, cardWidth, cardHeight), cardRadius);

        var cardColor = 0xFFFFFFFF;
        if(globalIdx == taskList.selectedIndex){
            cardColor = 0xFFE8F4FD;
        } elseif(globalIdx == taskList.hoverIndex){
            cardColor = 0xFFFCFCFC;
        }

        var cardBrush = gdip.solidBrush(cardColor);
        graphics.fillPath(cardBrush, cardPath);
        cardBrush.delete();
        cardPath.delete();

        // ===== 状态配色 =====
        var statusIcon, accentColor, statusText;
        if(item.isComplete){
            statusIcon = '\uF058';
            accentColor = 0xFF43A047;
            statusText = "已完成";
        } elseif(item.isError){
            statusIcon = '\uF057';
            accentColor = 0xFFE53935;
            statusText = item.status;
        } elseif(item.isRemoving){
            statusIcon = '\uF1DA';
            accentColor = 0xFF9E9E9E;
            statusText = "移除中";
        } elseif(item.status == "下载中"){
            statusIcon = '\uF019';
            accentColor = 0xFF1E88E5;
            statusText = null;
        } elseif(item.status == "已暂停"){
            statusIcon = '\uF04C';
            accentColor = 0xFFFF9800;
            statusText = "已暂停";
        } else {
            statusIcon = '\uF017';
            accentColor = 0xFF9E9E9E;
            statusText = item.status;
        }

        // 布局计算
        var iconSize = 36 * dpiScaleX;
        var contentLeft = cardLeft + cardPadding;
        var contentTop = yPos + cardPadding;
        var contentHeight = cardHeight - cardPadding * 2;
        var contentWidth = cardWidth - cardPadding * 2;

        // 左侧图标区域
        var iconX = contentLeft;
        var iconY = contentTop + (contentHeight - iconSize) / 2;

        // 绘制图标背景圆
        var iconBgPath = gdip.path();
        iconBgPath.addEllipse(::RECTF(iconX, iconY, iconSize, iconSize));
        var iconBgBrush = gdip.solidBrush((accentColor & 0x00FFFFFF) | 0x18000000);
        graphics.fillPath(iconBgBrush, iconBgPath);
        iconBgBrush.delete();
        iconBgPath.delete();

        // 绘制图标
        var iconBrush = gdip.solidBrush(accentColor);
        var iconRc = ::RECTF(iconX, iconY, iconSize, iconSize);
        strFormat.align = 1;
        strFormat.lineAlign = 1;
        graphics.drawString(statusIcon, iconFont, iconRc, strFormat, iconBrush);
        iconBrush.delete();

        // 右侧信息区域
        var infoLeft = iconX + iconSize + 12 * dpiScaleX;
        var infoWidth = contentWidth - iconSize - 12 * dpiScaleX;

        // 三行布局:标题、进度条+百分比、详情
        var lineHeight = contentHeight / 3;

        // ===== 第一行:文件名 =====
        var titleY = contentTop;
        var titleBrush = gdip.solidBrush(0xFF333333);
        var titleRc = ::RECTF(infoLeft, titleY, infoWidth, lineHeight);
        strFormat.align = 0;
        strFormat.lineAlign = 1;
        graphics.drawString(item.name, titleFont, titleRc, strFormat, titleBrush);
        titleBrush.delete();

        // ===== 第二行:进度条 + 百分比 =====
        var progressY = contentTop + lineHeight;
        var percentWidth = 45 * dpiScaleX;
        var progressWidth = infoWidth - percentWidth - 8 * dpiScaleX;
        var progressHeight = 5 * dpiScaleY;
        var progressTop = progressY + (lineHeight - progressHeight) / 2;

        // 进度条背景
        var bgPath = gdip.path();
        bgPath.addRoundRect(::RECTF(infoLeft, progressTop, progressWidth, progressHeight), progressHeight / 2);
        var bgBrush = gdip.solidBrush(0xFFE0E0E0);
        graphics.fillPath(bgBrush, bgPath);
        bgBrush.delete();
        bgPath.delete();

        // 进度条前景
        if(item.percent > 0){
            var fgWidth = math.max(progressHeight, progressWidth * item.percent / 100);
            var fgPath = gdip.path();
            fgPath.addRoundRect(::RECTF(infoLeft, progressTop, fgWidth, progressHeight), progressHeight / 2);
            var fgBrush = gdip.solidBrush(accentColor);
            graphics.fillPath(fgBrush, fgPath);
            fgBrush.delete();
            fgPath.delete();
        }

        // 百分比文字(紧跟进度条右侧)
        var percentX = infoLeft + progressWidth + 8 * dpiScaleX;
        var percentBrush = gdip.solidBrush(accentColor);
        var percentRc = ::RECTF(percentX, progressY, percentWidth, lineHeight);
        strFormat.align = 0;
        strFormat.lineAlign = 1;
        var percentText = item.isComplete ? "完成" : (string.format("%.0f", item.percent) ++ "%");
        graphics.drawString(percentText, percentFont, percentRc, strFormat, percentBrush);
        percentBrush.delete();

        // ===== 第三行:详情信息 =====
        var infoY = contentTop + lineHeight * 2;
        var grayBrush = gdip.solidBrush(0xFF888888);
        strFormat.align = 0;
        strFormat.lineAlign = 1;

        var infoText = "";
        if(statusText){
            infoText = statusText;
            if(#item.total) infoText = statusText ++ "  ·  " ++ item.total;
        } else {
            if(#item.speed) infoText = "↓ " ++ item.speed;
            if(#item.downloaded && #item.total){
                if(#infoText) infoText = infoText ++ "  ·  ";
                infoText = infoText ++ item.downloaded ++ " / " ++ item.total;
            }
            if(item.connections > 0){
                if(#infoText) infoText = infoText ++ "  ·  ";
                infoText = infoText ++ tostring(item.connections) ++ " 连接";
            }
        }

        var infoRc = ::RECTF(infoLeft, infoY, infoWidth, lineHeight);
        graphics.drawString(infoText, infoFont, infoRc, strFormat, grayBrush);
        grayBrush.delete();
    }

    // 空状态
    if(#pageItems == 0){
        var emptyIconFont = iconFamily.createFont(32, 0, 3);
        strFormat.align = 1;
        strFormat.lineAlign = 1;

        var emptyBrush = gdip.solidBrush(0xFFCCCCCC);
        var iconRc = ::RECTF(0, rcHeight/2 - 40*dpiScaleY, rcWidth, 40*dpiScaleY);
        graphics.drawString('\uF0ED', emptyIconFont, iconRc, strFormat, emptyBrush);
        emptyBrush.delete();

        var textBrush = gdip.solidBrush(0xFF999999);
        var textRc = ::RECTF(0, rcHeight/2 + 5*dpiScaleY, rcWidth, 25*dpiScaleY);
        graphics.drawString("输入下载链接开始下载", infoFont, textRc, strFormat, textBrush);
        textBrush.delete();

        emptyIconFont.delete();
    }

    strFormat.delete();
    titleFont.delete();
    infoFont.delete();
    percentFont.delete();
    iconFont.delete();
    fontFamily.delete();
    iconFamily.delete();
};

// ========== 鼠标交互 ==========
winform.plusList.onMouseMove = function(wParam, lParam){
    var x, y = win.getMessagePos(lParam);
    var scaleX, scaleY, dpiScaleX, dpiScaleY = winform.getScale();
    var rc = owner.getClientRect();
    var newHover = taskList.hitTest(x, y, rc.bottom - rc.top, dpiScaleY);
    if(newHover != taskList.hoverIndex){
        taskList.hoverIndex = newHover;
        owner.redraw();
    }
};

winform.plusList.onMouseLeave = function(){
    if(taskList.hoverIndex != 0){
        taskList.hoverIndex = 0;
        owner.redraw();
    }
};

winform.plusList.onMouseUp = function(wParam, lParam){
    var x, y = win.getMessagePos(lParam);
    var scaleX, scaleY, dpiScaleX, dpiScaleY = winform.getScale();
    var rc = owner.getClientRect();
    var clickIdx = taskList.hitTest(x, y, rc.bottom - rc.top, dpiScaleY);
    if(clickIdx > 0 && clickIdx != taskList.selectedIndex){
        taskList.selectedIndex = clickIdx;
        owner.redraw();
    }
};


// ========== 添加下载按钮 ==========
var btnAdd = winform.txtUrl.addCtrl(
    cls="plus";
    marginRight=0;marginBottom=2;width=26;
    iconText = '\uF019';
    iconStyle={
        align="right";font=LOGFONT(h=-15;name='FontAwesome');padding={top=3}
    }
);

btnAdd.skin({
    color = {
        default = 0xB0000000;
        hover = 0xFF1976D2;
        active = 0xFF1565C0;
    };
});

winform.pie.setPieRange(0, 100);
winform.show();

// ========== aria2 集成 ==========
import process.aria2;
var aria2 = process.aria2();
aria2.startServer(maxConcurrentDownloads = 10);

var aria2Ui = {
    addTaskInfo = function(gid, url, status){
        if(!gid){
            import bencode;
            var name = bencode.magnet.getName(url);
            taskList.addTask("error_" ++ time.tick(), name || "未知文件");
            var item = taskList.items[#taskList.items];
            item.status = status || "未知错误";
            item.isError = true;
        }
        else {
            var existItem = taskList.getItemByGid(gid);
            if(!existItem){
                var taskName,tempName = aria2.taskName(gid);
                taskList.addTask(gid, taskName || "未知文件");

               if(tempName){
                    //获取真正的文件名
                    winform.setInterval( 
                        function(){
                            taskName,tempName = aria2.taskName(gid)
                            if(!tempName){
                                taskList.updateTask(gid, {name = taskName});
                                return 0;//删除定时器
                            }
                        },2000
                    ) 
                }
            }

            if(status){
                taskList.updateTask(gid, {status = status});
            }
        }
        updatePageText();
        winform.plusList.redraw();
    };

    remove = function(gid){
        taskList.removeByGid(gid);
        updatePageText();
        winform.plusList.redraw();
    };

    updateProgress = function(gid, progress){
        var item = taskList.getItemByGid(gid);
        if(item && item.isRemoving) return;

        if(!item){
            owner.addTaskInfo(gid);
            item = taskList.getItemByGid(gid);
        }

        if(progress){
            var percent = 0;
            progress.totalLength = tonumber(progress.totalLength);
            if(progress.totalLength > 0){
                percent = progress.completedLength / progress.totalLength * 100;
            }
            taskList.updateTask(gid, {
                speed = math.size64(progress.downloadSpeed).format() ++ "/s";
                downloaded = math.size64(progress.completedLength).format();
                total = math.size64(progress.totalLength).format();
                connections = tonumber(progress.connections) || 0;
                percent = percent;
                status = "下载中";
            });
        }
        else {
            var info = aria2.tellStatus(gid, "totalLength");
            taskList.updateTask(gid, {
                speed = "";
                total = math.size64(info.totalLength).format();
                connections = 0;
            });
        }
        winform.plusList.redraw();
    };
};

aria2.onDownloadStart = function(task){
    aria2Ui.addTaskInfo(task.gid, , "开始下载");
};

aria2.onDownloadPause = function(task){
    taskList.updateTask(task.gid, {status = "已暂停"; speed = ""});
    winform.plusList.redraw();
};

aria2.onDownloadStop = function(task){
    var item = taskList.getItemByGid(task.gid);
    if(item && item.isRemoving){
        taskList.removeByGid(task.gid);
    }
    else {
        taskList.updateTask(task.gid, {status = "已停止"; speed = ""});
    }
    updatePageText();
    winform.plusList.redraw();
};

aria2.onDownloadComplete = function(task){
    if(aria2.taskIsTorrent(task.gid)){
        aria2Ui.remove(task.gid);
    }
    else {
        taskList.updateTask(task.gid, {
            isComplete = true;
            speed = "";
            percent = 100;
        });
        aria2Ui.updateProgress(task.gid);
    }
};

aria2.onDownloadError = function(task){
    var errMsg = aria2.taskErrorMessage(task.gid);
    taskList.updateTask(task.gid, {
        status = errMsg || "下载出错";
        isError = true;
        speed = "";
    });
    winform.plusList.redraw();
};

// ========== 右键菜单 ==========
winform.plusList.onContextMenu = function(x, y){
    var cx, cy = win.toClient(owner.hwnd, x, y);
    var scaleX, scaleY, dpiScaleX, dpiScaleY = winform.getScale();
    var rc = owner.getClientRect();
    var clickIdx = taskList.hitTest(cx, cy, rc.bottom - rc.top, dpiScaleY);

    if(clickIdx <= 0) return;
    taskList.selectedIndex = clickIdx;
    owner.redraw();

    var pageItems = taskList.getPageItems();
    var item = pageItems[clickIdx];
    if(!item) return;

    var gid = item.gid;
    var statusInfo = aria2.tellStatus(gid, "status", "belongsTo");
    if(!statusInfo) return;

    import win.ui.menu;
    var popmenu = win.ui.popmenu(winform);

    popmenu.add('移除', function(){
        aria2.taskRemove(gid);
        taskList.markRemoving(gid);
        updatePageText();
        winform.plusList.redraw();
    });

    if(statusInfo.status == "active"){
        popmenu.add('暂停', function(){
            aria2.taskPause(gid);
        });
    }
    if(statusInfo.status == "paused"){
        popmenu.add('继续下载', function(){
            aria2.taskUnpause(gid);
        });
    }

    var path = aria2.taskFilePath(gid);
    if(#path){
        popmenu.add('打开文件位置', function(){
            raw.explore(path, "/select");
        });
    }

    popmenu.add('复制链接', function(){
        var url = aria2.taskUrl(gid);
        if(url){
            import win.clip;
            win.clip.write(url);
        }
    });

    popmenu.popup(x, y, true);
};


btnAdd.oncommand = function(){
    var url = winform.txtUrl.text;
    if(!#url){
        winform.msgboxErr("请输入下载地址或种子文件路径");
        winform.txtUrl.setFocus();
        return;
    }

    aria2.taskAdd(url, function(gid, err){
        if(err){
            aria2Ui.addTaskInfo(null, url, err[["message"]]);
        }
        else {
            aria2Ui.addTaskInfo(gid, url, "添加成功");
        }
    });

    winform.txtUrl.text = "";
};

winform.txtUrl.editBox.onOk = function(){
    btnAdd.oncommand();
    return true;
};

import com.interface.ITaskbarList3;
var taskbar = com.interface.ITaskbarList3.Create();

updateDownloadStatus = function(init){
    aria2.tellActive(function(result, err){
        if(result){
            var totalLength, completedLength = math.size64(), math.size64();
            for(k, v in result){
                totalLength.add(v.totalLength);
                completedLength.add(v.completedLength);
                aria2Ui.updateProgress(v.gid, v);
            }

            var pos = 0;
            if(tonumber(totalLength) > 0){
                pos = (completedLength / totalLength) * 100;
            }
            winform.pie.progressPos = pos;
            winform.pie.text = math.round(pos, pos<10 ? 1 : 0) ++ "%";
            taskbar.SetProgressValue(winform.hwnd, pos, 100);
        }
    }, "gid", "status", "connections", "downloadSpeed", "totalLength", "completedLength");

    if(init){
        aria2.tellWaiting(0, 20, function(result, err){
            if(result){
                for(i, v in result){
                    aria2Ui.updateProgress(v.gid, v);
                }
            }
        }, "gid", "status", "connections", "downloadSpeed", "totalLength", "completedLength");
    }
};

aria2.ready(function(){
    updateDownloadStatus(true);
    winform.setInterval(updateDownloadStatus, 1500);
});

winform.onDestroy = function(){
    aria2.stop();
};

win.loopMessage();

return winform,aria2;
Markdown 格式