diff --git a/daed/Makefile b/daed/Makefile
index ca69e610..86f45863 100644
--- a/daed/Makefile
+++ b/daed/Makefile
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=daed
-PKG_VERSION:=1.26.0
-PKG_RELEASE:=3
+PKG_VERSION:=dae-lang-core-v0.2.0
+PKG_RELEASE:=4
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/daeuniverse/daed.git
-PKG_SOURCE_VERSION:=4982c709ef937fd19eced01b2ed07aad23bb56e8
+PKG_SOURCE_VERSION:=06857f52c33cc3264aea9eb90431afbe8909073c
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=AGPL-3.0-only MIT
diff --git a/luci-app-easytier/Makefile b/luci-app-easytier/Makefile
index f80881fd..900f72df 100644
--- a/luci-app-easytier/Makefile
+++ b/luci-app-easytier/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.0)
-PKG_RELEASE:=2
+PKG_RELEASE:=3
LUCI_TITLE:=LuCI support for EasyTier
LUCI_DEPENDS:=+kmod-tun +easytier +luci-compat
diff --git a/luci-app-easytier/luasrc/controller/easytier.lua b/luci-app-easytier/luasrc/controller/easytier.lua
index 8d12aa55..28c8f00c 100644
--- a/luci-app-easytier/luasrc/controller/easytier.lua
+++ b/luci-app-easytier/luasrc/controller/easytier.lua
@@ -74,6 +74,9 @@ function index()
entry({"admin", "vpn", "easytier", "restart_service"}, call("restart_service")).leaf = true
entry({"admin", "vpn", "easytier", "toggle_core"}, call("toggle_core")).leaf = true
entry({"admin", "vpn", "easytier", "toggle_web"}, call("toggle_web")).leaf = true
+ entry({"admin", "vpn", "easytier", "download_easytier"}, call("download_easytier")).leaf = true
+ entry({"admin", "vpn", "easytier", "download_progress"}, call("download_progress")).leaf = true
+ entry({"admin", "vpn", "easytier", "cancel_download"}, call("cancel_download")).leaf = true
end
function act_status()
@@ -749,3 +752,389 @@ function toggle_web()
luci.http.prepare_content("application/json")
luci.http.write_json({success = true})
end
+-- 检测CPU架构
+local function detect_arch()
+ local cputype = safe_exec("uname -ms | tr ' ' '_' | tr '[A-Z]' '[a-z]'")
+ local cpucore = ""
+
+ if cputype:match("linux.*armv.*") then
+ cpucore = "arm"
+ end
+ if cputype:match("linux.*armv7.*") and safe_exec("cat /proc/cpuinfo | grep vfp") ~= "" then
+ cpucore = "armv7"
+ end
+ if cputype:match("linux.*aarch64.*") or cputype:match("linux.*armv8.*") then
+ cpucore = "aarch64"
+ end
+ if cputype:match("linux.*86.*") then
+ cpucore = "i386"
+ end
+ if cputype:match("linux.*86_64.*") then
+ cpucore = "x86_64"
+ end
+ if cputype:match("linux.*mips.*") then
+ local mipstype = safe_exec("echo -n I | hexdump -o 2>/dev/null | awk '{ print substr($2,6,1); exit}'")
+ if mipstype == "0" then
+ cpucore = "mips"
+ else
+ cpucore = "mipsel"
+ end
+ end
+
+ return cpucore
+end
+
+-- 检查依赖
+local function check_dependencies()
+ local arch = detect_arch()
+ if arch == "" then
+ return false, i18n.translate("Unable to detect CPU architecture")
+ end
+ if arch == "i386" then
+ return false, i18n.translate("No x86-32bit program available")
+ end
+ if safe_exec("which unzip") == "" then
+ return false, i18n.translate("System lacks unzip, cannot download and extract")
+ end
+ return true, arch
+end
+
+-- 获取GitHub镜像列表
+local function get_github_proxies()
+ local uci = require "luci.model.uci".cursor()
+ local proxies = {}
+
+ -- 从UCI配置读取代理列表
+ local proxy_list = uci:get("easytier", "@easytier[0]", "github_proxys")
+
+ if proxy_list then
+ if type(proxy_list) == "table" then
+ -- 多个代理
+ for _, proxy in ipairs(proxy_list) do
+ if proxy and proxy ~= "" then
+ table.insert(proxies, proxy)
+ end
+ end
+ else
+ -- 单个代理
+ if proxy_list ~= "" then
+ table.insert(proxies, proxy_list)
+ end
+ end
+ end
+
+ -- 如果没有配置代理,使用默认值
+ if #proxies == 0 then
+ proxies = {
+ "https://ghproxy.net/",
+ "https://gh-proxy.com/",
+ "https://cdn.gh-proxy.com/",
+ "https://ghfast.top/"
+ }
+ end
+
+ -- 添加不使用代理的选项(空字符串表示直连)
+ table.insert(proxies, "")
+
+ return proxies
+end
+
+-- 下载文件
+local function download_file(url, output_path, progress_callback)
+ local download_tools = {"curl", "wget"}
+
+ for _, tool in ipairs(download_tools) do
+ if safe_exec("which " .. tool) ~= "" then
+ local cmd = ""
+ if tool == "curl" then
+ cmd = string.format("curl -L -k --connect-timeout 30 --max-time 300 -o '%s' '%s'", output_path, url)
+ elseif tool == "wget" then
+ cmd = string.format("wget --no-check-certificate --timeout=30 --tries=3 -O '%s' '%s'", output_path, url)
+ end
+
+ if progress_callback then
+ progress_callback(50, i18n.translate("Downloading with") .. " " .. tool .. "...")
+ end
+
+ local result = os.execute(cmd)
+ if result == 0 and nixio.fs.access(output_path) then
+ -- 检查文件大小,确保下载完整
+ local stat = nixio.fs.stat(output_path)
+ if stat and stat.size > 3 * 1024 * 1024 then -- 至少3MB
+ return true
+ end
+ end
+ end
+ end
+
+ return false
+end
+
+function download_easytier()
+ luci.http.prepare_content("application/json")
+
+ local json = require "luci.jsonc"
+ local progress_file = "/tmp/easytier_download_progress"
+
+ -- 检查是否已有下载任务
+ local existing_progress = safe_read_file(progress_file)
+ if existing_progress then
+ local progress_data = json.parse(existing_progress)
+ if progress_data and progress_data.progress > 0 and progress_data.progress < 100 and not progress_data.error then
+ luci.http.write_json({
+ success = true,
+ progress = progress_data.progress,
+ message = i18n.translate("Download task already exists"),
+ url = progress_data.url or ""
+ })
+ return
+ end
+ end
+
+ local req_data = json.parse(luci.http.content())
+ if not req_data or not req_data.version then
+ luci.http.write_json({success = false, message = i18n.translate("Missing version parameter")})
+ return
+ end
+
+ local version = req_data.version
+
+ -- 1. 检查依赖和架构
+ local ok, arch_or_error = check_dependencies()
+ if not ok then
+ luci.http.write_json({success = false, message = arch_or_error})
+ return
+ end
+
+ local arch = arch_or_error
+ local proxies = get_github_proxies()
+ local download_dir = "/tmp/easytier_download"
+ local zip_file = download_dir .. "/easytier-linux-" .. arch .. "-" .. version .. ".zip"
+
+ -- 创建下载目录
+ os.execute("mkdir -p " .. download_dir)
+
+ -- 创建取消标志文件
+ local cancel_file = "/tmp/easytier_download_cancel"
+ os.execute("rm -f " .. cancel_file)
+
+ -- 创建进度文件标记任务开始
+ local f = io.open(progress_file, "w")
+ if f then
+ f:write(json.stringify({
+ progress = 1,
+ message = "Downloading...",
+ url = "",
+ error = false
+ }))
+ f:close()
+ end
+
+ -- 检查是否被取消
+ local function check_cancelled()
+ return nixio.fs.access(cancel_file)
+ end
+
+ -- 2. 循环尝试不同的镜像地址下载、解压、验证
+ local download_success = false
+ local download_url = ""
+ local core_file, cli_file, web_file
+
+ for _, proxy in ipairs(proxies) do
+ -- 检查是否被取消
+ if check_cancelled() then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
+ return
+ end
+
+ download_url = proxy .. "https://github.com/EasyTier/EasyTier/releases/download/" .. version .. "/easytier-linux-" .. arch .. "-" .. version .. ".zip"
+
+ -- 删除之前的失败文件
+ os.execute("rm -f " .. zip_file)
+ os.execute("rm -rf " .. download_dir .. "/extracted")
+
+ -- 下载
+ if download_file(download_url, zip_file) then
+ local stat = nixio.fs.stat(zip_file)
+ if stat and stat.size > 10240 then
+ -- 检查是否被取消
+ if check_cancelled() then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
+ return
+ end
+
+ -- 解压
+ local extract_dir = download_dir .. "/extracted"
+ os.execute("mkdir -p " .. extract_dir)
+ local unzip_result = os.execute("cd " .. extract_dir .. " && unzip -o " .. zip_file .. " >/dev/null 2>&1")
+
+ if unzip_result == 0 then
+ -- 检查是否被取消
+ if check_cancelled() then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
+ return
+ end
+
+ -- 查找解压后的目录(使用通配符)
+ local find_cmd = "find " .. extract_dir .. " -maxdepth 1 -type d -name 'easytier-linux-*' 2>/dev/null | head -1"
+ local handle = io.popen(find_cmd)
+ local subdir = handle:read("*a"):match("^%s*(.-)%s*$")
+ handle:close()
+
+ if subdir and subdir ~= "" then
+ core_file = subdir .. "/easytier-core"
+ cli_file = subdir .. "/easytier-cli"
+ web_file = subdir .. "/easytier-web-embed"
+
+ local files_ok = nixio.fs.access(core_file) and nixio.fs.access(cli_file)
+ if arch ~= "mips" and arch ~= "mipsel" then
+ files_ok = files_ok and nixio.fs.access(web_file)
+ end
+
+ if files_ok then
+ -- 检查是否被取消
+ if check_cancelled() then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
+ return
+ end
+
+ -- 先赋予执行权限,再测试程序
+ local test_files = {core_file, cli_file}
+ if arch ~= "mips" and arch ~= "mipsel" then
+ table.insert(test_files, web_file)
+ end
+
+ local test_ok = true
+ for _, file in ipairs(test_files) do
+ os.execute("chmod +x " .. file)
+ if not test_binary(file) then
+ test_ok = false
+ break
+ end
+ end
+
+ if test_ok then
+ download_success = true
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- 删除失败的文件,继续下一个地址
+ os.execute("rm -f " .. zip_file)
+ end
+
+ if not download_success then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("All download attempts failed")})
+ return
+ end
+
+ -- 检查是否被取消(替换前)
+ if check_cancelled() then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Download cancelled")})
+ return
+ end
+
+ -- 3. 从UCI获取路径并安装
+ local uci = require "luci.model.uci".cursor()
+ local easytierbin = uci:get_first("easytier", "easytier", "easytierbin") or "/usr/bin/easytier-core"
+ local easytierwebbin = uci:get_first("easytier", "easytier", "easytierwebbin") or "/usr/bin/easytier-web"
+
+ local core_dir = easytierbin:match("^(.*/)[^/]+$") or "/usr/bin/"
+
+ local programs = {
+ {src = core_file, dest = easytierbin},
+ {src = cli_file, dest = core_dir .. "easytier-cli"}
+ }
+
+ if arch ~= "mips" and arch ~= "mipsel" then
+ table.insert(programs, {src = web_file, dest = easytierwebbin})
+ end
+
+ -- 安装并验证
+ for _, prog in ipairs(programs) do
+ local result = os.execute("cp " .. prog.src .. " " .. prog.dest .. " 2>/dev/null")
+ if result ~= 0 then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Failed to install program") .. ": " .. prog.dest})
+ return
+ end
+
+ os.execute("chmod +x " .. prog.dest)
+
+ -- 再次验证安装后的程序
+ if not test_binary(prog.dest) then
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. progress_file)
+ luci.http.write_json({success = false, message = i18n.translate("Program is incomplete or architecture mismatch")})
+ return
+ end
+ end
+
+ -- 7. 清理临时文件和版本缓存
+ os.execute("rm -rf " .. download_dir)
+ os.execute("rm -f " .. cancel_file)
+ os.execute("rm -f " .. progress_file)
+ nixio.fs.remove("/tmp/easytier.tag")
+ nixio.fs.remove("/tmp/easytierweb.tag")
+
+ luci.http.write_json({
+ success = true,
+ progress = 100,
+ message = i18n.translate("Download and installation completed")
+ })
+end
+
+function download_progress()
+ luci.http.prepare_content("application/json")
+
+ local progress_file = "/tmp/easytier_download_progress"
+ local content = safe_read_file(progress_file)
+
+ if content then
+ local json = require "luci.jsonc"
+ local progress_data = json.parse(content)
+ if progress_data then
+ luci.http.write_json({
+ success = true,
+ progress = progress_data.progress or 0,
+ message = progress_data.message or "",
+ url = progress_data.url or "",
+ error = progress_data.error or false
+ })
+ return
+ end
+ end
+
+ luci.http.write_json({
+ success = true,
+ progress = 0,
+ message = i18n.translate("Preparing..."),
+ url = "",
+ error = false
+ })
+end
+function cancel_download()
+ luci.http.prepare_content("application/json")
+
+ -- 创建取消标志文件
+ os.execute("touch /tmp/easytier_download_cancel")
+
+ luci.http.write_json({success = true})
+end
diff --git a/luci-app-easytier/luasrc/model/cbi/easytier_status.lua b/luci-app-easytier/luasrc/model/cbi/easytier_status.lua
index 31f22579..abf20e48 100644
--- a/luci-app-easytier/luasrc/model/cbi/easytier_status.lua
+++ b/luci-app-easytier/luasrc/model/cbi/easytier_status.lua
@@ -5,6 +5,9 @@ m.description = translate("A simple, secure, decentralized VPN solution for intr
.. "Project URL: github.com/EasyTier/EasyTier "
.. "Official Documentation "
.. "QQ Group ")
+ .. "
"
+ .. "
"
+ .. "
"
m.pageaction = false
-- 状态卡片
diff --git a/luci-app-easytier/luasrc/view/easytier/easytier_cli.htm b/luci-app-easytier/luasrc/view/easytier/easytier_cli.htm
index 2436db1a..0dc2dca5 100644
--- a/luci-app-easytier/luasrc/view/easytier/easytier_cli.htm
+++ b/luci-app-easytier/luasrc/view/easytier/easytier_cli.htm
@@ -35,7 +35,7 @@ local uci = require 'luci.model.uci'.cursor()
border-radius: 16px;
box-shadow: 0 8px 32px var(--card-shadow);
border: 1px solid var(--border-color);
- margin: 20px 0;
+ margin: 20px 0 20px 0;
overflow: hidden;
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -403,17 +403,72 @@ local uci = require 'luci.model.uci'.cursor()
/* Contributors Section */
.et-contributors-section {
- margin-top: 24px;
+ margin: 20px 0 20px 0;
padding: 20px;
background: var(--card-bg);
- border-top: 1px solid var(--border-color);
+ border-radius: 16px;
+ box-shadow: 0 8px 32px var(--card-shadow);
+ border: 1px solid var(--border-color);
+ backdrop-filter: blur(10px);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+}
+
+.et-contributors-section:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 40px var(--card-shadow);
+}
+
+.et-contributors-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
}
.et-contributors-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
- margin-bottom: 16px;
+}
+
+.et-contributors-pagination {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.et-contributors-dots {
+ display: flex;
+ gap: 6px;
+}
+
+.et-contributors-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--border-color);
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.et-contributors-dot.active {
+ background: var(--tab-active);
+ width: 24px;
+ border-radius: 4px;
+}
+
+.et-contributors-dot:hover {
+ background: var(--tab-active);
+ opacity: 0.7;
+}
+
+.et-contributors-wrapper {
+ position: relative;
+ overflow: hidden;
+ min-height: 60px;
}
.et-contributors-container {
@@ -422,6 +477,19 @@ local uci = require 'luci.model.uci'.cursor()
gap: 8px;
justify-content: flex-start;
align-items: center;
+ opacity: 0;
+ transform: translateX(20px);
+ transition: opacity 0.8s ease, transform 0.8s ease;
+}
+
+.et-contributors-container.active {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.et-contributors-container.exit {
+ opacity: 0;
+ transform: translateX(-20px);
}
.et-contributors-container a {
@@ -442,12 +510,32 @@ local uci = require 'luci.model.uci'.cursor()
}
.et-contributors-container img:hover {
- border-color: var(--info);
+ border-color: var(--tab-active);
}
@media (max-width: 768px) {
.et-contributors-section {
padding: 16px;
+ margin: 16px 0 16px 0;
+ }
+
+ .et-contributors-section:hover {
+ transform: none;
+ }
+
+ .et-contributors-header {
+ flex-direction: column;
+ gap: 12px;
+ align-items: flex-start;
+ }
+
+ .et-contributors-pagination {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .et-contributors-container {
+ justify-content: center;
}
.et-contributors-container img {
@@ -557,10 +645,18 @@ local uci = require 'luci.model.uci'.cursor()
<%:Collecting data...%>
-
-
-
+
+
+
+
@@ -700,6 +796,9 @@ var translations = {
// VPN Portal特殊处理
"ERROR: Wireguard VPN Portal Not Started": "<%:ERROR: Wireguard VPN Portal Not Started%>",
+ // 错误信息翻译
+ "no running instances found": "<%:no running instances found%>",
+
// 统计指标翻译
"compression_bytes_rx_before": "<%:Compression RX Before%>",
"compression_bytes_rx_after": "<%:Compression RX After%>",
@@ -808,6 +907,11 @@ function parseTable(content, tabName) {
return '' + escapeHtml(tr('ERROR: Wireguard VPN Portal Not Started')) + '
';
}
+ // 未找到运行实例错误处理
+ if (content.indexOf('no running instances found') !== -1) {
+ return '' + escapeHtml(tr('no running instances found')) + '
';
+ }
+
// Peer Center错误处理
if (content.indexOf('Error: Invalid service name: "PeerCenterRpc"') !== -1) {
return '<%:Peer Center service not available. This feature requires multiple nodes in the network.%>
';
@@ -1104,56 +1208,215 @@ document.addEventListener('DOMContentLoaded', function() {
refreshConnInfo();
});
-// 加载贡献者头像 (ES5)
-function loadContributors() {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', 'https://api.github.com/repos/EasyTier/EasyTier/contributors', true);
- xhr.timeout = 5000; // 5秒超时
+// 贡献者轮播管理 (ES5)
+var contributorsCarousel = {
+ allContributors: [],
+ currentPage: 0,
+ pageSize: 30,
+ totalPages: 0,
+ autoPlayTimer: null,
+ autoPlayInterval: 3000,
+ container: null,
+ section: null,
+ pageInfo: null,
+ dotsContainer: null,
- xhr.onload = function() {
- if (xhr.status === 200) {
- try {
- var data = JSON.parse(xhr.responseText);
- var container = document.getElementById('contributors');
- var section = document.getElementById('contributors-section');
-
- if (!container || !section || !data.length) return;
-
- container.innerHTML = '';
-
- for (var i = 0; i < data.length; i++) {
- var user = data[i];
- var a = document.createElement('a');
- a.href = user.html_url;
- a.target = '_blank';
- a.rel = 'noopener noreferrer';
-
- var img = document.createElement('img');
- img.src = user.avatar_url;
- img.alt = user.login;
- img.title = user.login + ' (' + user.contributions + ' contributions)';
-
- a.appendChild(img);
- container.appendChild(a);
+ init: function() {
+ this.container = document.getElementById('contributors');
+ this.section = document.getElementById('contributors-section');
+ this.pageInfo = document.getElementById('contributors-page-info');
+ this.dotsContainer = document.getElementById('contributors-dots');
+ this.setupHoverPause();
+ this.loadData();
+ },
+
+ setupHoverPause: function() {
+ var self = this;
+ if (!this.section) return;
+
+ this.section.addEventListener('mouseenter', function() {
+ self.stopAutoPlay();
+ });
+
+ this.section.addEventListener('mouseleave', function() {
+ self.startAutoPlay();
+ });
+
+ // 移动端滑动支持
+ this.setupTouchSwipe();
+ },
+
+ setupTouchSwipe: function() {
+ var self = this;
+ if (!this.container) return;
+
+ var startX = 0;
+ var startY = 0;
+ var threshold = 50; // 滑动阈值
+
+ this.container.addEventListener('touchstart', function(e) {
+ self.stopAutoPlay();
+ startX = e.touches[0].clientX;
+ startY = e.touches[0].clientY;
+ });
+
+ this.container.addEventListener('touchend', function(e) {
+ var endX = e.changedTouches[0].clientX;
+ var endY = e.changedTouches[0].clientY;
+ var deltaX = endX - startX;
+ var deltaY = endY - startY;
+
+ // 确保是水平滑动
+ if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > threshold) {
+ if (deltaX > 0) {
+ // 右滑 - 上一页
+ var prevPage = (self.currentPage - 1 + self.totalPages) % self.totalPages;
+ self.showPage(prevPage);
+ } else {
+ // 左滑 - 下一页
+ self.nextPage();
}
-
- // 成功加载后显示卡片
- section.style.display = 'block';
- } catch (e) {
- // 失败时保持隐藏
}
+
+ self.startAutoPlay();
+ });
+ },
+
+ loadData: function() {
+ var self = this;
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', 'https://api.github.com/repos/EasyTier/EasyTier/contributors?per_page=100', true);
+ xhr.timeout = 10000;
+
+ xhr.onload = function() {
+ if (xhr.status === 200) {
+ try {
+ var data = JSON.parse(xhr.responseText);
+ if (!data || data.length === 0) return;
+
+ self.allContributors = data;
+ self.totalPages = Math.ceil(data.length / self.pageSize);
+ self.renderDots();
+ self.showPage(0);
+ self.section.style.display = 'block';
+ self.startAutoPlay();
+ } catch (e) {
+ console.error('Failed to parse contributors:', e);
+ }
+ }
+ };
+
+ xhr.onerror = function() {
+ console.error('Failed to load contributors');
+ };
+
+ xhr.ontimeout = function() {
+ console.error('Contributors request timeout');
+ };
+
+ xhr.send();
+ },
+
+ renderDots: function() {
+ if (!this.dotsContainer) return;
+ this.dotsContainer.innerHTML = '';
+
+ for (var i = 0; i < this.totalPages; i++) {
+ var dot = document.createElement('div');
+ dot.className = 'et-contributors-dot';
+ dot.setAttribute('data-page', i);
+
+ var self = this;
+ (function(page) {
+ dot.onclick = function() {
+ self.stopAutoPlay();
+ self.showPage(page);
+ self.startAutoPlay();
+ };
+ })(i);
+
+ this.dotsContainer.appendChild(dot);
}
- };
+ },
- xhr.onerror = function() {
- // 失败时保持隐藏
- };
+ showPage: function(pageIndex) {
+ var self = this;
+ if (pageIndex < 0 || pageIndex >= this.totalPages) return;
+
+ // 退出动画
+ this.container.classList.remove('active');
+ this.container.classList.add('exit');
+
+ setTimeout(function() {
+ self.currentPage = pageIndex;
+ var start = pageIndex * self.pageSize;
+ var end = Math.min(start + self.pageSize, self.allContributors.length);
+ var pageData = self.allContributors.slice(start, end);
+
+ // 更新内容
+ self.container.innerHTML = '';
+ for (var i = 0; i < pageData.length; i++) {
+ var user = pageData[i];
+ var a = document.createElement('a');
+ a.href = user.html_url;
+ a.target = '_blank';
+ a.rel = 'noopener noreferrer';
+
+ var img = document.createElement('img');
+ img.src = user.avatar_url;
+ img.alt = user.login;
+ img.title = user.login + ' (' + user.contributions + ' contributions)';
+
+ a.appendChild(img);
+ self.container.appendChild(a);
+ }
+
+ // 更新分页信息
+ if (self.pageInfo) {
+ self.pageInfo.textContent = (pageIndex + 1) + ' / ' + self.totalPages;
+ }
+
+ // 更新指示器
+ var dots = self.dotsContainer.querySelectorAll('.et-contributors-dot');
+ for (var j = 0; j < dots.length; j++) {
+ dots[j].classList.remove('active');
+ }
+ if (dots[pageIndex]) {
+ dots[pageIndex].classList.add('active');
+ }
+
+ // 进入动画
+ self.container.classList.remove('exit');
+ setTimeout(function() {
+ self.container.classList.add('active');
+ }, 50);
+ }, 600);
+ },
- xhr.ontimeout = function() {
- // 超时时保持隐藏
- };
+ nextPage: function() {
+ var next = (this.currentPage + 1) % this.totalPages;
+ this.showPage(next);
+ },
- xhr.send();
+ startAutoPlay: function() {
+ var self = this;
+ this.stopAutoPlay();
+ this.autoPlayTimer = setInterval(function() {
+ self.nextPage();
+ }, this.autoPlayInterval);
+ },
+
+ stopAutoPlay: function() {
+ if (this.autoPlayTimer) {
+ clearInterval(this.autoPlayTimer);
+ this.autoPlayTimer = null;
+ }
+ }
+};
+
+// 加载贡献者
+function loadContributors() {
+ contributorsCarousel.init();
}
// 自动刷新
diff --git a/luci-app-easytier/luasrc/view/easytier/easytier_log.htm b/luci-app-easytier/luasrc/view/easytier/easytier_log.htm
index 0751b67b..5e6761ad 100644
--- a/luci-app-easytier/luasrc/view/easytier/easytier_log.htm
+++ b/luci-app-easytier/luasrc/view/easytier/easytier_log.htm
@@ -293,6 +293,9 @@ local translate = i18n.translate
+
+
+
-