# aardio 范例: GitHub API

```aardio
import win.ui;
import fonts.fontAwesome;
import web.rest.github;
import web.form.simpleMarkdown;
/*DSG{{*/
var winform = win.form(text="GitHub 助手";right=1000;bottom=680;bgcolor=0xFAFAFA;border="none")
winform.add(
btnQuery={cls="plus";text='\uF002  查询';left=770;top=78;right=920;bottom=118;bgcolor=0x4FA42E;border={radius=8};color=0xFFFFFF;dr=1;dt=1;font=LOGFONT(h=-15;name='FontAwesome';weight=600);notify=1;z=6};
cmbMirrors={cls="combobox";left=110;top=135;right=561;bottom=161;dl=1;dt=1;edge=1;font=LOGFONT(h=-14);hscroll=1;items={};mode="dropdown";vscroll=1;z=8};
custom={cls="custom";left=20;top=180;right=980;bottom=660;bgcolor=0xFFFFFF;db=1;dl=1;dr=1;dt=1;z=10};
editUrl={cls="plus";left=110;top=78;right=750;bottom=118;align="left";bgcolor=0xFFFFFF;border={color=0xD0D0D0;radius=8;width=1};dl=1;dr=1;dt=1;editable=1;font=LOGFONT(h=-14);iconColor=0x969696;iconStyle={align="left";font=LOGFONT(h=-16;name='FontAwesome');padding={left=10}};iconText='\uF002';textPadding={left=40;top=10;right=12;bottom=10};z=5};
iconGithub={cls="bkplus";text='\uF09B';left=20;top=12;right=52;bottom=44;color=0xFFFFFF;dl=1;dt=1;font=LOGFONT(h=-24;name='FontAwesome');z=2};
lblMirrors={cls="plus";text="优选镜像:";left=20;top=135;right=105;bottom=161;align="right";color=0x505050;dl=1;dt=1;font=LOGFONT(h=-13;weight=600);z=7};
lblUrl={cls="plus";text="项目地址:";left=20;top=78;right=105;bottom=118;align="right";color=0x505050;dl=1;dt=1;font=LOGFONT(h=-13;weight=600);z=4};
mirrorStatus={cls="plus";left=589;top=133;right=918;bottom=159;align="left";color=0x808080;dl=1;dr=1;dt=1;font=LOGFONT(h=-14);iconStyle={align="left";font=LOGFONT(name='FontAwesome')};textPadding={left=21};z=9};
titleBar={cls="bkplus";left=0;top=0;right=1002;bottom=60;bgcolor=0x3D2817;dl=1;dr=1;dt=1;z=1};
titleText={cls="bkplus";text="GitHub 助手";left=56;top=16;right=300;bottom=44;align="left";color=0xFFFFFF;dl=1;dt=1;font=LOGFONT(h=-20;weight=700);z=3}
)
/*}}*/

var wb = web.form.simpleMarkdown(winform.custom)
wb.translate = function( url ){
	if(!string.find(url,"<releases/download>|<\.zip!>|<\.gz!>|<\.7z!>")){
		return url;
	}
	var mirrorUrl = winform.cmbMirrors.selText || "";
	if(#mirrorUrl) return  mirrorUrl + "/" + url;
}

// 添加无边框窗口的标题栏按钮
import win.ui.simpleWindow;
win.ui.simpleWindow(winform); 
winform.show();
	
// 格式化日期
var formatDate = function(dateStr) {
	if(!dateStr) return "未知";
	var y, m, d, h, mi, s = string.match(dateStr, "(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)");
	if(y) return string.format("%s-%s-%s %s:%s:%s", y, m, d, h, mi, s);
	return dateStr;
}

winform.editUrl.setCueBannerText("请输入 用户名/项目名 或 GitHub 项目或文件网址");

// 支持回车查询
winform.editUrl.editBox.onOk = function(ctrl,alt,shift){ 
	winform.btnQuery.oncommand();
	return true; 	
}

// 生成 Markdown
var generateMarkdown = function(info) {
    var md = {};
    
    table.push(md, "# 📦 " + info.fullName);
    table.push(md, "");
    
    if(info.description) {
        table.push(md, "> " + info.description);
        table.push(md, "");
    }
    
    table.push(md, "---");
    table.push(md, "");
    
    // 基本统计
    table.push(md, "## 📊 项目统计");
    table.push(md, "");
    table.push(md, "| 指标 | 数值 | 指标 | 数值 |");
    table.push(md, "|:----:|:----:|:----:|:----:|");
    table.push(md, "| ⭐ Stars | **" + tostring(info.stars) + "** | 🍴 Forks | **" + tostring(info.forks) + "** |");
    table.push(md, "| 👀 Watchers | **" + tostring(info.watchers) + "** | 🐛 Issues | **" + tostring(info.openIssues) + "** |");
    table.push(md, "| 👥 Contributors | **" + tostring(info.contributorCount) + "** | 📦 Size | **" + string.format("%.2f MB", info.size/1024) + "** |");
    table.push(md, "");
    
    // 项目详情
    table.push(md, "## 📋 项目详情");
    table.push(md, "");
    table.push(md, "| 属性 | 信息 |");
    table.push(md, "|------|------|");
    table.push(md, "| 📜 开源协议 | " + info.license + " |");
    table.push(md, "| 🌿 默认分支 | `" + info.defaultBranch + "` |");
	if(info.language) table.push(md, "| 🔤 主要语言 | " + info.language + " |");
	if(info.homepage && #info.homepage) {
        table.push(md, "| 🏠 项目主页 | [" + info.homepage + "](" + info.homepage + ") |");
    }
    table.push(md, "| 🔗 GitHub | [" + info.htmlUrl + "](" + info.htmlUrl + ") |");
	 
    table.push(md, "");
    
    if(info.fork) {
        table.push(md, "> ⚠️ 这是一个 Fork 项目");
        table.push(md, "");
    }
    
    if(info.archived) {
        table.push(md, "> 📦 该项目已归档（只读）");
        table.push(md, "");
    }
    
    // 标签
    if(info.topics && #info.topics > 0) {
        table.push(md, "## 🏷️ 标签");
        table.push(md, "");
        var topicStr = "";
        for(i, topic in info.topics) {
            topicStr = topicStr + "`" + topic + "` ";
        }
        table.push(md, topicStr);
        table.push(md, "");
    }
    
    // 语言统计
    if(info.languages && table.count(info.languages) > 0) {
        table.push(md, "## 💻 编程语言占比");
        table.push(md, "");
        var total = 0;
        for(lang, bytes in info.languages) {
            total = total + bytes;
        }
        
        var langs = {};
        for(lang, bytes in info.languages) {
            table.push(langs, { name = lang; bytes = bytes; percent = bytes / total * 100 });
        }
        table.sort(langs, function(next) { return owner.bytes > next.bytes; });
        
        table.push(md, "| 语言 | 占比 | 语言 | 占比 |");
        table.push(md, "|:----:|:----:|:----:|:----:|");
        
        for(i = 1; #langs; 2) {
            var l1 = langs[i];
            var l2 = langs[i + 1];
            var row = "| " + l1.name + " | " + string.format("%.1f%%", l1.percent) + " |";
            if(l2) {
                row = row + " " + l2.name + " | " + string.format("%.1f%%", l2.percent) + " |";
            } else {
                row = row + " - | - |";
            }
            table.push(md, row);
        }
        table.push(md, "");
    }
    
    // 时间信息
    table.push(md, "## 📅 时间线");
    table.push(md, "");
    table.push(md, "| 事件 | 时间 |");
    table.push(md, "|------|------|");
    table.push(md, "| 🎉 仓库创建 | " + formatDate(info.createdAt) + " |");
    
    if(info.firstCommit) {
        table.push(md, "| 📝 首次提交 | " + formatDate(info.firstCommit.date) 
        	+ "[`" + string.left(info.firstCommit.sha, 7) + "`  " + info.firstCommit.author + "](" + (info.htmlUrl + "/commit/" + info.firstCommit.sha) + ") |");
        table.push(md, "| 📊 总提交数 | **" + tostring(info.firstCommit.totalCommits) + "** 次 |");
    }
    
    table.push(md, "| 🔄 最后更新 | " + formatDate(info.updatedAt) + " |");
    table.push(md, "| 🚀 最后推送 | " + formatDate(info.pushedAt) + " |");
    table.push(md, "");
 

    // Star 历史图
    table.push(md, "## ⭐ Star 历史趋势");
    table.push(md, "");
    table.push(md, "![Star History](https://api.star-history.com/svg?repos=" + info.user + "/" + info.repo + "&type=Date)");
    table.push(md, "");
    
    // 下载链接
	table.push(md, "## 📥 下载");
	table.push(md, "");
	table.push(md, "### 源码下载");
	table.push(md, "");
	table.push(md, "| 格式 | 链接 |");
	table.push(md, "|------|------|");
	
	
	table.push(md, "| 📦 ZIP | [" + info.repo + "-" + info.defaultBranch + ".zip](" + info.zipUrl + ") |");
	table.push(md, "");
	
	// 最新 Release
	if(info.latestRelease) {
    	table.push(md, "### 🎉 最新发布: " + info.latestRelease.tagName);
    	table.push(md, "");
    	if(info.latestRelease.name) {
        	table.push(md, "**" + info.latestRelease.name + "**");
        	table.push(md, "");
    	}
    	table.push(md, "- 📅 发布时间: " + formatDate(info.latestRelease.publishedAt));
    	table.push(md, "- 🔗 [查看发布页面](" + info.latestRelease.htmlUrl + ")");
    	table.push(md, "");
    	
    	// Release 附件下载
    	if(info.latestRelease.assets && #info.latestRelease.assets > 0) {
        	table.push(md, "**📎 附件下载:**");
        	table.push(md, "");
        	table.push(md, "| 文件名 | 大小 | 下载次数 |");
        	table.push(md, "|--------|------|----------|");
        	for(i, asset in info.latestRelease.assets) {
            	var sizeStr = asset.size > 1048576 
                	? string.format("%.2f MB", asset.size / 1048576)
                	: string.format("%.2f KB", asset.size / 1024);
            	table.push(md, "| [" + asset.name + "](" + asset.downloadUrl + ") | " + sizeStr + " | " + tostring(asset.downloadCount) + " |");
        	}
        	table.push(md, "");
    	} else {
        	// 没有附件时显示源码包
        	table.push(md, "**📎 源码包:**");
        	table.push(md, "");
        	
        	table.push(md, "- [下载 ZIP](https://github.com/"+info.user+"/"+info.repo+"/archive/refs/tags/"+info.latestRelease.tagName+".zip)");
        	
        	table.push(md, "");
    	}
	} else {
    	table.push(md, "> ℹ️ 该项目暂无发布版本");
    	table.push(md, "");
	}
    
    return string.join(md, '\n');
}

// 创建线程命令监听
winform.onQueryResult = function(info, err) {
	winform.btnQuery.disabledText = null; 
	
	if(info) {
		var md = generateMarkdown(info);
		wb.setMarkdown(md);
	} else {
		wb.showError('❌ 获取失败：' + (err || "未知错误，请检查项目地址是否正确"));
	}
}

winform.btnQuery.oncommand = function(id, event) {
    var url = string.trim(winform.editUrl.text);
    if(!#url) {
        winform.msgbox("请输入 GitHub 项目地址", "提示");
        winform.editUrl.setFocus();
        return;
    }
    
    winform.btnQuery.disabledText = ['\uF254','\uF251','\uF252','\uF253','\uF250'] 
    
    wb.setMarkdown("");
    wb.showLoading(" 请稍候，正在从 GitHub 获取数据...")
    
    thread.invoke(
        function(winform,url) {
            import web.rest.github;
            
            // 获取首次提交
            var getFirstCommit = function(user, repo, defaultBranch) {
                var client = web.rest.github();
                var repoApi = client.api().repos[user][repo];
                
                var totalCommits = 1;
                client.afterSend = function(statusCode, contentLength) {
                    var linkHeader = client.readHeader("Link");
                    if(linkHeader) { 
                        totalCommits = tonumber(string.match(linkHeader, `\&page\=(\d+)\>\;\s*rel\=\"last\"`)) : 1;
                    }
                } 
                
                var commitsApi = repoApi.commits;
                commitsApi.get({ sha = defaultBranch; per_page = 1 });
                client.afterSend = null;
                
                var firstCommitData = commitsApi.get({
                    sha = defaultBranch;
                    per_page = 1;
                    page = totalCommits;
                });
                
                client.close(); 
                
                if(firstCommitData && #firstCommitData) {
                    var first = firstCommitData[1];
                    return {
                        sha = first.sha;
                        author = first.commit.author.name;
                        date = first.commit.author.date;
                        message = first.commit.message;
                        totalCommits = totalCommits;
                    };
                }
                
               
                return null;
            }
            
            // 获取仓库接口
            var repoApi,repoUrl;
            repoApi,user,repo,repoUrl  = web.rest.github.repo(url)
            if(!repoApi){
				winform.onQueryResult(null, user || "无效的项目地址格式");
                return;
            }
			winform.editUrl.text = repoUrl;
			
			var restClient = repoApi.getRestClient();
            var repoInfo,err = repoApi.get();
            
            if(!repoInfo) {
                restClient.close();
                winform.onQueryResult(null, err || "无法获取仓库信息，请检查项目是否存在");
                return;
            }
            
            // 获取语言统计
            var languages = repoApi.languages.get();
            
            // 获取贡献者数量
            var contributorCount = 1;
            restClient.afterSend = function() {
                var linkHeader = restClient.readHeader("Link");
                if(linkHeader) {
                    contributorCount = tonumber(string.match(linkHeader, `\&page\=(\d+)\>\;\s*rel\=\"last\"`)) : 1;
                }
            }
            repoApi.contributors.get({ per_page = 1 });
            
            // 获取最新 Release
			var latestRelease = null;
			var releaseData = repoApi.releases.latest.get();
			 
			if(releaseData && releaseData.tag_name) {
    			latestRelease = {
        			tagName = releaseData.tag_name;
        			name = releaseData.name;
        			publishedAt = releaseData.published_at;
        			htmlUrl = releaseData.html_url;
        			zipballUrl = releaseData.zipball_url;
        			tarballUrl = releaseData.tarball_url;
        			assets = {};
    			};
    			
    			// 获取所有附件下载链接
    			if(releaseData.assets && #releaseData.assets) {
        			for(i, asset in releaseData.assets) {
            			table.push(latestRelease.assets, {
                			name = asset.name;
                			size = asset.size;
                			downloadUrl = asset.browser_download_url;
                			downloadCount = asset.download_count;
            			});
        			}
    			}
			}
            
            restClient.close();
            
            // 获取首次提交
            var firstCommit = getFirstCommit(user, repo, repoInfo.default_branch);
            
            var info = {
                user = user;
                repo = repo;
                fullName = repoInfo.full_name;
                description = repoInfo.description;
                stars = repoInfo.stargazers_count;
                forks = repoInfo.forks_count;
                watchers = repoInfo.watchers_count;
                openIssues = repoInfo.open_issues_count;
                language = repoInfo.language;
                languages = languages;
                license = repoInfo.license ? repoInfo.license.name : "未指定";
                defaultBranch = repoInfo.default_branch;
                createdAt = repoInfo.created_at;
                updatedAt = repoInfo.updated_at;
                pushedAt = repoInfo.pushed_at;
                homepage = repoInfo.homepage;
                htmlUrl = repoInfo.html_url;
                topics = repoInfo.topics;
                contributorCount = contributorCount;
                firstCommit = firstCommit;
                size = repoInfo.size;
                archived = repoInfo.archived;
                fork = repoInfo.fork; 
                latestRelease = latestRelease;
    			zipUrl = "https://github.com/" + user + "/" + repo + "/archive/refs/heads/" + repoInfo.default_branch + ".zip";
            };
            
            winform.onQueryResult(info);
        },winform,url
    )
}

