- 服务端新增活动开关运行时配置与 GM 接口(GET/POST /dnf/gm/activity-switches), 持久化到 Settings,接入月签到/周签到/PC房↔累计在线的实时下发 - 服务端新增频道开关 GM 接口(channel-switches),目录生成时过滤禁用频道 - channelcatalog/channelinfo 支持按禁用频道 ID 过滤目录与客户端脚本 - 启动器新增「游戏设置」页面:频道开关 Tab + 活动开关 Tab - native.py/server.py 新增对应转发 API - 窗口标题/页面 title/侧边栏「90轮回」统一改为「DNF90 灵狐小竹整合单机版」
721 lines
30 KiB
JavaScript
721 lines
30 KiB
JavaScript
/* 90轮回启动器 · 单窗口页面与工具整合 */
|
||
(function () {
|
||
const $ = (id) => document.getElementById(id);
|
||
const pages = ['start','gm','loadout','gamesettings','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:'工具 / 一键换装',gamesettings:'工具 / 游戏设置',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 === 'gamesettings') loadGameSettings();
|
||
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);
|
||
|
||
// ==================== 游戏设置(频道开关 / 活动开关) ====================
|
||
let gsCurrentTab = 'channels';
|
||
const gsToast = (msg, type = 'info') => {
|
||
if (typeof gmToast === 'function') gmToast(msg, type);
|
||
else console.log(msg);
|
||
};
|
||
|
||
function gsSwitchTab(tab) {
|
||
gsCurrentTab = tab;
|
||
document.querySelectorAll('#gsTabs .st-tab').forEach(el =>
|
||
el.classList.toggle('active', el.dataset.gs === tab));
|
||
$('gsChannelsPane').style.display = tab === 'channels' ? '' : 'none';
|
||
$('gsActivitiesPane').style.display = tab === 'activities' ? '' : 'none';
|
||
}
|
||
|
||
function gsChannelTypeText(type) {
|
||
const t = Number(type);
|
||
if (t === 22) return '特殊区域';
|
||
if (t === 11) return '普通城镇';
|
||
if (t === 2 || t === 3) return '引导';
|
||
return '频道 ' + t;
|
||
}
|
||
|
||
async function gsLoadChannels() {
|
||
const grid = $('gsChannelGrid');
|
||
const summary = $('gsChannelSummary');
|
||
if (!grid) return;
|
||
grid.innerHTML = '<div class="empty-box">加载中…</div>';
|
||
try {
|
||
const r = await api('/api/game-settings/channel-switches');
|
||
if (!r.ok) {
|
||
grid.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
|
||
summary.textContent = '';
|
||
return;
|
||
}
|
||
const d = r.data || {};
|
||
const list = d.channels || [];
|
||
summary.textContent = '共 ' + (d.total_channels ?? list.length) + ' 个频道 · 已启用 ' +
|
||
(d.enabled_channels ?? '—') + ' · 已停用 ' + (d.disabled_channels ?? '—');
|
||
if (!list.length) {
|
||
grid.innerHTML = '<div class="empty-box">暂无频道数据。请确认服务端已启动。</div>';
|
||
return;
|
||
}
|
||
grid.innerHTML = list.map(ch => {
|
||
const on = !!ch.enabled;
|
||
return `<div class="gs-card ${on ? '' : 'gs-off'}">
|
||
<div class="gs-card-head">
|
||
<span class="gs-card-name">${escapeHtml(ch.name || ('频道 ' + ch.id))}</span>
|
||
<span class="gm-tag ${on ? 'gm-tag-green' : 'gm-tag-gray'}">${on ? '启用' : '停用'}</span>
|
||
</div>
|
||
<div class="gs-card-meta muted">ID ${ch.id} · ${gsChannelTypeText(ch.type)}${ch.group ? ' · ' + escapeHtml(ch.group) : ''}${ch.port ? ' · 端口 ' + ch.port : ''}</div>
|
||
<div class="gs-card-actions">
|
||
<button class="shell-btn ${on ? '' : 'primary'}" data-gs-channel="${ch.id}" data-gs-enable="${!on}">${on ? '停用' : '启用'}</button>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
grid.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
|
||
}
|
||
}
|
||
|
||
async function gsLoadActivities() {
|
||
const list = $('gsActivityList');
|
||
const summary = $('gsActivitySummary');
|
||
if (!list) return;
|
||
list.innerHTML = '<div class="empty-box">加载中…</div>';
|
||
try {
|
||
const r = await api('/api/game-settings/activity-switches');
|
||
if (!r.ok) {
|
||
list.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(r.error || '未知错误') + '</div>';
|
||
summary.textContent = '';
|
||
return;
|
||
}
|
||
const d = r.data || {};
|
||
const items = d.switches || [];
|
||
const onCount = items.filter(x => x.enabled).length;
|
||
summary.textContent = '共 ' + items.length + ' 个活动 · 已开启 ' + onCount;
|
||
if (!items.length) {
|
||
list.innerHTML = '<div class="empty-box">暂无活动数据。请确认服务端已启动。</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = items.map(it => {
|
||
const on = !!it.enabled;
|
||
return `<div class="gs-row ${on ? '' : 'gs-off'}">
|
||
<div class="gs-row-main">
|
||
<div class="gs-row-name">${escapeHtml(it.title || it.key)}</div>
|
||
<div class="gs-row-desc muted">${escapeHtml(it.description || '')}</div>
|
||
</div>
|
||
<button class="gs-switch ${on ? 'on' : ''}" data-gs-activity="${escapeHtml(it.key)}" data-gs-enable="${!on}" role="switch" aria-checked="${on}">
|
||
<span class="gs-knob"></span>
|
||
</button>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
list.innerHTML = '<div class="empty-box">获取失败:' + escapeHtml(e && e.message ? e.message : e) + '</div>';
|
||
}
|
||
}
|
||
|
||
async function loadGameSettings() {
|
||
gsSwitchTab(gsCurrentTab);
|
||
if (gsCurrentTab === 'channels') await gsLoadChannels();
|
||
else await gsLoadActivities();
|
||
}
|
||
|
||
// 刷新按钮
|
||
$('gsRefresh').addEventListener('click', () => {
|
||
if (gsCurrentTab === 'channels') gsLoadChannels();
|
||
else gsLoadActivities();
|
||
});
|
||
|
||
// Tab 切换
|
||
document.querySelectorAll('#gsTabs .st-tab').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
gsSwitchTab(el.dataset.gs);
|
||
if (gsCurrentTab === 'channels') gsLoadChannels();
|
||
else gsLoadActivities();
|
||
});
|
||
});
|
||
|
||
// 频道开关点击(事件委托)
|
||
$('gsChannelGrid').addEventListener('click', async (e) => {
|
||
const btn = e.target.closest('button[data-gs-channel]');
|
||
if (!btn) return;
|
||
const id = Number(btn.dataset.gsChannel);
|
||
const enable = btn.dataset.gsEnable === 'true';
|
||
btn.disabled = true;
|
||
btn.textContent = '处理中…';
|
||
try {
|
||
const r = await post('/api/game-settings/channel-switch', { channel_id: id, enabled: enable });
|
||
if (r.ok) {
|
||
gsToast(enable ? '频道已启用' : '频道已停用', 'success');
|
||
await gsLoadChannels();
|
||
} else {
|
||
gsToast('操作失败:' + (r.error || '未知错误'), 'error');
|
||
btn.disabled = false;
|
||
btn.textContent = enable ? '启用' : '停用';
|
||
}
|
||
} catch (e) {
|
||
gsToast('操作失败:' + (e && e.message ? e.message : e), 'error');
|
||
btn.disabled = false;
|
||
btn.textContent = enable ? '启用' : '停用';
|
||
}
|
||
});
|
||
|
||
// 活动开关点击(事件委托)
|
||
$('gsActivityList').addEventListener('click', async (e) => {
|
||
const sw = e.target.closest('button[data-gs-activity]');
|
||
if (!sw) return;
|
||
const key = sw.dataset.gsActivity;
|
||
const enable = sw.dataset.gsEnable === 'true';
|
||
sw.disabled = true;
|
||
try {
|
||
const r = await post('/api/game-settings/activity-switch', { key: key, enabled: enable });
|
||
if (r.ok) {
|
||
gsToast(enable ? '活动已开启' : '活动已关闭', 'success');
|
||
await gsLoadActivities();
|
||
} else {
|
||
gsToast('操作失败:' + (r.error || '未知错误'), 'error');
|
||
sw.disabled = false;
|
||
}
|
||
} catch (e) {
|
||
gsToast('操作失败:' + (e && e.message ? e.message : e), 'error');
|
||
sw.disabled = false;
|
||
}
|
||
});
|
||
|
||
// 顶栏拖动窗口(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);
|
||
})();
|