Files
op-packages/luci-app-openclash/luasrc/view/openclash/update.htm
T

1596 lines
74 KiB
HTML

<%
local fs = require "luci.openclash"
local plugin_version = fs.oc_version()
local version_tab = (self and self.version_tab) or false
%>
<script src="/luci-static/resources/openclash/js/common.js?v=<%=plugin_version%>"></script>
<script src="/luci-static/resources/openclash/js/oc-icons.js?v=<%=plugin_version%>"></script>
<div class="oc">
<div id="selectPopup" class="select-popup<%= version_tab and ' select-popup-tab' or ' hidden' %>">
<div class="select-popup-header">
<div class="config-editor-title">
<%:Check Update%>
</div>
<div class="config-editor-actions">
<button type="button" class="icon-btn" onclick="refreshAllData()" title="<%:Refresh%>">
<svg width="14" height="14"><use href="#oc-icon-refresh"/></svg>
</button>
<button type="button" class="icon-btn<%= version_tab and ' oc-hidden' or '' %>" onclick="closeSelectPopup()" title="<%:Close%>">
<svg width="14" height="14"><use href="#oc-icon-close"/></svg>
</button>
</div>
</div>
<div class="select-config-section">
<div class="config-grid">
<div class="config-item">
<div class="oc-option-title">
<span class="oc-option-text"><%:CPU Arch%></span>
</div>
<span class="ver-value" id="CPU_ARCH_VALUE"><%:Collecting data...%></span>
</div>
<div class="config-item">
<div class="oc-option-title"><span class="oc-option-text"><%:Compiled Version%><b> *</b></span></div>
<div class="form-select-wrapper">
<select class="select-class oc-arch-select" id="CORE_VERSION" onchange="onArchChange()">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
</div>
</div>
<div class="config-item">
<div class="oc-option-title"><span class="oc-option-text"><%:Release Branch%></span></div>
<div class="form-select-wrapper">
<select class="select-class" id="RELEASE_BRANCH" onchange="onBranchChange()">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
</div>
</div>
<div class="config-item">
<div class="oc-option-title">
<span class="oc-option-text"><%:Smart Core%></span>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" class="help-link" title="<%:View core infos that support smart group%>">
<svg width="12" height="12"><use href="#oc-icon-help"/></svg>
</a>
</div>
<div class="form-select-wrapper">
<select class="select-class" id="SMART_ENABLE" onchange="onSmartChange()">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enabled%></option>
</select>
</div>
</div>
</div>
<div class="version-card">
<div class="version-row">
<div class="config-item">
<span class="oc-option-title"><span class="oc-option-text"><%:Plugin Version%></span></span>
<span class="ver-value" id="installed-plugin">--</span>
</div>
<div class="config-item">
<span class="oc-option-title"><span class="oc-option-text"><%:Select Version%></span></span>
<div class="form-select-wrapper">
<select class="select-class" id="select-plugin-ver" onchange="forceRefresh=true;refreshAddrInfo({cancel:true,user:true})">
<option value="__latest__"><%:Latest%></option>
</select>
</div>
</div>
<div class="config-item">
<span class="oc-option-title"><span class="oc-option-text"><%:Core Version%></span></span>
<span class="ver-value" id="installed-core">--</span>
</div>
<div class="config-item">
<span class="oc-option-title"><span class="oc-option-text"><%:Select Version%></span></span>
<div class="form-select-wrapper">
<select class="select-class" id="select-core-ver" onchange="forceRefresh=true;refreshAddrInfo({cancel:true,user:true})">
<option value="__latest__"><%:Latest%></option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="ver-hint" id="version-hint"></div>
<div class="select-popup-body" id="addr-list-body">
<div class="addr-list-scroll">
<div class="select-option addr-header">
<span class="addr-header-label"><span class="addr-header-index">#</span></span>
<span class="addr-header-label"><span class="addr-header-icon"><svg><use href="#oc-icon-link-tabler"/></svg></span><span><%:Address%></span></span>
<span class="addr-header-label"><span class="addr-header-icon"><svg><use href="#oc-icon-speedtest"/></svg></span><span><%:Latency%></span></span>
<span class="addr-header-label"><span class="addr-header-icon"><svg><use href="#oc-icon-package"/></svg></span><span><%:Plugin%></span></span>
<span class="addr-header-label"><span class="addr-header-icon"><svg><use href="#oc-icon-cpu"/></svg></span><span><%:Core%></span>
<div class="help-link" title="<%:Core path:%>/etc/openclash/core/clash_meta">
<svg width="12" height="12"><use href="#oc-icon-help"/></svg>
</div>
</span>
<span class="addr-header-label"><span class="addr-header-icon"><svg><use href="#oc-icon-zap"/></svg></span><span><%:Proxy%></span>
<div class="help-link" title="<%:Proxy The GitHub Address In The Config And OpenClash With Proxy(CDN) To Prevent File Download Failed.%>">
<svg width="12" height="12"><use href="#oc-icon-help"/></svg>
</div>
</span>
</div>
<div class="select-option addr-row" data-value="https://raw.githubusercontent.com/">
<span class="addr-row-index"></span>
<span class="addr-row-url">https://raw.githubusercontent.com/</span>
<span class="addr-status">--</span>
<span class="ver-text ver-plugin">--</span>
<span class="ver-text ver-core">--</span>
<span class="addr-proxy-static"><%:RAW%></span>
</div>
<div class="select-option addr-row" data-value="https://fastly.jsdelivr.net/">
<span class="addr-row-index"></span>
<span class="addr-row-url">https://fastly.jsdelivr.net/</span>
<span class="addr-status">--</span>
<span class="ver-text ver-plugin">--</span>
<span class="ver-text ver-core">--</span>
</div>
<div class="select-option addr-row" data-value="https://testingcf.jsdelivr.net/">
<span class="addr-row-index"></span>
<span class="addr-row-url">https://testingcf.jsdelivr.net/</span>
<span class="addr-status">--</span>
<span class="ver-text ver-plugin">--</span>
<span class="ver-text ver-core">--</span>
</div>
<div class="select-option addr-row" data-value="https://cdn.jsdelivr.net/">
<span class="addr-row-index"></span>
<span class="addr-row-url">https://cdn.jsdelivr.net/</span>
<span class="addr-status">--</span>
<span class="ver-text ver-plugin">--</span>
<span class="ver-text ver-core">--</span>
</div>
<div class="select-option" data-value="custom" id="custom-addr-option">
<span><%:Custom Your CDN URL%></span>
<span class="oc-flex-center" style="height:100%;">
<svg width="18" height="18"><use href="#oc-icon-chevron-down"/></svg>
</span>
</div>
<input type="text" id="customOptionInput" class="custom-option-input oc-hidden" value="" placeholder="<%:Type CDN URL, Format Like%> https://ghfast.top/">
<div class="select-option oc-hidden" id="addCustomOption"><%:Add%></div>
</div>
<div class="addr-list-loading oc-hidden" id="addr-list-loading">
<div class="addr-list-spinner"></div>
</div>
</div>
<div class="select-popup-footer" id="select-popup-footer">
<div class="form-select-wrapper">
<select class="select-class" id="BACKUP_SELECT">
<option value="backup"><%:Backup File%></option>
<option value="backup_ex_core"><%:Backup Exclude Cores%></option>
<option value="backup_only_core"><%:Backup Core%></option>
<option value="backup_only_config"><%:Backup Config%></option>
<option value="backup_only_rule"><%:Backup Rule Provider%></option>
<option value="backup_only_proxy"><%:Backup Proxy Provider%></option>
</select>
</div>
<button type="button" class="footer-btn" onclick="return backup_file(this)">
<svg width="14" height="14"><use href="#oc-icon-download"/></svg>
<span><%:Backup File%></span>
</button>
<button type="button" class="footer-btn" onclick="return restore_config(this)">
<svg width="14" height="14"><use href="#oc-icon-history"/></svg>
<span><%:Restore Default%></span>
</button>
<button type="button" class="footer-btn" onclick="return remove_all_core(this)">
<svg width="14" height="14"><use href="#oc-icon-trash"/></svg>
<span><%:Remove Core%></span>
</button>
</div>
<div class="select-popup-status-bar">
<div class="select-popup-status" id="addr-list-status"><%:Fetching data...%></div>
<div class="select-popup-tip"><%:Click%> <b><%:link%></b> <%:to update all, click%> <b><%:version%></b> <%:to update individually%></div>
</div>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
var core_version = document.getElementById('CORE_VERSION');
var release_branch = document.getElementById('RELEASE_BRANCH');
var smart_enable = document.getElementById('SMART_ENABLE');
var versionTabMode = document.getElementById('selectPopup').classList.contains('select-popup-tab');
var currentDownloadType = 'one_key';
var refreshTimer = null;
var versionRetryTimer = null;
var retryTimer = null;
var addrInfoBusy = false;
var addrInfoXHR = null;
var addrInfoError = false;
var addrDataReceived = false;
var reqUpdateDone = false;
var reqVersionHistoryDone = false;
var reqAddrInfoDone = false;
var versionHistoryBusy = false;
var versionHistoryXHR = null;
var hintTimer = null;
var defaultTipTimer = null;
var logStreamXHR = null;
var logStreamReconnect = null;
var logStreamActive = false;
var logRetryCount = 0;
var lastLogLineCount = null;
var logMaxWaitTimer = null;
var fetchedBranch = null;
var _isOix = false;
var _pkgType = 'opkg';
var forceRefresh = false;
var infoCache = {};
var currentProxyAddr = '';
window.levelTranslations = {
'info': '<%:Info%>',
'warning': '<%:Warning%>',
'error': '<%:Error%>',
'debug': '<%:Debug%>',
'tip': '<%:Tip%>',
'watchdog': '<%:Watchdog%>',
'fatal': '<%:Fatal%>'
};
function ocRandomInterval(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function isValidURL(str) {
var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.&#126;+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.&#126;+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?'+'\/$','i');
return !!pattern.test(str);
}
function showHint(message, duration) {
var hint = document.getElementById('version-hint');
if (defaultTipTimer) { clearTimeout(defaultTipTimer); defaultTipTimer = null; }
if (hintTimer) { clearTimeout(hintTimer); hintTimer = null; }
hint.innerHTML = '<svg><use href="#oc-icon-alert-circle"/></svg><span>' + message + '</span>';
hint.classList.add('show');
hint.classList.remove('note');
if (duration === 0) return;
hintTimer = setTimeout(function() {
showDefaultNote();
hintTimer = null;
}, duration || 5000);
}
function showLogLine(html) {
var hint = document.getElementById('version-hint');
if (defaultTipTimer) { clearTimeout(defaultTipTimer); defaultTipTimer = null; }
if (hintTimer) { clearTimeout(hintTimer); hintTimer = null; }
hint.innerHTML = '<svg><use href="#oc-icon-alert-circle"/></svg><span>' + html + '</span>';
hint.classList.add('show');
hint.classList.add('note');
}
function showDefaultNote() {
var hint = document.getElementById('version-hint');
if (hintTimer) { clearTimeout(hintTimer); hintTimer = null; }
if (defaultTipTimer) { clearTimeout(defaultTipTimer); defaultTipTimer = null; }
var tips = [
'<%:Note: if the update fails, you can manually download and upload%>',
'<%:Note: the client may not support update, because the firmware with squashfs format will not release flash space after updating%>'
];
var tip = tips[Math.floor(Math.random() * tips.length)];
hint.innerHTML = '<svg><use href="#oc-icon-alert-circle"/></svg><span>' + tip + '</span>';
hint.classList.add('show');
hint.classList.add('note');
defaultTipTimer = setTimeout(function() {
defaultTipTimer = null;
showDefaultNote();
}, 5000);
}
function classifyAddr(url) {
if (!url || url === '') return 'raw';
if (/raw\.githubusercontent\.com/i.test(url)) return 'raw';
if (/jsdelivr|fastly|testingcf/i.test(url)) return 'jsdelivr';
if (/dl\.dler\.io/i.test(url)) return 'dler';
return 'proxy';
}
function buildDownloadURL(addr, type, branch, version, sha, arch) {
var isJsDelivr = classifyAddr(addr) === 'jsdelivr';
var filename, pathPrefix;
if (type === 'core' && _isOix) {
if (!version) return '';
filename = 'mihomo-' + arch + '-' + version + '.gz';
var oixURL = 'https://github.com/vernesong/mihomo-oix/releases/download/Pre-Alpha/' + filename;
var proxyURL = 'https://dl.dler.io/mihomo-oix/' + filename + '?tag=Pre-Alpha';
var ctype = classifyAddr(addr);
if (ctype === 'dler') {
return proxyURL;
}
if (addr && addr !== '' && ctype !== 'raw' && ctype !== 'jsdelivr') {
return addr + oixURL;
}
return proxyURL;
}
if (type === 'plugin') {
var verNoV = version.replace(/^v/, '');
if (_pkgType === 'apk') {
filename = 'luci-app-openclash-' + verNoV + '.apk';
} else {
filename = 'luci-app-openclash_' + verNoV + '_all.ipk';
}
pathPrefix = 'package/' + branch;
} else {
filename = 'clash-' + arch + '.tar.gz';
var coreSubPath = (smart_enable.value === '1') ? 'smart' : 'meta';
pathPrefix = 'core/' + branch + '/' + coreSubPath;
}
if (sha) {
var refPath = (type === 'plugin') ? branch : branch + '/' + coreSubPath;
if (isJsDelivr)
return addr + 'gh/vernesong/OpenClash@' + sha + '/' + refPath + '/' + filename;
else if (classifyAddr(addr) === 'raw')
return 'https://raw.githubusercontent.com/vernesong/OpenClash/' + sha + '/' + refPath + '/' + filename;
else
return addr + 'https://raw.githubusercontent.com/vernesong/OpenClash/' + sha + '/' + refPath + '/' + filename;
} else {
if (isJsDelivr)
return addr + 'gh/vernesong/OpenClash@' + pathPrefix + '/' + filename;
else if (classifyAddr(addr) === 'raw')
return 'https://raw.githubusercontent.com/vernesong/OpenClash/' + pathPrefix + '/' + filename;
else
return addr + 'https://raw.githubusercontent.com/vernesong/OpenClash/' + pathPrefix + '/' + filename;
}
}
function findSHAbyVersion(selectEl, targetVer) {
if (!targetVer || !selectEl) return '';
for (var i = 0; i < selectEl.options.length; i++) {
if (selectEl.options[i].getAttribute('data-ver') === targetVer) {
var v = selectEl.options[i].value;
return (v && v !== '__latest__') ? v : '';
}
}
return '';
}
function escAttr(s) {
return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
}
function makeVerCell(addrUrl, type, version) {
var branch = fetchedBranch || release_branch.value || 'dev';
var arch = core_version.value;
var selectEl = type === 'plugin'
? document.getElementById('select-plugin-ver')
: document.getElementById('select-core-ver');
var verText = version;
var isLatest = false;
if (!verText) {
if (type === 'plugin') {
return null;
}
var selectedOption = selectEl.options[selectEl.selectedIndex];
isLatest = !selectedOption || selectedOption.value === '__latest__';
if (!isLatest) {
verText = selectedOption.getAttribute('data-ver');
}
if (!verText) return null;
}
var useVersion = (verText === 'Latest') ? '' : verText;
var sha = useVersion ? findSHAbyVersion(selectEl, useVersion) : '';
var dlUrl = buildDownloadURL(addrUrl, type, branch, useVersion, sha, arch);
var verLink = '<a href="javascript:void(0)" class="ver-link" data-type="' + type + '" data-ver="' + escAttr(useVersion) + '" data-dl="' + escAttr(dlUrl) + '" title="<%:Click to Install%>">' + verText + '</a>';
if (!dlUrl) {
return verLink;
}
return verLink +
'<a href="' + escAttr(dlUrl) + '" target="_blank" rel="noopener" class="dl-btn" title="<%:Download%>"><svg width="11" height="11"><use href="#oc-icon-download"/></svg></a>';
}
function updateAddrRow(row, addrUrl, info) {
var smart = smart_enable.value || '0';
var latencyEl = row.querySelector('.addr-status');
var verSpans = row.querySelectorAll('.ver-text');
var pluginEl = verSpans[0];
var coreEl = verSpans[1];
if (info.latency != null && info.latency > 0) {
var ms = parseInt(info.latency);
latencyEl.textContent = ms + ' ms';
latencyEl.className = 'addr-status ' + (ms <= 500 ? 'fast' : ms <= 1000 ? 'medium' : 'slow');
latencyEl.title = '';
} else if (info.latency === -1) {
latencyEl.textContent = '<%:Access Timed Out%>';
latencyEl.className = 'addr-status err';
latencyEl.title = '';
} else if (info.latency === -2) {
latencyEl.textContent = '<%:Access Denied%>';
latencyEl.className = 'addr-status err';
latencyEl.title = '';
} else {
latencyEl.textContent = '--';
latencyEl.className = 'addr-status';
latencyEl.title = '';
}
if (pluginEl) {
if (info.plugin_ver) {
pluginEl.innerHTML = makeVerCell(addrUrl, 'plugin', info.plugin_ver);
pluginEl.classList.add('has-data');
pluginEl.classList.remove('err');
} else {
var selLink = makeVerCell(addrUrl, 'plugin');
if (selLink) {
pluginEl.innerHTML = selLink;
pluginEl.classList.add('has-data');
pluginEl.classList.remove('err');
} else if (info.latency === -1 || info.latency === -2) {
pluginEl.textContent = info.latency === -1 ? '<%:Access Timed Out%>' : '<%:Access Denied%>';
pluginEl.classList.add('has-data', 'err');
} else {
pluginEl.textContent = '--';
pluginEl.classList.remove('has-data', 'err');
}
}
}
if (coreEl) {
var coreVer = (smart === '1' && info.core_smart_ver) ? info.core_smart_ver : info.core_meta_ver;
if (coreVer) {
coreEl.innerHTML = makeVerCell(addrUrl, 'core', coreVer);
coreEl.classList.add('has-data');
coreEl.classList.remove('err');
} else if (info.latency === -1 || info.latency === -2 || info.latency === -3 || info.latency == null) {
if (info.latency === -1) {
coreEl.textContent = '<%:Access Timed Out%>';
coreEl.classList.add('has-data', 'err');
} else if (info.latency === -2) {
coreEl.textContent = '<%:Access Denied%>';
coreEl.classList.add('has-data', 'err');
} else {
coreEl.textContent = '--';
coreEl.classList.remove('has-data', 'err');
}
} else {
var selLink = makeVerCell(addrUrl, 'core');
if (selLink) {
coreEl.innerHTML = selLink;
coreEl.classList.add('has-data');
coreEl.classList.remove('err');
} else if (info.core_error) {
coreEl.textContent = info.core_error === 'timeout' ? '<%:Access Timed Out%>' : '<%:Access Denied%>';
coreEl.classList.add('has-data', 'err');
} else {
coreEl.textContent = '<%:Access Timed Out%>';
coreEl.classList.add('has-data', 'err');
}
}
}
infoCache[addrUrl] = {
plugin_ver: info.plugin_ver || null,
core_meta_ver: info.core_meta_ver || null,
core_smart_ver: info.core_smart_ver || null,
latency: info.latency,
core_error: info.core_error || null
};
}
function buildAddrRows(st) {
var listBody = document.querySelector('#addr-list-body .addr-list-scroll') || document.getElementById('addr-list-body');
var customOpt = document.getElementById('custom-addr-option');
var existing = {};
listBody.querySelectorAll('.addr-row').forEach(function(row) {
var v = row.dataset.value;
if (v) existing[v] = true;
});
function addRow(val) {
if (!val || existing[val]) return;
var div = document.createElement('div');
div.className = 'select-option addr-row';
div.dataset.value = val;
div.innerHTML = '<span class="addr-row-index"></span><span class="addr-row-url">' + val + '</span><span class="addr-status">--</span><span class="ver-text ver-plugin">--</span><span class="ver-text ver-core">--</span>';
listBody.insertBefore(div, customOpt);
existing[val] = true;
}
var mod = st.github_address_mod;
if (mod && mod !== '0') addRow(mod);
(st.cdn_list || []).forEach(addRow);
}
function renderProxyStates() {
document.querySelectorAll('#addr-list-body .addr-row').forEach(function(row) {
if (classifyAddr(row.dataset.value) === 'raw') return;
var el = row.querySelector('.addr-proxy');
if (!el) {
el = document.createElement('span');
el.className = 'addr-proxy';
row.appendChild(el);
}
var on = (row.dataset.value === currentProxyAddr);
el.textContent = on ? '<%:Enabled%>' : '<%:Disabled%>';
el.className = 'addr-proxy ' + (on ? 'on' : 'off');
});
}
function toggleProxy(row) {
var val = row.dataset.value;
if (!val) return;
var newVal = (val === currentProxyAddr) ? '0' : val;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_github_address_mod")%>', {value: newVal}, function(x) {
if (x && x.status == 200) {
currentProxyAddr = newVal;
renderProxyStates();
} else {
showHint('<%:Failed to save%>');
}
});
}
function fetchUpdateInfo(opts) {
core_version.disabled = true;
release_branch.disabled = true;
smart_enable.disabled = true;
document.getElementById('select-plugin-ver').disabled = true;
document.getElementById('select-core-ver').disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update")%>', null, function(x, st) {
if (x && x.status == 200) {
if (st.corever && st.corever != '0') core_version.value = st.corever; else core_version.value = '0';
if (st.release_branch) release_branch.value = st.release_branch; else release_branch.value = 'master';
if (st.smart_enable) smart_enable.value = st.smart_enable; else smart_enable.value = '0';
_isOix = !!(st.oix_core);
_pkgType = st.pkg_type || 'opkg';
document.getElementById('installed-plugin').textContent = (st.opcv && st.opcv != '0') ? st.opcv : '--';
document.getElementById('installed-core').textContent = (st.coremetacv && st.coremetacv != '0') ? st.coremetacv : '--';
var cpuArchEl = document.getElementById('CPU_ARCH_VALUE');
if (cpuArchEl) {
cpuArchEl.textContent = (st.coremodel && st.coremodel != '') ? st.coremodel : '--';
}
currentProxyAddr = (st.github_address_mod || '0');
buildAddrRows(st);
renderProxyStates();
}
core_version.disabled = false;
release_branch.disabled = false;
smart_enable.disabled = false;
fetchedBranch = release_branch.value;
updateDataStatus({ update: true });
fetchVersionHistory(!!(opts && opts.force), {cancel: true});
refreshAddrInfo({cancel: true, user: true});
});
}
function initAllData() {
fetchUpdateInfo();
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
refreshTimer = setInterval(refreshAddrInfo, ocRandomInterval(10000, 20000));
}
function fetchVersionHistory(force, opts) {
if (document.getElementById('selectPopup').classList.contains('hidden')) return;
if (versionHistoryBusy) {
if (!(opts && opts.cancel)) return;
if (versionHistoryXHR) { try { versionHistoryXHR.abort(); } catch(e) {} versionHistoryXHR = null; }
versionHistoryBusy = false;
}
versionHistoryBusy = true;
updateDataStatus({ version: false });
var branch = fetchedBranch || release_branch.value || 'dev';
var pluginSelect = document.getElementById('select-plugin-ver');
var coreSelect = document.getElementById('select-core-ver');
var hint = document.getElementById('version-hint');
var params = { branch: branch };
var smart = smart_enable.value || '0';
var appendHistoryEntry = function(entry) {
if (entry.type === 'plugin' && entry.version) {
pluginSelect.innerHTML += '<option value="' + entry.sha + '" data-ver="' + entry.version + '">' + entry.version + ' (<%:Date%>: ' + (entry.date||'').substring(0,10) + ')</option>';
} else if (!_isOix && entry.type === 'core_meta' && smart !== '1' && entry.version) {
coreSelect.innerHTML += '<option value="' + entry.sha + '" data-ver="' + entry.version + '">' + entry.version + ' (<%:Date%>: ' + (entry.date||'').substring(0,10) + ')</option>';
} else if (!_isOix && entry.type === 'core_smart' && smart === '1' && entry.version) {
coreSelect.innerHTML += '<option value="' + entry.sha + '" data-ver="' + entry.version + '">' + entry.version + ' (<%:Date%>: ' + (entry.date||'').substring(0,10) + ')</option>';
}
};
if (force) params.force = '1';
pluginSelect.disabled = true;
coreSelect.disabled = true;
pluginSelect.innerHTML = '<option value="__latest__"><%:Latest%></option>';
coreSelect.innerHTML = '<option value="__latest__"><%:Latest%></option>';
var url = '<%=luci.dispatcher.build_url("admin", "services", "openclash", "version_history")%>?' +
Object.keys(params).map(function(k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');
var xhr = new XMLHttpRequest();
xhr.timeout = 0;
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
var lastLen = 0;
var pendingBuf = '';
xhr.onprogress = function() {
if (xhr.status != 200) return;
var text = xhr.responseText;
if (text.length <= lastLen) return;
var newText = text.substring(lastLen);
lastLen = text.length;
pendingBuf += newText;
var lines = pendingBuf.split('\n');
pendingBuf = lines.pop();
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
try {
var entry = JSON.parse(line);
if (entry.complete) {
if (entry.error) {
showHint('<%:Failed to get version history%>');
pluginSelect.disabled = true;
coreSelect.disabled = true;
if (!versionRetryTimer) {
versionRetryTimer = setInterval(fetchVersionHistory, 10000);
}
} else {
pluginSelect.disabled = false;
coreSelect.disabled = false;
if (versionRetryTimer) { clearInterval(versionRetryTimer); versionRetryTimer = null; }
}
continue;
}
appendHistoryEntry(entry);
} catch(e) {}
}
};
xhr.onload = function() {
if (xhr.status != 200 || !xhr.responseText) {
showHint('<%:Failed to get version history%>');
if (!versionRetryTimer) {
versionRetryTimer = setInterval(fetchVersionHistory, 10000);
}
return;
}
var text = xhr.responseText;
if (text.length > lastLen) {
var newText = text.substring(lastLen);
lastLen = text.length;
pendingBuf += newText;
var lines = pendingBuf.split('\n');
pendingBuf = lines.pop();
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
try {
var entry = JSON.parse(line);
if (entry.complete) {
if (entry.error) {
showHint('<%:Failed to get version history%>');
pluginSelect.disabled = true;
coreSelect.disabled = true;
if (!versionRetryTimer) {
versionRetryTimer = setInterval(fetchVersionHistory, 10000);
}
} else {
pluginSelect.disabled = false;
coreSelect.disabled = false;
if (versionRetryTimer) { clearInterval(versionRetryTimer); versionRetryTimer = null; }
}
continue;
}
appendHistoryEntry(entry);
} catch(e) {}
}
}
if (pendingBuf && pendingBuf.trim()) {
var line = pendingBuf.trim();
pendingBuf = '';
try {
var entry = JSON.parse(line);
if (entry.complete) {
if (entry.error) {
showHint('<%:Failed to get version history%>');
pluginSelect.disabled = true;
coreSelect.disabled = true;
if (!versionRetryTimer) {
versionRetryTimer = setInterval(fetchVersionHistory, 10000);
}
} else {
pluginSelect.disabled = false;
coreSelect.disabled = false;
if (versionRetryTimer) { clearInterval(versionRetryTimer); versionRetryTimer = null; }
}
} else {
appendHistoryEntry(entry);
}
} catch(e) {}
}
};
xhr.onerror = function() {
showHint('<%:Failed to get version history%>');
if (!versionRetryTimer) {
versionRetryTimer = setInterval(fetchVersionHistory, 10000);
}
};
xhr.onloadend = function() {
if (versionHistoryXHR === xhr) {
versionHistoryXHR = null;
versionHistoryBusy = false;
}
updateDataStatus({ version: true });
};
xhr.send();
versionHistoryXHR = xhr;
}
function saveOptions() {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {
core_ver: core_version.value,
release_branch: release_branch.value,
smart_enable: smart_enable.value
},function() {});
}
function onArchChange() {
saveOptions();
document.querySelectorAll('#addr-list-body .addr-row').forEach(function(row) {
var addrUrl = row.dataset.value;
if (!addrUrl || addrUrl === 'custom') return;
var info = infoCache[addrUrl];
if (info) updateAddrRow(row, addrUrl, info);
});
}
function onBranchChange() {
fetchedBranch = release_branch.value;
if (versionRetryTimer) { clearInterval(versionRetryTimer); versionRetryTimer = null; }
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
saveOptions();
resetDataStatus(true);
fetchVersionHistory(false, {cancel: true});
forceRefresh = true;
refreshAddrInfo({cancel: true, user: true});
}
function onSmartChange() {
saveOptions();
resetDataStatus(true);
fetchVersionHistory(false, {cancel: true});
forceRefresh = true;
refreshAddrInfo({cancel: true, user: true});
}
function setAddrListLoading(loading) {
var overlay = document.getElementById('addr-list-loading');
if (overlay) overlay.classList.toggle('oc-hidden', !loading);
}
function clearAddrRows() {
document.querySelectorAll('#addr-list-body .addr-row').forEach(function(row) {
var latencyEl = row.querySelector('.addr-status');
var verSpans = row.querySelectorAll('.ver-text');
if (latencyEl) {
latencyEl.textContent = '--';
latencyEl.className = 'addr-status';
}
verSpans.forEach(function(el) {
el.textContent = '--';
el.classList.remove('has-data', 'err');
});
});
}
function updateDataStatus(flags) {
if (flags) {
if ('update' in flags) reqUpdateDone = flags.update;
if ('version' in flags) reqVersionHistoryDone = flags.version;
if ('addr' in flags) reqAddrInfoDone = flags.addr;
}
var status = document.getElementById('addr-list-status');
if (!status) return;
if (addrInfoError) {
status.textContent = '<%:Failed to get data, will retry%>';
return;
}
status.textContent = (reqUpdateDone && reqVersionHistoryDone && reqAddrInfoDone) ? '<%:Ready%>' : '<%:Fetching data...%>';
}
function resetDataStatus(keepUpdate) {
if (!keepUpdate) reqUpdateDone = false;
reqVersionHistoryDone = false;
reqAddrInfoDone = false;
addrInfoError = false;
updateDataStatus();
}
function refreshAddrInfo(opts) {
if (document.getElementById('selectPopup').classList.contains('hidden')) return;
var addrs = [];
document.querySelectorAll('#addr-list-body .addr-row').forEach(function(row) {
var v = row.dataset.value;
if (v && v !== 'custom' && isValidURL(v)) addrs.push(v);
});
if (addrs.length === 0) return;
if (addrInfoBusy) {
if (!(opts && opts.cancel)) return;
if (addrInfoXHR) { try { addrInfoXHR.abort(); } catch(e) {} addrInfoXHR = null; }
addrInfoBusy = false;
}
addrInfoBusy = true;
if (opts && opts.user) updateDataStatus({ addr: false });
var branch = fetchedBranch || release_branch.value || 'dev';
var smart = smart_enable.value || '0';
var pluginVer = document.getElementById('select-plugin-ver').value;
var coreVer = document.getElementById('select-core-ver').value;
var params = {
addrs: addrs.join(','),
branch: branch,
smart: smart,
plugin_ver: pluginVer || '',
core_ver: coreVer || ''
};
if (opts && opts.merge) {
params.merge = '1';
}
if (forceRefresh) {
params.force = '1';
forceRefresh = false;
infoCache = {};
addrDataReceived = false;
showDefaultNote();
clearAddrRows();
}
if (opts && opts.user && !addrDataReceived) setAddrListLoading(true);
var url = '<%=luci.dispatcher.build_url("admin", "services", "openclash", "addr_info")%>?' +
Object.keys(params).map(function(k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');
var xhr = new XMLHttpRequest();
xhr.timeout = 0;
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
var lastLen = 0;
var pendingBuf = '';
xhr.onprogress = function() {
if (xhr.status != 200) return;
var text = xhr.responseText;
if (text.length <= lastLen) return;
var newText = text.substring(lastLen);
lastLen = text.length;
pendingBuf += newText;
var lines = pendingBuf.split('\n');
pendingBuf = lines.pop();
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
try {
var info = JSON.parse(line);
if (info.complete) {
if (info.error === 'version_stale') {
fetchVersionHistory(true);
}
continue;
}
var addrUrl = info.addr;
if (!addrUrl) continue;
if (!addrDataReceived) { addrDataReceived = true; setAddrListLoading(false); }
var rows = document.querySelectorAll('#addr-list-body .addr-row[data-value="' + CSS.escape(addrUrl) + '"]');
rows.forEach(function(row) { updateAddrRow(row, addrUrl, info); });
} catch(e) {}
}
};
xhr.onload = function() {
if (xhr.status != 200 || !xhr.responseText) {
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
addrInfoError = true;
if (!retryTimer) {
retryTimer = setInterval(refreshAddrInfo, 10000);
}
return;
}
addrInfoError = false;
var text = xhr.responseText;
if (text.length > lastLen) {
var newText = text.substring(lastLen);
lastLen = text.length;
pendingBuf += newText;
var lines = pendingBuf.split('\n');
pendingBuf = lines.pop();
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
try {
var info = JSON.parse(line);
if (info.complete) {
if (info.error === 'version_stale') {
fetchVersionHistory(true);
}
continue;
}
var addrUrl = info.addr;
if (!addrUrl) continue;
if (!addrDataReceived) { addrDataReceived = true; setAddrListLoading(false); }
var rows = document.querySelectorAll('#addr-list-body .addr-row[data-value="' + CSS.escape(addrUrl) + '"]');
rows.forEach(function(row) { updateAddrRow(row, addrUrl, info); });
} catch(e) {}
}
}
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
if (!refreshTimer) {
refreshTimer = setInterval(refreshAddrInfo, ocRandomInterval(10000, 20000));
}
};
xhr.onerror = function() {
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
addrInfoError = true;
retryTimer = setInterval(refreshAddrInfo, 10000);
};
xhr.onloadend = function() {
if (addrInfoXHR === xhr) {
addrInfoXHR = null;
addrInfoBusy = false;
}
if (opts && opts.user) updateDataStatus({ addr: true });
};
xhr.send();
addrInfoXHR = xhr;
}
function initHeaderDrag() {
var popup = document.getElementById('selectPopup');
var header = popup ? popup.querySelector('.select-popup-header') : null;
if (!popup || !header || popup.classList.contains('select-popup-tab')) return;
if (header.dataset.ocDragInit) return;
header.dataset.ocDragInit = '1';
header.classList.add('oc-draggable');
var dragging = false;
var startX = 0, startY = 0, startLeft = 0, startTop = 0;
header.addEventListener('pointerdown', function(e) {
if (e.target.closest && e.target.closest('button, a, select, input')) return;
dragging = true;
var rect = popup.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
popup.style.transform = 'none';
popup.style.left = rect.left + 'px';
popup.style.top = rect.top + 'px';
popup.style.transition = 'none';
if (header.setPointerCapture) {
try { header.setPointerCapture(e.pointerId); } catch(err) {}
}
e.preventDefault();
});
header.addEventListener('pointermove', function(e) {
if (!dragging) return;
popup.style.left = (startLeft + (e.clientX - startX)) + 'px';
popup.style.top = (startTop + (e.clientY - startY)) + 'px';
});
function stopDrag(e) {
if (!dragging) return;
dragging = false;
popup.style.transition = '';
if (header.releasePointerCapture) {
try { header.releasePointerCapture(e.pointerId); } catch(err) {}
}
}
header.addEventListener('pointerup', stopDrag);
header.addEventListener('pointercancel', stopDrag);
}
function update(btn, type) {
var selectPopup = document.getElementById('selectPopup');
var customOptionInput = document.getElementById('customOptionInput');
var addCustomOption = document.getElementById('addCustomOption');
currentDownloadType = type || 'one_key';
customOptionInput.classList.add('oc-hidden');
addCustomOption.classList.add('oc-hidden');
customOptionInput.value = 'https://ghfast.top/';
fetchedBranch = null;
addrDataReceived = false;
resetDataStatus();
selectPopup.classList.remove('hidden');
selectPopup.onclick = function(event) { click_handler(event); };
setAddrListLoading(true);
initAllData();
initHeaderDrag();
}
function handleVersionUpdate(verLink) {
var row = verLink.closest ? verLink.closest('.addr-row') : null;
var addrUrl = row ? row.dataset.value : '';
var type = verLink.getAttribute('data-type');
var ver = verLink.getAttribute('data-ver') || '';
var dlUrl = verLink.getAttribute('data-dl') || '';
var branch = fetchedBranch || release_branch.value || 'dev';
var arch = core_version.value;
var selectEl = type === 'plugin'
? document.getElementById('select-plugin-ver')
: document.getElementById('select-core-ver');
if (type === 'core' && (!arch || arch === '0')) {
showHint('<%:No Compiled Version Selected, Please Select In Update Page And Try Again%>');
return;
}
var sha = ver ? findSHAbyVersion(selectEl, ver) : '';
var selOpt = selectEl.options[selectEl.selectedIndex];
var isLatest = !selOpt || selOpt.value === '__latest__';
if (ver && !sha && !isLatest && !(type === 'core' && _isOix)) {
showHint('<%:The selected version is not in the version history, please refresh and try again%>');
return;
}
if (ver) {
dlUrl = buildDownloadURL(addrUrl, type, branch, ver, sha, arch);
}
showDefaultNote();
var addrInfo = infoCache[addrUrl];
if (!addrInfo || addrInfo.latency == null) {
showHint('<%:Version info not ready, please wait and try again%>');
return;
}
if (addrInfo.latency === -1 || addrInfo.latency === -2 || addrInfo.latency === -3) {
var errMsg = addrInfo.latency === -1 ? '<%:Link access timed out, cannot download%>' : '<%:Link access failed, cannot download%>';
showHint(errMsg);
return;
}
if (!dlUrl) {
showHint('<%:Failed to get download link%>');
return;
}
if (type === 'plugin') {
var installedVer = (document.getElementById('installed-plugin').textContent || '').trim();
if (ver && installedVer && installedVer !== '--' && installedVer === ver) {
showHint('<%:The selected version is the same as the installed version, no need to update%>');
return;
}
} else {
var installedCoreVer = (document.getElementById('installed-core').textContent || '').trim();
if (ver && installedCoreVer && installedCoreVer !== '--' && installedCoreVer === ver) {
showHint('<%:The selected version is the same as the installed version, no need to update%>');
return;
}
}
startLog('<%:Checking...%>', false, type === 'core' ? 'openclash_core.sh' : 'openclash_update.sh');
var addr = addrUrl;
if (addr === 'https://raw.githubusercontent.com/') addr = '';
if (type === 'core') {
var params = {download_url: dlUrl};
if (addr && addr !== '') params.url = addr;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "core_download")%>', params, function() {});
} else {
var pParams = {url: addr, download_url: dlUrl, update_type: 'plugin'};
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "one_key_update")%>', pParams, function() {});
}
}
function initPopupResizeObserver() {
var popup = document.getElementById('selectPopup');
if (!popup || typeof ResizeObserver === 'undefined') return;
if (popup.dataset.ocResizeInit) return;
popup.dataset.ocResizeInit = '1';
new ResizeObserver(function() {
if (popup.offsetWidth > 0 && popup.offsetHeight > 0) {
if (popup.classList.contains('select-popup-tab')) return;
if (popup.style.left || popup.style.top) return;
popup.style.transform = 'translate(' + Math.round(-popup.offsetWidth / 2) + 'px, ' + Math.round(-popup.offsetHeight / 2) + 'px)';
}
}).observe(popup);
}
function click_handler(event) {
var target = event.target;
var customInput = document.getElementById('customOptionInput');
var addBtn = document.getElementById('addCustomOption');
var optionElem = target.closest ? target.closest('.select-option') : null;
var staticEl = target.closest ? target.closest('.addr-proxy-static') : null;
if (staticEl) return;
var verLink = target.closest ? target.closest('.ver-link') : null;
if (verLink) {
handleVersionUpdate(verLink);
return;
}
var dlBtn = target.closest ? target.closest('.dl-btn') : null;
if (dlBtn) {
return;
}
var proxyEl = target.closest ? target.closest('.addr-proxy') : null;
if (proxyEl) {
var prow = proxyEl.closest('.addr-row');
if (prow) toggleProxy(prow);
return;
}
if (optionElem && optionElem.dataset.value === 'custom' && !customInput.classList.contains('oc-hidden')) {
customInput.classList.add('oc-hidden');
addBtn.classList.add('oc-hidden');
} else if (optionElem && optionElem.dataset.value === 'custom') {
customInput.classList.remove('oc-hidden');
addBtn.classList.remove('oc-hidden');
customInput.focus();
} else if (target === customInput) {
customInput.classList.remove('oc-hidden');
addBtn.classList.remove('oc-hidden');
} else if (target === addBtn) {
var val = customInput.value.trim();
if (val !== '' && isValidURL(val)) {
var listBody = document.querySelector('#addr-list-body .addr-list-scroll') || document.getElementById('addr-list-body');
var div = document.createElement('div');
div.className = 'select-option addr-row';
div.dataset.value = val;
div.innerHTML = '<span class="addr-row-index"></span><span class="addr-row-url">' + val + '</span><span class="addr-status">--</span><span class="ver-text ver-plugin">--</span><span class="ver-text ver-core">--</span>';
listBody.insertBefore(div, document.getElementById('custom-addr-option'));
customInput.classList.add('oc-hidden');
addBtn.classList.add('oc-hidden');
renderProxyStates();
refreshAddrInfo({merge: true, cancel: true, user: true});
} else {
alert("<%:Please enter a valid URL!%>");
}
} else if (optionElem && optionElem.classList.contains('addr-row') && target.closest('.addr-row-url')) {
var addr = optionElem.dataset.value;
showDefaultNote();
var addrInfo = infoCache[addr];
if (!addrInfo || addrInfo.latency == null) {
showHint('<%:Version info not ready, please wait and try again%>');
return;
}
if (addrInfo.latency === -1 || addrInfo.latency === -2 || addrInfo.latency === -3) {
var errMsg = addrInfo.latency === -1 ? '<%:Link access timed out, cannot download%>' : '<%:Link access failed, cannot download%>';
showHint(errMsg);
return;
}
var branch = fetchedBranch || release_branch.value || 'dev';
var arch = core_version.value;
var v = core_version.value;
var r = release_branch.value;
var s = smart_enable.value;
if (!arch || arch === '0') {
showHint('<%:No Compiled Version Selected, Please Select In Update Page And Try Again%>');
return;
}
var pluginSel = document.getElementById('select-plugin-ver');
var coreSel = document.getElementById('select-core-ver');
var pluginOpt = pluginSel.options[pluginSel.selectedIndex];
var coreOpt = coreSel.options[coreSel.selectedIndex];
var isPluginLatest = !pluginOpt || pluginOpt.value === '__latest__';
var isCoreLatest = !coreOpt || coreOpt.value === '__latest__';
var pluginVer = addrInfo.plugin_ver || '';
var coreVer = (s === '1' && addrInfo.core_smart_ver)
? addrInfo.core_smart_ver
: (addrInfo.core_meta_ver || '');
var pluginSHA = (!isPluginLatest && addrInfo.plugin_ver) ? findSHAbyVersion(pluginSel, addrInfo.plugin_ver) : '';
var useCoreVer = (s === '1' && addrInfo.core_smart_ver) ? addrInfo.core_smart_ver : addrInfo.core_meta_ver;
var coreSHA = (!isCoreLatest && useCoreVer) ? findSHAbyVersion(coreSel, useCoreVer) : '';
if (currentDownloadType !== 'core_download' && !isPluginLatest && addrInfo.plugin_ver && !pluginSHA) {
showHint('<%:The selected version is not in the version history, please refresh and try again%>');
return;
}
if (!isCoreLatest && useCoreVer && !coreSHA) {
showHint('<%:The selected version is not in the version history, please refresh and try again%>');
return;
}
if (currentDownloadType !== 'core_download') {
// One-key update: both plugin and core will be updated
if (!pluginVer) {
showHint('<%:Failed to get plugin version info from this link, please check again later%>');
return;
}
if (!coreVer) {
showHint('<%:Failed to get core version info from this link, please check again later%>');
return;
}
var installedVer = (document.getElementById('installed-plugin').textContent || '').trim();
var installedCoreVer = (document.getElementById('installed-core').textContent || '').trim();
var pluginSame = installedVer && installedVer !== '--' && installedVer === pluginVer;
var coreSame = installedCoreVer && installedCoreVer !== '--' && installedCoreVer === coreVer;
if (pluginSame && coreSame) {
showHint('<%:The selected version is the same as the installed version, no need to update%>');
return;
}
} else {
// Core download only: check core only
if (!coreVer) {
showHint('<%:Failed to get core version info from this link, please check again later%>');
return;
}
var installedCoreVer = (document.getElementById('installed-core').textContent || '').trim();
if (installedCoreVer && installedCoreVer !== '--' && installedCoreVer === coreVer) {
showHint('<%:The selected version is the same as the installed version, no need to update%>');
return;
}
}
startLog('<%:Checking...%>', false, currentDownloadType === 'core_download' ? 'openclash_core.sh' : 'openclash_update.sh');
if (addr === 'https://raw.githubusercontent.com/') addr = '';
if (currentDownloadType === 'core_download') {
var coreURL = buildDownloadURL(addr, 'core', r, coreVer, coreSHA, arch);
var params = {download_url: coreURL};
if (addr && addr !== '') params.url = addr;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "core_download")%>', params, function() {});
} else {
var pluginURL = buildDownloadURL(addr, 'plugin', r, pluginVer, pluginSHA, arch);
var oneKeyParams = {url: addr};
if (!pluginSame) {
oneKeyParams.download_url = pluginURL;
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "one_key_update")%>', oneKeyParams, function() {});
}
}
}
function refreshAllData() {
showDefaultNote();
infoCache = {};
addrDataReceived = false;
resetDataStatus();
setAddrListLoading(true);
forceRefresh = true;
clearAddrRows();
if (versionRetryTimer) { clearInterval(versionRetryTimer); versionRetryTimer = null; }
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
fetchedBranch = null;
fetchUpdateInfo({force: true});
refreshTimer = setInterval(refreshAddrInfo, ocRandomInterval(10000, 20000));
}
function closeSelectPopup() {
var popup = document.getElementById('selectPopup');
var customInput = document.getElementById('customOptionInput');
var addBtn = document.getElementById('addCustomOption');
if (!versionTabMode) {
popup.classList.add('hidden');
popup.style.transform = '';
popup.style.left = '';
popup.style.top = '';
popup.style.transition = '';
}
if (addrInfoXHR) { try { addrInfoXHR.abort(); } catch(e) {} addrInfoXHR = null; }
if (versionHistoryXHR) { try { versionHistoryXHR.abort(); } catch(e) {} versionHistoryXHR = null; }
addrInfoBusy = false;
versionHistoryBusy = false;
if (!logStreamActive) { showDefaultNote(); }
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
if (versionRetryTimer) { clearInterval(versionRetryTimer); versionRetryTimer = null; }
if (retryTimer) { clearInterval(retryTimer); retryTimer = null; }
customInput.classList.add('oc-hidden');
addBtn.classList.add('oc-hidden');
}
function setBtnBusy(btn, busy, busyText) {
if (!btn) return;
var btnText = btn.querySelector('span:not(.loading-spinner)');
if (busy) {
if (btnText && !btn.dataset.ocBtnOriginalText) {
btn.dataset.ocBtnOriginalText = btnText.textContent;
}
if (btnText && busyText) { btnText.textContent = busyText; }
ocSetBtnLoading(btn, true);
} else {
ocSetBtnLoading(btn, false);
if (btnText && btn.dataset.ocBtnOriginalText) {
btnText.textContent = btn.dataset.ocBtnOriginalText;
delete btn.dataset.ocBtnOriginalText;
}
}
}
function remove_all_core(btn) {
if (confirm('<%:Are you sure want to remove all core files?%>')) {
setBtnBusy(btn, true, '<%:Removing...%>');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "remove_all_core")%>', null, function(x) {
alert((x && x.status == 200) ? '<%:Remove succeeded!%>' : '<%:Remove failed!%>');
setBtnBusy(btn, false);
});
}
return false;
}
function backup_file(btn) {
var endpoint = document.getElementById('BACKUP_SELECT').value;
var url = '<%=luci.dispatcher.build_url("admin", "services", "openclash")%>' + '/' + endpoint;
setBtnBusy(btn, true, '<%:Backing up...%>');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onload = function() {
setBtnBusy(btn, false);
if (xhr.status == 200 && xhr.response) {
var disposition = xhr.getResponseHeader('Content-Disposition') || '';
var match = disposition.match(/filename="?([^";]+)"?/);
var filename = match ? match[1] : endpoint + '.tar.gz';
var blobUrl = URL.createObjectURL(xhr.response);
var a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 1000);
} else {
alert('<%:Backup failed!%>');
}
};
xhr.onerror = function() {
setBtnBusy(btn, false);
alert('<%:Backup failed!%>');
};
xhr.send();
return false;
}
function restore_config(btn) {
if (confirm('<%:Are you sure want to restore the default config?%>')) {
setBtnBusy(btn, true, '<%:Restoring...%>');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "restore")%>', null, function(x) {
setBtnBusy(btn, false);
if (x && x.status == 200) {
alert('<%:Restore succeeded!%>');
} else {
alert('<%:Restore failed!%>');
}
window.location.href = '<%=luci.dispatcher.build_url("admin", "services", "openclash", "settings")%>';
});
}
return false;
}
function startLog(initialMessage, skipClearLog, scriptName) {
if (logStreamXHR) { logStreamXHR.abort(); logStreamXHR = null; }
if (logStreamReconnect) { clearTimeout(logStreamReconnect); logStreamReconnect = null; }
if (initialMessage) {
logRetryCount = 0;
lastLogLineCount = null;
}
logStreamActive = true;
var watchMode = !!scriptName && scriptName !== 'view';
var streamUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclash", "startlog")%>';
if (watchMode) {
streamUrl += '?script=' + encodeURIComponent(scriptName);
}
if (initialMessage) {
if (logMaxWaitTimer) { clearTimeout(logMaxWaitTimer); }
logMaxWaitTimer = setTimeout(function() {
if (logStreamXHR) { logStreamXHR.abort(); logStreamXHR = null; }
logStreamActive = false;
var hint = document.getElementById('version-hint');
if (hint) { hint.classList.remove('logging'); }
}, 600000);
}
if (initialMessage) {
showLogLine('<b style="color:var(--info-color)">' + initialMessage + '</b>');
}
function openStream() {
if (logStreamXHR) { logStreamXHR.abort(); logStreamXHR = null; }
var xhr = new XMLHttpRequest();
xhr.timeout = 0;
xhr.open('GET', streamUrl, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
var streamEnded = false;
var gotData = false;
var processedLines = 0;
var finishTimer = null;
function finalCleanup(showDefault) {
if (logStreamXHR === xhr) { logStreamXHR = null; }
try { xhr.abort(); } catch(e) {}
logStreamActive = false;
if (showDefault) {
var hint = document.getElementById('version-hint');
if (hint) { hint.classList.remove('logging'); }
showDefaultNote();
}
}
xhr.onprogress = function() {
if (logStreamXHR !== xhr) return;
if (streamEnded) return;
var text = xhr.responseText || '';
var startLine = (lastLogLineCount != null) ? lastLogLineCount : 0;
if (processedLines > startLine) startLine = processedLines;
var lines = text.split('\n');
var hasPartial = text.length > 0 && text.charAt(text.length - 1) !== '\n';
var endIdx = hasPartial ? lines.length - 1 : lines.length;
var newChunk = lines.slice(startLine, endIdx).join('\n');
processedLines = endIdx;
var raw = newChunk.replace(/##FINISHED##|##CONTINUE##|##FINISH##/g, '');
if (raw.trim()) {
gotData = true;
displayLog(raw);
}
if (/##FINISHED##\s*$/.test(text) && !finishTimer) {
finishTimer = setTimeout(function() {
streamEnded = true;
finalCleanup(true);
}, 10000);
}
};
xhr.onload = function() {
if (logStreamXHR !== xhr) return;
if (streamEnded) return;
var text = xhr.responseText || '';
if (!watchMode || /##FINISHED##\s*$/.test(text)) {
if (!finishTimer) {
finishTimer = setTimeout(function() {
streamEnded = true;
finalCleanup(true);
}, 10000);
}
return;
}
if (finishTimer) { clearTimeout(finishTimer); finishTimer = null; }
streamEnded = true;
lastLogLineCount = text.replace(/##FINISHED##\s*$|##CONTINUE##\s*$/, '').replace(/\n+$/, '').split('\n').length;
finalCleanup(false);
if (logStreamReconnect) clearTimeout(logStreamReconnect);
logStreamReconnect = setTimeout(function() {
if (!logStreamXHR) startLog(null, true, scriptName);
}, 1000);
};
xhr.onerror = function() {
if (logStreamXHR !== xhr) return;
if (finishTimer) { clearTimeout(finishTimer); finishTimer = null; }
streamEnded = true;
lastLogLineCount = (xhr.responseText || '').replace(/##FINISHED##\s*$|##CONTINUE##\s*$/, '').replace(/\n+$/, '').split('\n').length;
finalCleanup(false);
if (watchMode) {
if (logStreamReconnect) clearTimeout(logStreamReconnect);
logStreamReconnect = setTimeout(function() {
if (!logStreamXHR) startLog(null, true, scriptName);
}, 1000);
}
};
xhr.send();
logStreamXHR = xhr;
}
if (skipClearLog) {
openStream();
} else {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "del_start_log")%>', null, function() {
openStream();
});
}
}
function displayLog(logContent) {
var hint = document.getElementById('version-hint');
if (!hint) return;
var lines = logContent.split('\n');
var allLines = [];
for (var i = 0; i < lines.length; i++) {
var t = lines[i].replace(/##FINISHED##|##CONTINUE##|##FINISH##/g, '').trim().replace(/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s*/, '');
if (t) allLines.push(t);
}
if (allLines.length === 0) return;
var maxLines = 3;
if (!hint.classList.contains('logging')) {
hint.classList.add('logging');
hint.innerHTML = '';
hint.scrollTop = 0;
}
var existing = [];
for (var i = 0; i < hint.children.length; i++) {
var child = hint.children[i];
if (child.tagName === 'DIV' && child.textContent !== '\u200B' && child.firstChild && child.firstChild.textContent) {
existing.push(child.firstChild.textContent);
}
}
var merged = existing.concat(allLines);
if (merged.length > maxLines) {
merged = merged.slice(merged.length - maxLines);
}
hint.innerHTML = '';
hint.scrollTop = 0;
for (var j = 0; j < merged.length; j++) {
var color = ocGetLogColor(merged[j]);
var div = document.createElement('div');
div.style.whiteSpace = 'nowrap';
div.innerHTML = '<b style="color:' + color + '">' + merged[j] + '</b>';
hint.appendChild(div);
}
while (hint.children.length < maxLines) {
var spacer = document.createElement('div');
spacer.style.visibility = 'hidden';
spacer.style.whiteSpace = 'nowrap';
spacer.textContent = '\u200B';
hint.insertBefore(spacer, hint.firstChild);
}
hint.style.willChange = 'scroll-position';
ocAnimateScroll(hint);
}
showDefaultNote();
if (versionTabMode) {
var tabPopup = document.getElementById('selectPopup');
if (tabPopup) {
tabPopup.onclick = function(event) { click_handler(event); };
}
setAddrListLoading(true);
resetDataStatus();
initAllData();
}
function stopLogStream() {
if (logStreamReconnect) { clearTimeout(logStreamReconnect); logStreamReconnect = null; }
if (logMaxWaitTimer) { clearTimeout(logMaxWaitTimer); logMaxWaitTimer = null; }
if (logStreamXHR) { logStreamXHR.abort(); logStreamXHR = null; }
logStreamActive = false;
var hint = document.getElementById('version-hint');
if (hint) { hint.classList.remove('logging'); }
}
window.addEventListener('beforeunload', stopLogStream);
window.addEventListener('pagehide', stopLogStream);
initPopupResizeObserver();
//]]>
</script>