wb.setMarkdown(`# 🔍 GitHub 助手

## ✨ 功能特性

- 📊 显示项目基本统计（Stars、Forks、Issues 等）
- 📋 展示项目详情（语言、协议、标签等）
- 📅 时间线信息（创建时间、首次提交、最后更新）
- 📈 Star 历史趋势图
- 📥 源码与 Release 下载链接
- 🚀 多线程自动获取可用镜像服务器（点击下载链接时自动转换）

> GitHub 接口如果调用次数过多时会失败，可更换 IP 再试
`);
 

winform.mirrorStatus.disabledText = ['\uF254','\uF251','\uF252','\uF253','\uF250',text=' 正在检测镜像...']
 
import web.rest.githubMirrors;
var gitMirrors = web.rest.githubMirrors();

if(gitMirrors){	
	var first; 
	winform.setInterval( 
		function(){
			while(var url = gitMirrors.pop(1)){
				if(url){
					winform.cmbMirrors.add(url);
					if(!first){
						winform.cmbMirrors.selIndex = 1;
						first = 1;
					}
				}	
			}
			
			if(!gitMirrors.busy()){
				 winform.mirrorStatus.disabledText = null;
				 winform.mirrorStatus.argbColor = 0xFF2EA44F;
				 winform.mirrorStatus.text = '镜像就绪，请点击下载链接';
				 winform.mirrorStatus.iconText = '\uF00C';
				 return false;
			}
		},1000
	)
}
else{
	winform.mirrorStatus.argbColor = 0xFFFE0505;
	winform.mirrorStatus.text = '访问接口失败，请更换 IP 或稍后再试';
	winform.mirrorStatus.iconText = '\uF00D';
}

winform.editUrl.editBox.enablePopMenu()

// 查询按钮样式
winform.btnQuery.background = 0xFF2EA44F; // GitHub 绿色
winform.btnQuery.skin({
	background={
		default=0xFF2EA44F;
		hover=0xFF3FB950;
		active=0xFF238636;
		disabled=0xFFAAAAAA
	};
	color={
		default=0xFFFFFFFF
	};
	border={
		default={radius=8;width=0}
	}
})

// 输入框悬停效果
winform.editUrl.skin({
	border={
		default={color=0xFFD0D0D0;radius=8;width=1};
		hover={color=0xFF2EA44F;radius=8;width=2};
		focus={color=0xFF2EA44F;radius=8;width=2}
	}
})

win.loopMessage();
```