- 删除服务端工具、支援兵、插件三个页面(侧边栏入口 + HTML section) - server.py: 删除 servertool(9个)、companion/basic-attack 相关接口及常量 - native.py: 删除无人引用的 get_server_features/save_server_features/ companion_state/companion_save(保留 GM 工具依赖的 gameplay 方法) - shell.js: 删除 loadServerTool/loadCompanion 全部逻辑及状态变量 - shell.css: 清理 st-/cp- 相关样式 - templates/支援兵页面模板.md: 支援兵页面完整导出为独立模板 - 侧边栏「更新日志」更名为「游戏更新」
554 lines
23 KiB
JavaScript
554 lines
23 KiB
JavaScript
/* 90轮回启动器 · 单窗口页面与工具整合 */
|
||
(function () {
|
||
const $ = (id) => document.getElementById(id);
|
||
const pages = ['start','gm','loadout','settings','changelog'];
|
||
let shellGmInit = false;
|
||
let loConfig = null, loPage = 0, loScheme = -1;
|
||
let loCooldownUntil = 0, loCooldownCharacter = '', loCooldownTimer = null;
|
||
let hotkeyCapture = false;
|
||
|
||
const crumb = {start:'开始 / 本地服务端',gm:'GM工具',loadout:'工具 / 一键换装',settings:'设置',changelog:'更新日志'};
|
||
|
||
function shellGo(page) {
|
||
if (!pages.includes(page)) page = 'start';
|
||
document.querySelectorAll('.shell-page').forEach(el => el.classList.toggle('active', el.id === 'page-' + page));
|
||
document.querySelectorAll('.shell-nav-item').forEach(el => el.classList.toggle('active', el.dataset.shellPage === page));
|
||
$('shellCrumb').textContent = crumb[page] || page;
|
||
if (page === 'start') loadStart();
|
||
if (page === 'loadout') loadLoadout();
|
||
if (page === 'settings') loadSettings();
|
||
if (page === 'changelog') loadChangelog();
|
||
if (page === 'gm') {
|
||
// 进入 GM:GM 覆盖整个窗口(隐藏顶部导航/面包屑/底部状态栏),并铺满工作区
|
||
document.body.classList.add('gm-fullscreen');
|
||
const w = win();
|
||
if (w && w.enter_gm) w.enter_gm();
|
||
if (!shellGmInit && typeof initGm === 'function') {
|
||
shellGmInit = true;
|
||
initGm();
|
||
if (typeof renderGmPage === 'function') renderGmPage('dashboard');
|
||
} else if (typeof renderGmPage === 'function') {
|
||
renderGmPage(gmCurrentPage || 'dashboard');
|
||
}
|
||
} else {
|
||
// 离开 GM:移除全屏状态,并恢复窗口几何(若正处于全屏)
|
||
if (document.body.classList.contains('gm-fullscreen')) {
|
||
document.body.classList.remove('gm-fullscreen');
|
||
const w2 = win();
|
||
if (w2 && w2.restore_gm) w2.restore_gm();
|
||
}
|
||
}
|
||
}
|
||
window.shellGo = shellGo;
|
||
|
||
// 从 GM 返回启动器:恢复窗口几何 + 回到开始页
|
||
function shellExitGm() {
|
||
document.body.classList.remove('gm-fullscreen');
|
||
const w = win();
|
||
if (w && w.restore_gm) w.restore_gm();
|
||
shellGo('start');
|
||
}
|
||
window.shellExitGm = shellExitGm;
|
||
|
||
document.querySelectorAll('.shell-nav-item').forEach(el => {
|
||
el.addEventListener('click', () => shellGo(el.dataset.shellPage));
|
||
});
|
||
|
||
// 顶部窗口按钮
|
||
const win = () => (window.pywebview && window.pywebview.api) || null;
|
||
const bMin = $('shellWinMin'); if (bMin) bMin.addEventListener('click', () => { const w = win(); if (w && w.minimize) w.minimize(); });
|
||
const bClose = $('shellWinClose'); if (bClose) bClose.addEventListener('click', () => { const w = win(); if (w && w.close) w.close(); });
|
||
|
||
|
||
// 账号登录:打开原账号管理弹窗,可选择默认账号
|
||
$('shellAccount').addEventListener('click', () => {
|
||
if (typeof openAccounts === 'function') openAccounts();
|
||
else alert('账号管理未加载');
|
||
});
|
||
|
||
async function shellAddAccount() {
|
||
const user = $('newAccUser').value.trim();
|
||
const pass = $('newAccPass').value;
|
||
const toast = (msg, type) => {
|
||
if (typeof gmToast === 'function') gmToast(msg, type || 'info');
|
||
else alert(msg);
|
||
};
|
||
if (!user || !pass) { toast('请填写用户名和密码', 'err'); return; }
|
||
try {
|
||
const r = await post('/api/accounts-config', {op:'add', username:user, password:pass});
|
||
if (!r.ok) { toast('添加失败:' + (r.error || '未知错误'), 'err'); return; }
|
||
$('newAccUser').value = '';
|
||
$('newAccPass').value = '';
|
||
$('accountsModal').classList.add('hidden');
|
||
await loadAccountLabel();
|
||
toast('账号添加成功:' + user, 'ok');
|
||
} catch (e) { toast('添加失败:' + e.message, 'err'); }
|
||
}
|
||
|
||
// app.js 的 init 如果没有完成绑定,这里也保证添加按钮可用
|
||
const addBtn = $('btnAddAccount');
|
||
if (addBtn) {
|
||
const fresh = addBtn.cloneNode(true);
|
||
addBtn.replaceWith(fresh);
|
||
fresh.addEventListener('click', shellAddAccount);
|
||
}
|
||
|
||
// 开始页
|
||
let startState = 'idle'; // idle | starting | started
|
||
function setStartState(state) {
|
||
startState = state;
|
||
const btn = $('shellStart');
|
||
if (!btn) return;
|
||
btn.disabled = state === 'starting';
|
||
if (state === 'starting') btn.innerHTML = '<span>⏳</span> 游戏启动中…';
|
||
else if (state === 'started') btn.innerHTML = '<span>✔</span> 游戏已启动';
|
||
else btn.innerHTML = '<span>▶</span> 开始游戏';
|
||
}
|
||
$('shellStart').addEventListener('click', () => {
|
||
if (startState === 'starting') return;
|
||
setStartState('starting');
|
||
doAction('start');
|
||
});
|
||
$('shellStartMenu').addEventListener('click', () => $('shellStartPop').classList.toggle('hidden'));
|
||
document.querySelectorAll('#shellStartPop button').forEach(b => b.addEventListener('click', () => {
|
||
$('shellStartPop').classList.add('hidden');
|
||
if (b.dataset.shellAction === 'stop') setStartState('idle');
|
||
doAction(b.dataset.shellAction);
|
||
}));
|
||
$('shellStop').addEventListener('click', () => {
|
||
setStartState('idle');
|
||
doAction('stop');
|
||
});
|
||
|
||
async function doAction(action) {
|
||
const map = {start:'start', launch:'start', stop:'stop'};
|
||
const payload = {action: map[action] || action};
|
||
if (payload.action === 'start') {
|
||
payload.account = $('shellAccountSelect').value || '';
|
||
}
|
||
try {
|
||
const r = await post('/api/action', payload);
|
||
if (!r.ok) {
|
||
if (startState === 'starting') setStartState('idle');
|
||
alert(r.error || '操作失败');
|
||
}
|
||
setTimeout(loadStart, 900);
|
||
} catch (e) {
|
||
if (startState === 'starting') setStartState('idle');
|
||
alert('操作失败:' + e.message);
|
||
}
|
||
}
|
||
|
||
async function loadStart() {
|
||
try {
|
||
const s = await api('/api/status');
|
||
const running = !!s.server_running;
|
||
if (running && startState !== 'started') setStartState('started');
|
||
else if (!running && startState === 'started') setStartState('idle');
|
||
$('shellStatusDot').style.background = running ? '#3dd68b' : '#5a7568';
|
||
$('shellStatusText').textContent = running ? '运行中' : '未运行';
|
||
$('shellPid').textContent = running ? '服务端已启动' : 'PID —';
|
||
$('shellBottomStatus').textContent = running ? '运行中' : '未运行';
|
||
$('statClient').textContent = running ? '运行中' : '未运行';
|
||
$('statPorts').textContent = running ? '18111 / 7001' : '—';
|
||
$('statPvf').textContent = s.db_exists ? '已校验通过' : '未检测';
|
||
$('statClientDir').textContent = s.client_dir || '—';
|
||
$('statDb').textContent = s.db_exists ? 'SQLite 就绪' : '未创建';
|
||
$('statGmPort').textContent = '18111';
|
||
$('statApiPort').textContent = location.port || '19527';
|
||
$('shellBottomPort').textContent = running ? '18111 / 7001' : '—';
|
||
$('statState').textContent = s.control_exists && s.client_dir_exists ? '已就绪' : '不完整';
|
||
$('statClient').textContent = s.client_dir_exists ? (running ? '运行中' : '已就绪') : '缺失';
|
||
$('shellClock').textContent = '检查于 ' + new Date().toLocaleTimeString('zh-CN', {hour12:false});
|
||
$('statResource').textContent = s.admin ? '管理员' : '普通权限';
|
||
loadAccountLabel();
|
||
loadUpdateStatus();
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
async function loadAccountLabel() {
|
||
try {
|
||
const r = await api('/api/accounts-config');
|
||
const list = (r && r.data) || [];
|
||
const selected = list.find(a => a.is_default) || list[0];
|
||
$('shellAccountName').textContent = selected ? selected.username : '未配置账号';
|
||
const sel = $('shellAccountSelect');
|
||
if (sel && list.length) {
|
||
const current = sel.value;
|
||
sel.innerHTML = list.map(a => `<option value="${a.username}">${a.username}</option>`).join('');
|
||
sel.value = selected ? selected.username : (current || list[0].username);
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
// 检查更新:填充「版本」卡片里的「本地 / 远程最新 / 状态」
|
||
async function loadUpdateStatus() {
|
||
const remoteEl = $('statRemote');
|
||
const versionEl = $('statVersion');
|
||
try {
|
||
const r = await api('/api/update/check');
|
||
if (versionEl) versionEl.textContent = r.current || '—';
|
||
if (!remoteEl) return;
|
||
if (r.error) {
|
||
remoteEl.textContent = '检查失败';
|
||
remoteEl.className = '';
|
||
remoteEl.title = '无法连接更新服务器:' + r.error;
|
||
} else if (r.has_update && r.latest) {
|
||
remoteEl.textContent = r.latest;
|
||
remoteEl.className = 'green';
|
||
remoteEl.title = '发现新版本,点击查看';
|
||
remoteEl.style.cursor = 'pointer';
|
||
remoteEl.onclick = () => { window.open(r.latest_url, '_blank'); };
|
||
} else if (r.latest) {
|
||
remoteEl.textContent = r.latest;
|
||
remoteEl.className = 'green';
|
||
remoteEl.title = '已是最新版本';
|
||
} else {
|
||
remoteEl.textContent = '暂无发布';
|
||
remoteEl.className = '';
|
||
}
|
||
} catch (e) {
|
||
if (remoteEl) { remoteEl.textContent = '检查失败'; remoteEl.className = ''; }
|
||
}
|
||
}
|
||
|
||
// 更新日志页面:拉取所有 Release 的 changelog
|
||
async function loadChangelog() {
|
||
const box = $('changelogBody');
|
||
if (!box) return;
|
||
box.innerHTML = '<div class="empty-box">正在获取更新日志…</div>';
|
||
try {
|
||
const r = await api('/api/update/changelog');
|
||
if (!r.ok) {
|
||
box.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
|
||
return;
|
||
}
|
||
const rels = r.releases || [];
|
||
if (!rels.length) {
|
||
box.innerHTML = '<div class="empty-box">暂无发布记录。<br>在 Gitea 仓库「发布 Release」并填写更新说明后,这里会自动显示。</div>';
|
||
return;
|
||
}
|
||
box.innerHTML = rels.map(rel => {
|
||
const badge = rel.prerelease ? '<span class="gm-tag gm-tag-gray">预发布</span>' : '';
|
||
const body = (rel.body || '').replace(/\r?\n/g, '<br>');
|
||
const assets = (rel.assets || []).map(a =>
|
||
`<a class="changelog-asset" href="${escapeHtml(a.download_url)}" target="_blank">⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)</a>`
|
||
).join('');
|
||
const when = rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : '';
|
||
return `<div class="changelog-item">
|
||
<div class="changelog-head">
|
||
<span class="changelog-tag">${escapeHtml(rel.tag || rel.name || '')}</span>
|
||
<span class="changelog-name">${escapeHtml(rel.name || '')}</span>
|
||
${badge}
|
||
<span class="changelog-time">${escapeHtml(when)}</span>
|
||
</div>
|
||
<div class="changelog-body">${body}</div>
|
||
${assets ? '<div class="changelog-assets">' + assets + '</div>' : ''}
|
||
<a class="changelog-link" href="${escapeHtml(rel.html_url)}" target="_blank">在浏览器中查看 ↗</a>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
box.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
|
||
}
|
||
}
|
||
|
||
$('shellAccountSelect').addEventListener('change', async () => {
|
||
const username = $('shellAccountSelect').value;
|
||
try {
|
||
const r = await post('/api/accounts-config', {op:'select', username});
|
||
if (r.ok) { $('shellAccountName').textContent = username; }
|
||
} catch (e) { /* ignore */ }
|
||
});
|
||
|
||
// ---------------- 一键换装 ----------------
|
||
function normalizeConfig(cfg) {
|
||
cfg = cfg || {};
|
||
if (!Array.isArray(cfg.Pages) || !cfg.Pages.length) cfg.Pages = [{Name:'默认页', Schemes:[]}];
|
||
cfg.Pages.forEach(p => { if (!Array.isArray(p.Schemes)) p.Schemes = []; });
|
||
cfg.ActivePage = Math.max(0, Math.min(Number(cfg.ActivePage) || 0, cfg.Pages.length - 1));
|
||
return cfg;
|
||
}
|
||
|
||
async function loadLoadout() {
|
||
try {
|
||
const r = await api('/api/loadout/config');
|
||
loConfig = normalizeConfig(r.data);
|
||
loPage = loConfig.ActivePage || 0;
|
||
renderLoadout();
|
||
} catch (e) { $('loStatus').textContent = '读取失败:' + e.message; }
|
||
}
|
||
|
||
function currentPage() { return loConfig.Pages[loPage]; }
|
||
function currentScheme() { return currentPage().Schemes[loScheme] || null; }
|
||
|
||
function loadoutCooldownRemaining() {
|
||
return Math.max(0, Math.ceil((loCooldownUntil - Date.now()) / 1000));
|
||
}
|
||
|
||
function updateLoadoutCooldown() {
|
||
const s = currentScheme();
|
||
const active = s && s.CharacterName === loCooldownCharacter;
|
||
const remain = active ? loadoutCooldownRemaining() : 0;
|
||
const el = $('loCooldown');
|
||
if (remain > 0) {
|
||
el.textContent = `换装冷却:${remain} 秒`;
|
||
el.classList.add('active');
|
||
$('loApply').disabled = true;
|
||
} else {
|
||
el.textContent = '';
|
||
el.classList.remove('active');
|
||
$('loApply').disabled = false;
|
||
if (loCooldownTimer) { clearInterval(loCooldownTimer); loCooldownTimer = null; }
|
||
}
|
||
}
|
||
|
||
function startLoadoutCooldown(character, seconds) {
|
||
loCooldownCharacter = character || '';
|
||
loCooldownUntil = Date.now() + Math.max(0, Number(seconds) || 0) * 1000;
|
||
if (loCooldownTimer) clearInterval(loCooldownTimer);
|
||
updateLoadoutCooldown();
|
||
loCooldownTimer = setInterval(updateLoadoutCooldown, 250);
|
||
}
|
||
|
||
|
||
function renderLoadout() {
|
||
const pages = $('loPages'); pages.innerHTML = '';
|
||
loConfig.Pages.forEach((p, i) => {
|
||
const b = document.createElement('button');
|
||
b.className = 'lo-page' + (i === loPage ? ' active' : '');
|
||
b.textContent = p.Name || ('页 ' + (i + 1));
|
||
b.onclick = async () => { loPage = i; loConfig.ActivePage = i; loScheme = -1; renderLoadout(); await saveLoadout(false); };
|
||
pages.appendChild(b);
|
||
});
|
||
const list = $('loList'); list.innerHTML = '';
|
||
currentPage().Schemes.forEach((s, i) => {
|
||
const d = document.createElement('div');
|
||
d.className = 'lo-item' + (i === loScheme ? ' active' : '');
|
||
d.textContent = `${s.Name || '方案'} [${s.CharacterName || '未绑定'}] ${hotkeyText(s.Modifiers, s.KeyCode)} (${(s.Slots||[]).length} 件)`;
|
||
d.onclick = () => { loScheme = i; renderLoadout(); };
|
||
list.appendChild(d);
|
||
});
|
||
const s = currentScheme();
|
||
$('loName').value = s ? (s.Name || '') : '';
|
||
$('loCharacter').value = s ? (s.CharacterName || '') : '';
|
||
$('loHotkey').value = s ? hotkeyText(s.Modifiers, s.KeyCode) : '未设置';
|
||
const slots = $('loSlots');
|
||
slots.innerHTML = '';
|
||
if (!s || !(s.Slots || []).length) {
|
||
slots.innerHTML = '<div class="empty-box">选择方案后显示装备快照;点击「保存当前装备」从服务端读取。</div>';
|
||
} else {
|
||
s.Slots.forEach(it => {
|
||
const d = document.createElement('div');
|
||
d.className = 'lo-slot';
|
||
d.innerHTML = `槽位 ${it.slot}<b>${it.item_id}</b>`;
|
||
slots.appendChild(d);
|
||
});
|
||
}
|
||
updateLoadoutCooldown();
|
||
}
|
||
|
||
function hotkeyText(mods, key) {
|
||
if (!key) return '未设置';
|
||
const p = [];
|
||
if (mods & 2) p.push('Ctrl');
|
||
if (mods & 1) p.push('Alt');
|
||
if (mods & 4) p.push('Shift');
|
||
p.push(keyName(key));
|
||
return p.join('+');
|
||
}
|
||
function keyName(code) {
|
||
if (code >= 112 && code <= 123) return 'F' + (code - 111);
|
||
if (code >= 65 && code <= 90) return String.fromCharCode(code);
|
||
if (code >= 48 && code <= 57) return String.fromCharCode(code);
|
||
return String(code);
|
||
}
|
||
|
||
$('loAddPage').addEventListener('click', async () => {
|
||
const name = prompt('换装页名称', '换装页' + (loConfig.Pages.length + 1));
|
||
if (!name) return;
|
||
loConfig.Pages.push({Name:name, Schemes:[]});
|
||
loPage = loConfig.Pages.length - 1; loConfig.ActivePage = loPage; loScheme = -1;
|
||
renderLoadout(); await saveLoadout();
|
||
});
|
||
|
||
$('loAddScheme').addEventListener('click', async () => {
|
||
const name = prompt('方案名称', '方案' + (currentPage().Schemes.length + 1));
|
||
if (!name) return;
|
||
const character = prompt('绑定角色游戏内名称(可稍后修改)', '') || '';
|
||
currentPage().Schemes.push({Name:name, CharacterName:character, Modifiers:0, KeyCode:0, Slots:[]});
|
||
loScheme = currentPage().Schemes.length - 1;
|
||
renderLoadout(); await saveLoadout();
|
||
});
|
||
|
||
$('loDeleteScheme').addEventListener('click', async () => {
|
||
if (loScheme < 0) return;
|
||
if (!confirm('删除当前方案?')) return;
|
||
currentPage().Schemes.splice(loScheme, 1);
|
||
loScheme = -1; renderLoadout(); await saveLoadout();
|
||
});
|
||
|
||
$('loSaveConfig').addEventListener('click', () => saveLoadout());
|
||
$('loName').addEventListener('change', () => { const s=currentScheme(); if(s){s.Name=$('loName').value; renderLoadout();} });
|
||
$('loCharacter').addEventListener('change', () => { const s=currentScheme(); if(s){s.CharacterName=$('loCharacter').value.trim(); renderLoadout();} });
|
||
|
||
async function saveLoadout(show = true) {
|
||
try {
|
||
if (loConfig) { loConfig.ActivePage = loPage; const s=currentScheme(); if(s){s.Name=$('loName').value; s.CharacterName=$('loCharacter').value.trim();} }
|
||
const r = await post('/api/loadout/config', {config: loConfig});
|
||
if (r.ok) { if (show) $('loStatus').textContent = '配置已保存 ' + new Date().toLocaleTimeString(); }
|
||
else $('loStatus').textContent = r.error || '保存失败';
|
||
} catch (e) { $('loStatus').textContent = '保存失败:' + e.message; }
|
||
}
|
||
|
||
$('loRefreshChars').addEventListener('click', async () => {
|
||
try {
|
||
const r = await api('/api/loadout/characters');
|
||
if (!r.ok) { $('loStatus').textContent = r.error || '读取失败'; return; }
|
||
const list = r.data || [];
|
||
$('loStatus').textContent = list.map(c => `${c.name}(${c.online?'在线':'离线'})`).join(' ') || '没有角色';
|
||
} catch (e) { $('loStatus').textContent = e.message; }
|
||
});
|
||
|
||
$('loSaveCurrent').addEventListener('click', async () => {
|
||
const s = currentScheme();
|
||
if (!s) { $('loStatus').textContent = '请先选择方案'; return; }
|
||
if (!s.CharacterName) { $('loStatus').textContent = '请先填写绑定角色'; return; }
|
||
try {
|
||
const r = await api('/api/loadout/equipment?character=' + encodeURIComponent(s.CharacterName));
|
||
if (!r.ok) { $('loStatus').textContent = r.error || '读取装备失败'; return; }
|
||
s.Slots = r.data.slots || [];
|
||
renderLoadout(); await saveLoadout();
|
||
$('loStatus').textContent = `已保存 ${s.Slots.length} 个部位`;
|
||
} catch (e) { $('loStatus').textContent = e.message; }
|
||
});
|
||
|
||
$('loApply').addEventListener('click', async () => {
|
||
const s = currentScheme();
|
||
if (!s) { $('loStatus').textContent = '请先选择方案'; return; }
|
||
if (s.CharacterName === loCooldownCharacter && loadoutCooldownRemaining() > 0) {
|
||
$('loStatus').textContent = `换装冷却中,还需 ${loadoutCooldownRemaining()} 秒`;
|
||
return;
|
||
}
|
||
try {
|
||
const r = await post('/api/loadout/apply', {character:s.CharacterName, slots:s.Slots || []});
|
||
if (r.ok) {
|
||
const d = r.data || {};
|
||
const applied = (d.applied || []).length;
|
||
const skipped = (d.skipped || []).length;
|
||
if (Number(d.cooldown_seconds) > 0) {
|
||
startLoadoutCooldown(s.CharacterName, Number(d.cooldown_seconds));
|
||
$('loStatus').textContent = `应用完成:成功 ${applied},跳过 ${skipped};换装冷却 ${d.cooldown_seconds} 秒`;
|
||
} else {
|
||
$('loStatus').textContent = `应用完成:成功 ${applied},跳过 ${skipped}`;
|
||
}
|
||
} else {
|
||
const msg = r.error || '应用失败';
|
||
const m = msg.match(/cooldown[^0-9]*(\d+)\s*seconds?/i);
|
||
if (m) {
|
||
startLoadoutCooldown(s.CharacterName, Number(m[1]));
|
||
$('loStatus').textContent = `换装冷却中,还需 ${m[1]} 秒才能再次换装`;
|
||
} else {
|
||
$('loStatus').textContent = msg;
|
||
}
|
||
}
|
||
} catch (e) { $('loStatus').textContent = e.message; }
|
||
});
|
||
|
||
$('loCaptureHotkey').addEventListener('click', () => {
|
||
if (!currentScheme()) { $('loStatus').textContent = '请先选择方案'; return; }
|
||
hotkeyCapture = true; $('loHotkey').value = '请按下组合键…'; $('loHotkey').focus();
|
||
});
|
||
$('loHotkey').addEventListener('keydown', (e) => {
|
||
if (!hotkeyCapture) return;
|
||
e.preventDefault();
|
||
let mods = 0;
|
||
if (e.ctrlKey) mods |= 2;
|
||
if (e.altKey) mods |= 1;
|
||
if (e.shiftKey) mods |= 4;
|
||
const key = e.keyCode;
|
||
if (!key || key === 17 || key === 18 || key === 16) return;
|
||
const s = currentScheme();
|
||
if (!s) return;
|
||
s.Modifiers = mods; s.KeyCode = key;
|
||
hotkeyCapture = false;
|
||
$('loHotkey').value = hotkeyText(mods, key);
|
||
saveLoadout();
|
||
});
|
||
|
||
async function loadSettings() {
|
||
try {
|
||
const s = await api('/api/status');
|
||
$('setServerRoot').textContent = s.server_root || '未绑定';
|
||
$('setControl').textContent = s.control_exists ? '已找到 DNF90Control.exe' : '缺失';
|
||
$('setDb').textContent = s.db || '未找到';
|
||
$('setClient').textContent = s.client_dir || '未找到';
|
||
$('setRun').textContent = s.server_running ? '运行中' : '未运行';
|
||
$('setNative').textContent = s.native_available ? '可用' : '不可用';
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
$('setRefresh').addEventListener('click', loadSettings);
|
||
|
||
// 顶栏拖动窗口(JS move 兜底,兼容 pywebview EdgeChromium)
|
||
// 同时支持第一行标题栏(.shell-topnav)与第二行面包屑栏(.shell-topbar)
|
||
['shell-topnav', 'shell-topbar'].forEach((sel) => {
|
||
const topbar = document.querySelector('.' + sel);
|
||
if (!topbar) return;
|
||
let dragState = null;
|
||
const onMove = (e) => {
|
||
if (!dragState) return;
|
||
const w = win();
|
||
if (w && w.move) w.move(dragState.x + e.screenX - dragState.sx, dragState.y + e.screenY - dragState.sy);
|
||
};
|
||
const onUp = () => {
|
||
dragState = null;
|
||
document.removeEventListener('mousemove', onMove, true);
|
||
document.removeEventListener('mouseup', onUp, true);
|
||
};
|
||
topbar.addEventListener('mousedown', (e) => {
|
||
if (e.target.closest('button,select,input,a')) return;
|
||
const w = win();
|
||
if (!w || !w.getPosition) return;
|
||
e.preventDefault();
|
||
const sx = e.screenX, sy = e.screenY;
|
||
w.getPosition().then((pos) => {
|
||
dragState = {x: Number(pos[0]) || 0, y: Number(pos[1]) || 0, sx, sy};
|
||
document.addEventListener('mousemove', onMove, true);
|
||
document.addEventListener('mouseup', onUp, true);
|
||
}).catch(() => {});
|
||
});
|
||
});
|
||
document.querySelectorAll('.win-resize').forEach(el => {
|
||
el.addEventListener('mousedown', (e) => {
|
||
e.preventDefault();
|
||
const edge = el.dataset.edge;
|
||
const w = win();
|
||
if (!w) return;
|
||
// 优先系统边缘缩放;右侧/底部再用 JS 尺寸兜底
|
||
if (['left','top','topleft','topright','bottomleft'].includes(edge) && w.start_resize) w.start_resize(edge);
|
||
if (['right','bottom','bottomright'].includes(edge) && w.resize_window) {
|
||
const sx = e.screenX, sy = e.screenY;
|
||
const sw = window.innerWidth, sh = window.innerHeight;
|
||
let rz = {edge, sx, sy, sw, sh};
|
||
const move = (ev) => {
|
||
let nw = sw, nh = sh;
|
||
if (edge.includes('right') || edge === 'right') nw = sw + ev.screenX - sx;
|
||
if (edge.includes('bottom') || edge === 'bottom') nh = sh + ev.screenY - sy;
|
||
w.resize_window(nw, nh);
|
||
};
|
||
const up = () => {
|
||
rz = null;
|
||
document.removeEventListener('mousemove', move, true);
|
||
document.removeEventListener('mouseup', up, true);
|
||
};
|
||
document.addEventListener('mousemove', move, true);
|
||
document.addEventListener('mouseup', up, true);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 兼容旧 GM 页面里的返回
|
||
window.shellGo = shellGo;
|
||
loadStart();
|
||
setInterval(loadStart, 4000);
|
||
})();
|