/* 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 = '⏳ 游戏启动中…';
else if (state === 'started') btn.innerHTML = '✔ 游戏已启动';
else btn.innerHTML = '▶ 开始游戏';
}
$('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 => ``).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 = '
正在获取更新日志…
';
try {
const r = await api('/api/update/changelog');
if (!r.ok) {
box.innerHTML = '获取失败:' + escapeHtml(r.error || '未知错误') + '
';
return;
}
const rels = r.releases || [];
if (!rels.length) {
box.innerHTML = '暂无发布记录。
在 Gitea 仓库「发布 Release」并填写更新说明后,这里会自动显示。
';
return;
}
box.innerHTML = rels.map(rel => {
const badge = rel.prerelease ? '预发布' : '';
const body = (rel.body || '').replace(/\r?\n/g, '
');
const assets = (rel.assets || []).map(a =>
`⬇ ${escapeHtml(a.name)} (${fmt(a.size)} B)`
).join('');
const when = rel.published_at ? rel.published_at.replace('T', ' ').slice(0, 16) : '';
return `
${escapeHtml(rel.tag || rel.name || '')}
${escapeHtml(rel.name || '')}
${badge}
${escapeHtml(when)}
${body}
${assets ? '
' + assets + '
' : ''}
在浏览器中查看 ↗
`;
}).join('');
} catch (e) {
box.innerHTML = '获取失败:' + escapeHtml(e && e.message ? e.message : e) + '
';
}
}
$('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 = '选择方案后显示装备快照;点击「保存当前装备」从服务端读取。
';
} else {
s.Slots.forEach(it => {
const d = document.createElement('div');
d.className = 'lo-slot';
d.innerHTML = `槽位 ${it.slot}${it.item_id}`;
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 = '加载中…
';
try {
const r = await api('/api/game-settings/channel-switches');
if (!r.ok) {
grid.innerHTML = '获取失败:' + escapeHtml(r.error || '未知错误') + '
';
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 = '暂无频道数据。请确认服务端已启动。
';
return;
}
grid.innerHTML = list.map(ch => {
const on = !!ch.enabled;
return `
${escapeHtml(ch.name || ('频道 ' + ch.id))}
${on ? '启用' : '停用'}
ID ${ch.id} · ${gsChannelTypeText(ch.type)}${ch.group ? ' · ' + escapeHtml(ch.group) : ''}${ch.port ? ' · 端口 ' + ch.port : ''}
`;
}).join('');
} catch (e) {
grid.innerHTML = '获取失败:' + escapeHtml(e && e.message ? e.message : e) + '
';
}
}
async function gsLoadActivities() {
const list = $('gsActivityList');
const summary = $('gsActivitySummary');
if (!list) return;
list.innerHTML = '加载中…
';
try {
const r = await api('/api/game-settings/activity-switches');
if (!r.ok) {
list.innerHTML = '获取失败:' + escapeHtml(r.error || '未知错误') + '
';
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 = '暂无活动数据。请确认服务端已启动。
';
return;
}
list.innerHTML = items.map(it => {
const on = !!it.enabled;
return `
${escapeHtml(it.title || it.key)}
${escapeHtml(it.description || '')}
`;
}).join('');
} catch (e) {
list.innerHTML = '获取失败:' + escapeHtml(e && e.message ? e.message : e) + '
';
}
}
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);
})();