|
|
@@ -49,14 +49,21 @@ class SymjaConsole {
|
|
|
// 顶部导航按钮
|
|
|
this.helpBtn?.addEventListener('click', () => this.showHelp());
|
|
|
this.pluginsBtn?.addEventListener('click', () => this.showPluginPanel());
|
|
|
+ this.settingsBtn?.addEventListener('click', () => this.showSettings());
|
|
|
|
|
|
- // MCP 状态更新
|
|
|
- this.updateMcpStatus();
|
|
|
- setInterval(() => this.updateMcpStatus(), 3000);
|
|
|
+ // 设置面板
|
|
|
+ document.getElementById('closeSettings')?.addEventListener('click', () => this.hideSettings());
|
|
|
+ document.getElementById('applySettings')?.addEventListener('click', () => this.applySettings());
|
|
|
+
|
|
|
+ // 加载已保存的设置
|
|
|
+ this.loadSettings();
|
|
|
|
|
|
- // MCP 事件同步(每秒轮询)
|
|
|
+ // MCP 状态更新和事件轮询(仅在服务器开启时运行)
|
|
|
this.mcpEventCount = 0;
|
|
|
- setInterval(() => this.pollMcpEvents(), 1000);
|
|
|
+ this.mcpPollTimer = null;
|
|
|
+ this.mcpStatusTimer = null;
|
|
|
+ this.updateMcpStatus();
|
|
|
+ this.startMcpPollingIfNeeded();
|
|
|
|
|
|
// 快捷键工具栏
|
|
|
this.initShortcutsBar();
|
|
|
@@ -420,7 +427,20 @@ class SymjaConsole {
|
|
|
}
|
|
|
|
|
|
showWelcome() {
|
|
|
- // 不显示欢迎消息
|
|
|
+ const line = document.createElement('div');
|
|
|
+ line.className = 'terminal-line';
|
|
|
+ line.innerHTML = `<div class="welcome-message">
|
|
|
+ <h2>🧮 某人的计算器 v1.2</h2>
|
|
|
+ <p>基于 Symja 的交互式符号计算控制台</p>
|
|
|
+ <p><strong>快速开始:</strong></p>
|
|
|
+ <p><code>2+2</code> 算术运算</p>
|
|
|
+ <p><code>D[x^3, x]</code> 求导</p>
|
|
|
+ <p><code>Integrate[x^2, {x,0,1}]</code> 定积分</p>
|
|
|
+ <p><code>Solve[x^2-5*x+6==0, x]</code> 解方程</p>
|
|
|
+ <p><code>Plot[Sin[x], {x,0,2*Pi}]</code> 函数绘图</p>
|
|
|
+ <p style="margin-top:12px">输入 <code>help</code> 查看完整文档 · 输入 <code>plugins</code> 管理插件</p>
|
|
|
+ </div>`;
|
|
|
+ this.output.appendChild(line);
|
|
|
}
|
|
|
|
|
|
evaluate() {
|
|
|
@@ -482,7 +502,7 @@ class SymjaConsole {
|
|
|
this.scrollToBottom();
|
|
|
}
|
|
|
|
|
|
- addPlotOutput(plotJson, count) {
|
|
|
+ addPlotOutput(plotJson, count, labels) {
|
|
|
const data = JSON.parse(plotJson);
|
|
|
const line = document.createElement('div');
|
|
|
line.className = 'terminal-line';
|
|
|
@@ -492,13 +512,37 @@ class SymjaConsole {
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
canvas.className = 'plot-canvas';
|
|
|
- canvas.width = 720;
|
|
|
- canvas.height = 500;
|
|
|
+ const dpr = window.devicePixelRatio || 1;
|
|
|
+ canvas.width = 720 * dpr;
|
|
|
+ canvas.height = 500 * dpr;
|
|
|
+ canvas.style.width = '720px';
|
|
|
+ canvas.style.height = '500px';
|
|
|
container.appendChild(canvas);
|
|
|
|
|
|
- // 保存按钮
|
|
|
+ // 图例(多曲线时自动显示)
|
|
|
+ let legendVisible = true;
|
|
|
+ let legendEl = null;
|
|
|
+ if (data.elements && data.elements.length > 1) {
|
|
|
+ legendEl = this.buildLegend(data, labels);
|
|
|
+ if (legendEl) container.appendChild(legendEl);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存按钮 + 图例开关
|
|
|
const actions = document.createElement('div');
|
|
|
actions.className = 'plot-actions';
|
|
|
+
|
|
|
+ if (legendEl) {
|
|
|
+ const legendBtn = document.createElement('button');
|
|
|
+ legendBtn.className = 'plot-save-btn';
|
|
|
+ legendBtn.textContent = '🏷 图例';
|
|
|
+ legendBtn.onclick = () => {
|
|
|
+ legendVisible = !legendVisible;
|
|
|
+ legendEl.style.display = legendVisible ? 'flex' : 'none';
|
|
|
+ legendBtn.style.opacity = legendVisible ? '1' : '0.5';
|
|
|
+ };
|
|
|
+ actions.appendChild(legendBtn);
|
|
|
+ }
|
|
|
+
|
|
|
const saveBtn = document.createElement('button');
|
|
|
saveBtn.className = 'plot-save-btn';
|
|
|
saveBtn.textContent = '💾 保存图片';
|
|
|
@@ -509,13 +553,30 @@ class SymjaConsole {
|
|
|
line.appendChild(container);
|
|
|
this.output.appendChild(line);
|
|
|
|
|
|
- // 初始化渲染器
|
|
|
if (!this.plotRenderer) {
|
|
|
this.plotRenderer = new PlotRenderer();
|
|
|
}
|
|
|
this.plotRenderer.render(canvas, data);
|
|
|
}
|
|
|
|
|
|
+ buildLegend(data, labels) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.className = 'plot-legend';
|
|
|
+ const colors = [
|
|
|
+ [0.369,0.507,0.710],[0.851,0.208,0.125],[0.267,0.612,0.329],
|
|
|
+ [0.737,0.349,0.800],[1.0,0.608,0.0]
|
|
|
+ ];
|
|
|
+ data.elements.forEach((el, i) => {
|
|
|
+ const c = el.color || colors[i % colors.length];
|
|
|
+ const rgb = `rgb(${Math.round(c[0]*255)},${Math.round(c[1]*255)},${Math.round(c[2]*255)})`;
|
|
|
+ const item = document.createElement('span');
|
|
|
+ item.className = 'legend-item';
|
|
|
+ item.innerHTML = `<span class="legend-swatch" style="background:${rgb}"></span>${labels ? labels[i] : '曲线'+(i+1)}`;
|
|
|
+ div.appendChild(item);
|
|
|
+ });
|
|
|
+ return div;
|
|
|
+ }
|
|
|
+
|
|
|
savePlot(canvas) {
|
|
|
try {
|
|
|
const dataUrl = canvas.toDataURL('image/png');
|
|
|
@@ -1112,20 +1173,29 @@ class SymjaConsole {
|
|
|
}
|
|
|
|
|
|
loadHistory() {
|
|
|
+ try {
|
|
|
+ if (typeof Android !== 'undefined' && Android.loadHistory) {
|
|
|
+ const saved = Android.loadHistory();
|
|
|
+ return saved ? JSON.parse(saved) : [];
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ // 回退到 localStorage
|
|
|
try {
|
|
|
const saved = localStorage.getItem('symja_history');
|
|
|
return saved ? JSON.parse(saved) : [];
|
|
|
- } catch (e) {
|
|
|
- return [];
|
|
|
- }
|
|
|
+ } catch (e) { return []; }
|
|
|
}
|
|
|
|
|
|
saveHistory() {
|
|
|
+ const json = JSON.stringify(this.history);
|
|
|
try {
|
|
|
- localStorage.setItem('symja_history', JSON.stringify(this.history));
|
|
|
- } catch (e) {
|
|
|
- // 忽略存储错误
|
|
|
- }
|
|
|
+ if (typeof Android !== 'undefined' && Android.saveHistory) {
|
|
|
+ Android.saveHistory(json);
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ try {
|
|
|
+ localStorage.setItem('symja_history', json);
|
|
|
+ } catch (e) {}
|
|
|
}
|
|
|
|
|
|
saveSession(name) {
|
|
|
@@ -1446,9 +1516,92 @@ class SymjaConsole {
|
|
|
}
|
|
|
|
|
|
showSettings() {
|
|
|
- // 设置功能待实现
|
|
|
- this.addOutput('⚙️ 设置功能即将推出...', 'info');
|
|
|
+ const panel = document.getElementById('settingsPanel');
|
|
|
+ if (!panel) return;
|
|
|
+ this.hideKeyboard();
|
|
|
+ document.getElementById('settingMcpEnabled').value = this.getMcpEnabled() ? '1' : '0';
|
|
|
+ document.getElementById('settingMcpPort').value = this.getSetting('mcpPort', '8734');
|
|
|
+ document.getElementById('settingFontSize').value = this.getSetting('fontSize', '14');
|
|
|
+ panel.classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ hideSettings() {
|
|
|
+ document.getElementById('settingsPanel')?.classList.remove('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ applySettings() {
|
|
|
+ const wasEnabled = this.getMcpEnabled();
|
|
|
+ const enabled = document.getElementById('settingMcpEnabled').value === '1';
|
|
|
+ const port = document.getElementById('settingMcpPort').value;
|
|
|
+ const fontSize = document.getElementById('settingFontSize').value;
|
|
|
+ this.saveSetting('mcpPort', port);
|
|
|
+ this.saveSetting('fontSize', fontSize);
|
|
|
+ this.setMcpEnabled(enabled);
|
|
|
+ document.querySelector('.terminal-output').style.fontSize = fontSize + 'px';
|
|
|
+ document.querySelector('.terminal-input').style.fontSize = fontSize + 'px';
|
|
|
+ this.hideSettings();
|
|
|
+ this.addOutput('⚙️ 设置已应用', 'help');
|
|
|
this.scrollToBottom();
|
|
|
+
|
|
|
+ // MCP 状态变更时更新轮询
|
|
|
+ if (enabled !== wasEnabled) {
|
|
|
+ if (enabled) {
|
|
|
+ this.startMcpPollingIfNeeded();
|
|
|
+ } else {
|
|
|
+ this.stopMcpPolling();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getMcpEnabled() {
|
|
|
+ try {
|
|
|
+ if (typeof Android !== 'undefined' && Android.getMcpEnabled !== undefined) {
|
|
|
+ return Android.getMcpEnabled();
|
|
|
+ }
|
|
|
+ } catch(e) {}
|
|
|
+ return this.getSetting('mcpEnabled', '1') === '1';
|
|
|
+ }
|
|
|
+
|
|
|
+ setMcpEnabled(val) {
|
|
|
+ this.saveSetting('mcpEnabled', val ? '1' : '0');
|
|
|
+ try {
|
|
|
+ if (typeof Android !== 'undefined' && Android.setMcpEnabled) {
|
|
|
+ Android.setMcpEnabled(val);
|
|
|
+ }
|
|
|
+ } catch(e) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ startMcpPollingIfNeeded() {
|
|
|
+ if (!this.getMcpEnabled()) return;
|
|
|
+ if (!this.mcpPollTimer) {
|
|
|
+ this.mcpPollTimer = setInterval(() => this.pollMcpEvents(), 1000);
|
|
|
+ }
|
|
|
+ if (!this.mcpStatusTimer) {
|
|
|
+ this.mcpStatusTimer = setInterval(() => this.updateMcpStatus(), 3000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ stopMcpPolling() {
|
|
|
+ if (this.mcpPollTimer) { clearInterval(this.mcpPollTimer); this.mcpPollTimer = null; }
|
|
|
+ if (this.mcpStatusTimer) { clearInterval(this.mcpStatusTimer); this.mcpStatusTimer = null; }
|
|
|
+ const status = document.getElementById('mcpStatus');
|
|
|
+ if (status) { status.className = 'mcp-status'; status.title = 'MCP 已关闭'; }
|
|
|
+ }
|
|
|
+
|
|
|
+ getSetting(key, def) {
|
|
|
+ try { return localStorage.getItem('ijs_' + key) || def; } catch(e) { return def; }
|
|
|
+ }
|
|
|
+
|
|
|
+ saveSetting(key, val) {
|
|
|
+ try { localStorage.setItem('ijs_' + key, val); } catch(e) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ loadSettings() {
|
|
|
+ const fs = this.getSetting('fontSize', '14');
|
|
|
+ try {
|
|
|
+ document.querySelector('.terminal-output').style.fontSize = fs + 'px';
|
|
|
+ document.querySelector('.terminal-input').style.fontSize = fs + 'px';
|
|
|
+ } catch(e) {}
|
|
|
}
|
|
|
|
|
|
clear() {
|
|
|
@@ -1482,12 +1635,11 @@ class SymjaConsole {
|
|
|
}
|
|
|
|
|
|
loadExecutionCount() {
|
|
|
- // 每次打开应用都从 1 开始
|
|
|
- return 1;
|
|
|
+ return 1; // 每次打开应用从1开始
|
|
|
}
|
|
|
|
|
|
saveExecutionCount() {
|
|
|
- // 不再保存到 localStorage,每次打开应用都重置
|
|
|
+ // 不持久化编号,每次打开都重置
|
|
|
}
|
|
|
|
|
|
navigateHistory(direction) {
|
|
|
@@ -1886,9 +2038,6 @@ class SymjaConsole {
|
|
|
this.addOutput(ev.result, 'mcp-result', { seq: this.mcpEventCount });
|
|
|
}
|
|
|
});
|
|
|
- this.executionCount++;
|
|
|
- this.saveExecutionCount();
|
|
|
- this.updatePrompt();
|
|
|
this.scrollToBottom();
|
|
|
} catch (e) {
|
|
|
// Ignore polling errors
|
|
|
@@ -2151,53 +2300,55 @@ class PlotRenderer {
|
|
|
// ── 手势 ──────────────────────────────────────────
|
|
|
bindGestures() {
|
|
|
const canvas = this.canvas;
|
|
|
- let lastDist = 0, lastX = 0, lastY = 0;
|
|
|
- let lastTap = 0;
|
|
|
+ // 移除旧监听器防止泄露
|
|
|
+ if (this._touchHandlers) {
|
|
|
+ canvas.removeEventListener('touchstart', this._touchHandlers.start);
|
|
|
+ canvas.removeEventListener('touchmove', this._touchHandlers.move);
|
|
|
+ canvas.removeEventListener('touchend', this._touchHandlers.end);
|
|
|
+ }
|
|
|
+ let lastDist = 0, lastX = 0, lastY = 0, lastTap = 0;
|
|
|
+ const self = this;
|
|
|
|
|
|
- canvas.addEventListener('touchstart', (e) => {
|
|
|
+ const onStart = (e) => {
|
|
|
if (e.touches.length === 2) {
|
|
|
- lastDist = Math.hypot(
|
|
|
- e.touches[0].clientX - e.touches[1].clientX,
|
|
|
- e.touches[0].clientY - e.touches[1].clientY
|
|
|
- );
|
|
|
+ lastDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
|
|
} else if (e.touches.length === 1) {
|
|
|
lastX = e.touches[0].clientX;
|
|
|
lastY = e.touches[0].clientY;
|
|
|
}
|
|
|
e.preventDefault();
|
|
|
- }, { passive: false });
|
|
|
-
|
|
|
- canvas.addEventListener('touchmove', (e) => {
|
|
|
+ };
|
|
|
+ const onMove = (e) => {
|
|
|
if (e.touches.length === 2) {
|
|
|
- const dist = Math.hypot(
|
|
|
- e.touches[0].clientX - e.touches[1].clientX,
|
|
|
- e.touches[0].clientY - e.touches[1].clientY
|
|
|
- );
|
|
|
+ const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
|
|
const scale = dist / lastDist;
|
|
|
- this.transform.sx = Math.max(0.1, Math.min(20, this.transform.sx / scale));
|
|
|
- this.transform.sy = Math.max(0.1, Math.min(20, this.transform.sy / scale));
|
|
|
+ self.transform.sx = Math.max(0.1, Math.min(20, self.transform.sx / scale));
|
|
|
+ self.transform.sy = Math.max(0.1, Math.min(20, self.transform.sy / scale));
|
|
|
lastDist = dist;
|
|
|
- this.draw();
|
|
|
+ self.draw();
|
|
|
} else if (e.touches.length === 1) {
|
|
|
const dx = (e.touches[0].clientX - lastX) / canvas.clientWidth;
|
|
|
const dy = (e.touches[0].clientY - lastY) / canvas.clientHeight;
|
|
|
- this.transform.ox -= dx / this.transform.sx;
|
|
|
- this.transform.oy += dy / this.transform.sy;
|
|
|
+ self.transform.ox -= dx / self.transform.sx;
|
|
|
+ self.transform.oy += dy / self.transform.sy;
|
|
|
lastX = e.touches[0].clientX;
|
|
|
lastY = e.touches[0].clientY;
|
|
|
- this.draw();
|
|
|
+ self.draw();
|
|
|
}
|
|
|
e.preventDefault();
|
|
|
- }, { passive: false });
|
|
|
-
|
|
|
- canvas.addEventListener('touchend', (e) => {
|
|
|
+ };
|
|
|
+ const onEnd = (e) => {
|
|
|
const now = Date.now();
|
|
|
if (now - lastTap < 300) {
|
|
|
- // 双击重置
|
|
|
- this.transform = { ox: 0, oy: 0, sx: 1, sy: 1 };
|
|
|
- this.draw();
|
|
|
+ self.transform = { ox: 0, oy: 0, sx: 1, sy: 1 };
|
|
|
+ self.draw();
|
|
|
}
|
|
|
lastTap = now;
|
|
|
- });
|
|
|
+ };
|
|
|
+
|
|
|
+ canvas.addEventListener('touchstart', onStart, { passive: false });
|
|
|
+ canvas.addEventListener('touchmove', onMove, { passive: false });
|
|
|
+ canvas.addEventListener('touchend', onEnd);
|
|
|
+ this._touchHandlers = { start: onStart, move: onMove, end: onEnd };
|
|
|
}
|
|
|
}
|