/* 90轮回启动器 · 单窗口页面与工具整合 */ (function () { const $ = (id) => document.getElementById(id); const pages = ['start','changelog','gm','loadout','gamesettings']; let shellGmInit = false; let loConfig = null, loPage = 0, loScheme = -1; let loCooldownUntil = 0, loCooldownCharacter = '', loCooldownTimer = null; let hotkeyCapture = false; const crumb = {start:'开始 / 本地服务端',changelog:'游戏更新',gm:'GM工具',loadout:'工具 / 一键换装',gamesettings:'工具 / 游戏设置'}; 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 === '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(); }); // ==================== 游戏设置(频道开关 / 活动开关) ==================== 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); })();