kingkont 0.20.8 → 0.20.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +37 -15
- package/lib/providers.js +8 -3
- package/main.js +3 -4
- package/package.json +3 -3
- package/renderer/board.js +3632 -2943
- package/renderer/chat.js +4 -0
- package/renderer/cloudFs.js +174 -3
- package/renderer/cloudProjects.js +205 -93
- package/renderer/drawings.js +259 -0
- package/renderer/generate.js +5 -0
- package/renderer/notifyPanel.js +4 -1
- package/renderer/state.js +18 -10
- package/renderer/styles.css +85 -1
- package/server.js +35 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// renderer/drawings.js — рисование стрелок на холсте.
|
|
2
|
+
//
|
|
3
|
+
// Юзерская команда: «стрелка это нода». Поэтому drag по холсту создаёт
|
|
4
|
+
// обычную ноду `type:'drawing'` со своим bbox + points. Дальше всё что
|
|
5
|
+
// можно делать с нодой работает «бесплатно»: select, drag, Backspace/×,
|
|
6
|
+
// Cmd-Z (через pushHistory ДО мутации), сохранение в scene.json.
|
|
7
|
+
//
|
|
8
|
+
// 3 инструмента (toolbar [data-draw-tool="…"]):
|
|
9
|
+
// • pencil — тонкая серая линия
|
|
10
|
+
// • lead — средне-чёрная
|
|
11
|
+
// • paint — толстая жёлтая
|
|
12
|
+
//
|
|
13
|
+
// Хранение в node:
|
|
14
|
+
// { id, type:'drawing', tool, x, y, width, height, points: [[relX, relY],…] }
|
|
15
|
+
// points в системе bbox (0,0 = node.x, node.y). При drag меняется только x/y.
|
|
16
|
+
//
|
|
17
|
+
// API (window.boardDrawings):
|
|
18
|
+
// setTool(name) — включить/выключить режим
|
|
19
|
+
// renderInto(node, el) — добавить <svg> внутрь node-элемента (зовёт board.js
|
|
20
|
+
// из createNodeEl)
|
|
21
|
+
// smoothPath / arrowHead — экспортированы для возможной кастомизации.
|
|
22
|
+
|
|
23
|
+
(function () {
|
|
24
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
25
|
+
const _draw = { tool: null, current: null };
|
|
26
|
+
|
|
27
|
+
function _state() { try { return state; } catch { return null; } }
|
|
28
|
+
|
|
29
|
+
function _canvasCoord(ev) {
|
|
30
|
+
const c = document.getElementById('canvas');
|
|
31
|
+
if (!c) return { x: ev.clientX, y: ev.clientY };
|
|
32
|
+
const r = c.getBoundingClientRect();
|
|
33
|
+
const sx = c.offsetWidth ? r.width / c.offsetWidth : 1;
|
|
34
|
+
const sy = c.offsetHeight ? r.height / c.offsetHeight : 1;
|
|
35
|
+
return { x: (ev.clientX - r.left) / (sx || 1), y: (ev.clientY - r.top) / (sy || 1) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Catmull-Rom → cubic bezier для мягких линий.
|
|
39
|
+
function smoothPath(pts) {
|
|
40
|
+
if (!pts || pts.length === 0) return '';
|
|
41
|
+
if (pts.length === 1) return `M ${pts[0][0]} ${pts[0][1]}`;
|
|
42
|
+
if (pts.length === 2) return `M ${pts[0][0]} ${pts[0][1]} L ${pts[1][0]} ${pts[1][1]}`;
|
|
43
|
+
let d = `M ${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
|
|
44
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
45
|
+
const p0 = pts[i - 1] || pts[i];
|
|
46
|
+
const p1 = pts[i];
|
|
47
|
+
const p2 = pts[i + 1];
|
|
48
|
+
const p3 = pts[i + 2] || p2;
|
|
49
|
+
const c1x = p1[0] + (p2[0] - p0[0]) / 6;
|
|
50
|
+
const c1y = p1[1] + (p2[1] - p0[1]) / 6;
|
|
51
|
+
const c2x = p2[0] - (p3[0] - p1[0]) / 6;
|
|
52
|
+
const c2y = p2[1] - (p3[1] - p1[1]) / 6;
|
|
53
|
+
d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
|
|
54
|
+
}
|
|
55
|
+
return d;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function arrowHead(pts, size) {
|
|
59
|
+
if (!pts || pts.length < 2) return null;
|
|
60
|
+
const last = pts[pts.length - 1];
|
|
61
|
+
let prev = null;
|
|
62
|
+
for (let i = pts.length - 2; i >= 0; i--) {
|
|
63
|
+
if (Math.hypot(last[0] - pts[i][0], last[1] - pts[i][1]) > 12) { prev = pts[i]; break; }
|
|
64
|
+
}
|
|
65
|
+
if (!prev) prev = pts[0];
|
|
66
|
+
const ang = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
|
|
67
|
+
const len = size || 14, w = (size || 14) * 0.55;
|
|
68
|
+
const ax = last[0] - len * Math.cos(ang);
|
|
69
|
+
const ay = last[1] - len * Math.sin(ang);
|
|
70
|
+
const lx = ax + w * Math.cos(ang + Math.PI / 2);
|
|
71
|
+
const ly = ay + w * Math.sin(ang + Math.PI / 2);
|
|
72
|
+
const rx = ax - w * Math.cos(ang + Math.PI / 2);
|
|
73
|
+
const ry = ay - w * Math.sin(ang + Math.PI / 2);
|
|
74
|
+
return `${last[0].toFixed(1)},${last[1].toFixed(1)} ${lx.toFixed(1)},${ly.toFixed(1)} ${rx.toFixed(1)},${ry.toFixed(1)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolStyle(tool) {
|
|
78
|
+
return ({
|
|
79
|
+
pencil: { color: '#bbb', width: 1.5, opacity: 0.8, headSize: 10 },
|
|
80
|
+
lead: { color: '#2b2b2b', width: 2.6, opacity: 0.92, headSize: 13 },
|
|
81
|
+
paint: { color: '#e8b730', width: 5, opacity: 0.95, headSize: 18 },
|
|
82
|
+
})[tool] || { color: '#888', width: 2, opacity: 1, headSize: 12 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Рендер SVG внутрь node-элемента. Зовётся из board.js createNodeEl
|
|
86
|
+
// (когда node.type === 'drawing'). Идемпотентно — старый <svg.drawing-svg>
|
|
87
|
+
// удаляется перед вставкой нового.
|
|
88
|
+
function renderInto(node, el) {
|
|
89
|
+
const old = el.querySelector('svg.drawing-svg');
|
|
90
|
+
if (old) old.remove();
|
|
91
|
+
const w = node.width || 200, h = node.height || 100;
|
|
92
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
93
|
+
svg.setAttribute('class', 'drawing-svg');
|
|
94
|
+
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
95
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
96
|
+
svg.style.cssText = 'display:block;width:100%;height:100%;overflow:visible;pointer-events:none;';
|
|
97
|
+
const st = toolStyle(node.tool);
|
|
98
|
+
const color = node.color || st.color;
|
|
99
|
+
const line = document.createElementNS(SVG_NS, 'path');
|
|
100
|
+
line.setAttribute('class', 'line');
|
|
101
|
+
line.setAttribute('d', smoothPath(node.points || []));
|
|
102
|
+
line.setAttribute('fill', 'none');
|
|
103
|
+
line.setAttribute('stroke', color);
|
|
104
|
+
line.setAttribute('stroke-width', st.width);
|
|
105
|
+
line.setAttribute('stroke-opacity', st.opacity);
|
|
106
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
107
|
+
line.setAttribute('stroke-linejoin', 'round');
|
|
108
|
+
svg.appendChild(line);
|
|
109
|
+
if (node.arrow !== false) {
|
|
110
|
+
const head = arrowHead(node.points || [], st.headSize);
|
|
111
|
+
if (head) {
|
|
112
|
+
const tri = document.createElementNS(SVG_NS, 'polygon');
|
|
113
|
+
tri.setAttribute('class', 'head');
|
|
114
|
+
tri.setAttribute('points', head);
|
|
115
|
+
tri.setAttribute('fill', color);
|
|
116
|
+
tri.setAttribute('opacity', st.opacity);
|
|
117
|
+
svg.appendChild(tri);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
el.appendChild(svg);
|
|
121
|
+
return svg;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// === Temp-preview во время draw'а. SVG-overlay вне node-системы. ========
|
|
125
|
+
let _previewEl = null;
|
|
126
|
+
function _ensurePreview() {
|
|
127
|
+
let svg = document.getElementById('drawing-preview-svg');
|
|
128
|
+
if (svg) return svg;
|
|
129
|
+
const c = document.getElementById('canvas');
|
|
130
|
+
if (!c) return null;
|
|
131
|
+
svg = document.createElementNS(SVG_NS, 'svg');
|
|
132
|
+
svg.id = 'drawing-preview-svg';
|
|
133
|
+
svg.setAttribute('width', '6000');
|
|
134
|
+
svg.setAttribute('height', '4000');
|
|
135
|
+
svg.setAttribute('class', 'drawing-preview');
|
|
136
|
+
svg.style.cssText = 'position:absolute;left:0;top:0;pointer-events:none;z-index:1000;';
|
|
137
|
+
c.appendChild(svg);
|
|
138
|
+
return svg;
|
|
139
|
+
}
|
|
140
|
+
function _updatePreview(points, tool) {
|
|
141
|
+
const svg = _ensurePreview();
|
|
142
|
+
if (!svg) return;
|
|
143
|
+
if (_previewEl) _previewEl.remove();
|
|
144
|
+
const st = toolStyle(tool);
|
|
145
|
+
const g = document.createElementNS(SVG_NS, 'g');
|
|
146
|
+
const line = document.createElementNS(SVG_NS, 'path');
|
|
147
|
+
line.setAttribute('d', smoothPath(points));
|
|
148
|
+
line.setAttribute('fill', 'none');
|
|
149
|
+
line.setAttribute('stroke', st.color);
|
|
150
|
+
line.setAttribute('stroke-width', st.width);
|
|
151
|
+
line.setAttribute('stroke-opacity', st.opacity);
|
|
152
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
153
|
+
line.setAttribute('stroke-linejoin', 'round');
|
|
154
|
+
g.appendChild(line);
|
|
155
|
+
const head = arrowHead(points, st.headSize);
|
|
156
|
+
if (head) {
|
|
157
|
+
const tri = document.createElementNS(SVG_NS, 'polygon');
|
|
158
|
+
tri.setAttribute('points', head);
|
|
159
|
+
tri.setAttribute('fill', st.color);
|
|
160
|
+
tri.setAttribute('opacity', st.opacity);
|
|
161
|
+
g.appendChild(tri);
|
|
162
|
+
}
|
|
163
|
+
svg.appendChild(g);
|
|
164
|
+
_previewEl = g;
|
|
165
|
+
}
|
|
166
|
+
function _clearPreview() {
|
|
167
|
+
if (_previewEl) { _previewEl.remove(); _previewEl = null; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function setTool(tool) {
|
|
171
|
+
if (_draw.tool === tool) tool = null;
|
|
172
|
+
_draw.tool = tool;
|
|
173
|
+
const c = document.getElementById('canvas');
|
|
174
|
+
if (c) c.style.cursor = tool ? 'crosshair' : '';
|
|
175
|
+
document.querySelectorAll('[data-draw-tool]').forEach(b => {
|
|
176
|
+
b.classList.toggle('active', !!tool && b.dataset.drawTool === tool);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _onMouseDown(e) {
|
|
181
|
+
if (!_draw.tool) return;
|
|
182
|
+
if (e.button !== 0) return;
|
|
183
|
+
if (e.target.closest('.node')) return;
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
const { x, y } = _canvasCoord(e);
|
|
187
|
+
_draw.current = { tool: _draw.tool, points: [[x, y]] };
|
|
188
|
+
_updatePreview(_draw.current.points, _draw.current.tool);
|
|
189
|
+
document.addEventListener('mousemove', _onMouseMove);
|
|
190
|
+
document.addEventListener('mouseup', _onMouseUp, { once: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _onMouseMove(e) {
|
|
194
|
+
if (!_draw.current) return;
|
|
195
|
+
const { x, y } = _canvasCoord(e);
|
|
196
|
+
const last = _draw.current.points[_draw.current.points.length - 1];
|
|
197
|
+
if (Math.hypot(x - last[0], y - last[1]) < 1.5) return;
|
|
198
|
+
_draw.current.points.push([x, y]);
|
|
199
|
+
_updatePreview(_draw.current.points, _draw.current.tool);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function _onMouseUp() {
|
|
203
|
+
document.removeEventListener('mousemove', _onMouseMove);
|
|
204
|
+
_clearPreview();
|
|
205
|
+
if (!_draw.current) return;
|
|
206
|
+
const pts = _draw.current.points;
|
|
207
|
+
const tool = _draw.current.tool;
|
|
208
|
+
_draw.current = null;
|
|
209
|
+
if (pts.length < 3) return; // случайный клик → игнор
|
|
210
|
+
// BBox + relative-points + padding под линию и arrowhead.
|
|
211
|
+
const PAD = 22;
|
|
212
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
213
|
+
for (const [x, y] of pts) {
|
|
214
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
215
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
216
|
+
}
|
|
217
|
+
const x = Math.round(minX - PAD);
|
|
218
|
+
const y = Math.round(minY - PAD);
|
|
219
|
+
const width = Math.round(maxX - minX + PAD * 2);
|
|
220
|
+
const height = Math.round(maxY - minY + PAD * 2);
|
|
221
|
+
const rel = pts.map(([px, py]) => [+(px - x).toFixed(1), +(py - y).toFixed(1)]);
|
|
222
|
+
const node = {
|
|
223
|
+
id: crypto.randomUUID(),
|
|
224
|
+
type: 'drawing',
|
|
225
|
+
tool, x, y, width, height, points: rel,
|
|
226
|
+
};
|
|
227
|
+
const board = _state()?.currentBoard;
|
|
228
|
+
if (!board) return;
|
|
229
|
+
// Snapshot ДО мутации → Cmd-Z вернёт состояние без этой ноды.
|
|
230
|
+
if (typeof window.pushHistory === 'function') {
|
|
231
|
+
window.pushHistory('Рисование стрелки');
|
|
232
|
+
}
|
|
233
|
+
board.metadata.nodes.push(node);
|
|
234
|
+
// Рендерим ноду без полного renderCanvas (быстрее).
|
|
235
|
+
if (typeof window.createNodeEl === 'function') {
|
|
236
|
+
const el = await window.createNodeEl(node);
|
|
237
|
+
const canvas = document.getElementById('canvas');
|
|
238
|
+
if (canvas) canvas.appendChild(el);
|
|
239
|
+
}
|
|
240
|
+
if (typeof window.scheduleSave === 'function') window.scheduleSave();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _onKeyDown(e) {
|
|
244
|
+
if (e.key === 'Escape' && _draw.tool) setTool(null);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
window.boardDrawings = { setTool, renderInto, smoothPath, arrowHead, toolStyle };
|
|
248
|
+
|
|
249
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
250
|
+
const canvas = document.getElementById('canvas');
|
|
251
|
+
if (canvas) canvas.addEventListener('mousedown', _onMouseDown, true);
|
|
252
|
+
document.addEventListener('keydown', _onKeyDown);
|
|
253
|
+
document.querySelectorAll('[data-draw-tool]').forEach(b => {
|
|
254
|
+
b.addEventListener('click', () => setTool(b.dataset.drawTool));
|
|
255
|
+
});
|
|
256
|
+
// 🗑 «Очистить» из старой версии больше нет — каждая стрелка теперь
|
|
257
|
+
// нода, удаляется через Backspace/× или Cmd-Z прямо после draw'а.
|
|
258
|
+
});
|
|
259
|
+
})();
|
package/renderer/generate.js
CHANGED
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
// важен: см. <script> теги внизу index.html.
|
|
8
8
|
|
|
9
9
|
// =================== Add-menu (dblclick / ПКМ по пустому холсту) ===================
|
|
10
|
+
// В view-only режиме (расшаренный/публичный шаблон) меню «создать ноду»
|
|
11
|
+
// бессмысленно — юзер всё равно не может ничего сохранить. Не показываем.
|
|
12
|
+
function _isViewOnly() { return document.body.classList.contains('view-only-mode'); }
|
|
10
13
|
canvasWrap.addEventListener('dblclick', e => {
|
|
11
14
|
if (!state.currentBoard) return;
|
|
15
|
+
if (_isViewOnly()) return;
|
|
12
16
|
if (e.target.closest('.node, .conn, .resize-handle, .anchor')) return;
|
|
13
17
|
if (e.target.closest('#addMenu, .modal')) return;
|
|
14
18
|
showAddMenu(e.clientX, e.clientY);
|
|
15
19
|
});
|
|
16
20
|
canvasWrap.addEventListener('contextmenu', e => {
|
|
17
21
|
if (!state.currentBoard) return;
|
|
22
|
+
if (_isViewOnly()) return;
|
|
18
23
|
if (e.target.closest('.node, .conn, .resize-handle, .anchor, button, input, textarea, select')) return;
|
|
19
24
|
if (e.target.closest('#addMenu, .modal')) return;
|
|
20
25
|
e.preventDefault();
|
package/renderer/notifyPanel.js
CHANGED
|
@@ -617,6 +617,9 @@
|
|
|
617
617
|
ws.onclose = () => setTimeout(_connect, 5000);
|
|
618
618
|
ws.onerror = () => {};
|
|
619
619
|
}
|
|
620
|
-
|
|
620
|
+
// /ws живёт только в Electron-server'е (lib/wsHub.js → server.js). В web
|
|
621
|
+
// job'ы запускаются client-side (web-shim → polling chatium API), push-канал
|
|
622
|
+
// не нужен. Без guard'а — постоянные failed-WSS попытки на kingkont.ru/ws.
|
|
623
|
+
if (!window.__KINGKONT_WEB__) _connect();
|
|
621
624
|
});
|
|
622
625
|
})();
|
package/renderer/state.js
CHANGED
|
@@ -159,23 +159,31 @@ window.bgJobEnd = bgJobEnd;
|
|
|
159
159
|
window.bgJobsAll = bgJobsAll;
|
|
160
160
|
|
|
161
161
|
// === SYSTEM NOTIFICATION (HTML5 Notification API) ===
|
|
162
|
-
//
|
|
163
|
-
//
|
|
162
|
+
// Запрос разрешения ТОЛЬКО из user-gesture'а (Chrome иначе кидает
|
|
163
|
+
// console.error: «Notification prompting can only be done from a user
|
|
164
|
+
// gesture»). Привязываемся к первому click/keydown — он считается жестом.
|
|
164
165
|
let _notifPermAsked = false;
|
|
165
166
|
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
function _askNotifOnGesture() {
|
|
168
|
+
if (_notifPermAsked) return;
|
|
168
169
|
_notifPermAsked = true;
|
|
169
|
-
Notification.requestPermission().catch(() => {});
|
|
170
|
-
|
|
170
|
+
try { Notification.requestPermission().catch(() => {}); } catch {}
|
|
171
|
+
document.removeEventListener('click', _askNotifOnGesture);
|
|
172
|
+
document.removeEventListener('keydown', _askNotifOnGesture);
|
|
173
|
+
}
|
|
174
|
+
document.addEventListener('click', _askNotifOnGesture, { once: false });
|
|
175
|
+
document.addEventListener('keydown', _askNotifOnGesture, { once: false });
|
|
171
176
|
}
|
|
172
177
|
async function systemNotify(title, body, opts = {}) {
|
|
173
178
|
if (typeof Notification === 'undefined') return null;
|
|
174
179
|
let perm = Notification.permission;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
// Chrome требует чтобы requestPermission вызывался ТОЛЬКО из user-gesture'а
|
|
181
|
+
// (click/keydown handler). systemNotify часто зовут из фоновых событий
|
|
182
|
+
// (generation done, WS push) → запрос упадёт с console.error «Notification
|
|
183
|
+
// prompting can only be done from a user gesture». Без granted перм-а
|
|
184
|
+
// OS-нотификации не работают, но в notifyPanel событие УЖЕ добавлено.
|
|
185
|
+
// Если default — silently skip; запросим permission на первом «реальном»
|
|
186
|
+
// user-клике (см. _wireNotifPermPrompt ниже).
|
|
179
187
|
if (perm !== 'granted') {
|
|
180
188
|
// Без OS-нотификации — событие УЖЕ добавлено в notifyPanel через
|
|
181
189
|
// showToast-wrapper (single-source-of-truth). Раньше тут был fallback
|
package/renderer/styles.css
CHANGED
|
@@ -171,7 +171,10 @@
|
|
|
171
171
|
.sidebar-header .brand .sub.has-project {
|
|
172
172
|
color: #fff; text-transform: none; letter-spacing: 0;
|
|
173
173
|
font-size: 18px; font-weight: 600; line-height: 1.2;
|
|
174
|
-
|
|
174
|
+
/* Всегда одна строка — иначе после загрузки имя проекта переносится
|
|
175
|
+
и шапка прыгает вверх-вниз. ellipsis даёт `…` если не помещается. */
|
|
176
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
177
|
+
min-width: 0;
|
|
175
178
|
}
|
|
176
179
|
/* Имя выбранной сцены спрятано в шапке (юзер: «название выбранной сцены
|
|
177
180
|
скрывай»). Сама подсветка сцены остаётся в sidebar-list. */
|
|
@@ -492,6 +495,11 @@
|
|
|
492
495
|
.welcome-recent-grid {
|
|
493
496
|
display: grid;
|
|
494
497
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
498
|
+
/* grid-auto-rows: max-content — иначе cards (у которых дети все
|
|
499
|
+
position:absolute → 0 intrinsic height) получают row-height ~18px,
|
|
500
|
+
и aspect-ratio:4/3 рисует контент за пределы трека. Соседние cards
|
|
501
|
+
визуально наезжают друг на друга — именно это юзер видел в Electron. */
|
|
502
|
+
grid-auto-rows: max-content;
|
|
495
503
|
gap: 16px;
|
|
496
504
|
overflow-y: auto; overflow-x: hidden;
|
|
497
505
|
padding: 8px 52px 24px;
|
|
@@ -1054,6 +1062,82 @@
|
|
|
1054
1062
|
.connections-layer .line { stroke: #6a8aaa; stroke-width: 2; fill: none; pointer-events: none; }
|
|
1055
1063
|
.connections-layer .conn:hover .line { stroke: #f88; stroke-width: 3; }
|
|
1056
1064
|
.connections-layer .temp-line { stroke: #6a8aaa; stroke-width: 2; stroke-dasharray: 6 4; fill: none; pointer-events: none; }
|
|
1065
|
+
/* === Drawing-нода (стрелка). ============================================
|
|
1066
|
+
`.drawing-node` — обычная .node, но без header/body/anchor (см.
|
|
1067
|
+
createNodeEl branch). Прозрачный фон, без border'а в нормальном
|
|
1068
|
+
состоянии — видна только SVG-линия. На select подсвечиваем bbox
|
|
1069
|
+
стандартным outline'ом для консистентности с другими нодами. */
|
|
1070
|
+
.node.drawing-node {
|
|
1071
|
+
background: transparent; border: none; box-shadow: none; padding: 0;
|
|
1072
|
+
overflow: visible; cursor: move;
|
|
1073
|
+
}
|
|
1074
|
+
.node.drawing-node .drawing-svg { display: block; width: 100%; height: 100%; }
|
|
1075
|
+
.node.drawing-node.selected,
|
|
1076
|
+
.node.drawing-node:hover {
|
|
1077
|
+
outline: 1px dashed rgba(106,138,170,0.6); outline-offset: 2px;
|
|
1078
|
+
}
|
|
1079
|
+
.node.drawing-node.selected {
|
|
1080
|
+
outline: 1.5px solid #4af; outline-offset: 2px;
|
|
1081
|
+
}
|
|
1082
|
+
/* Toolbar drawing-buttons: active — подсветка как «нажата». */
|
|
1083
|
+
.toolbar [data-draw-tool] { font-size: 16px; padding: 4px 8px; }
|
|
1084
|
+
.toolbar [data-draw-tool].active {
|
|
1085
|
+
background: #3a5273; color: #fff; border-color: #4a6a9a;
|
|
1086
|
+
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15);
|
|
1087
|
+
}
|
|
1088
|
+
.toolbar-sep { display: inline-block; width: 1px; height: 20px; background: #333; margin: 0 6px; vertical-align: middle; }
|
|
1089
|
+
|
|
1090
|
+
/* === Edit-affordances visibility ========================================
|
|
1091
|
+
Юзер: «вид по умолчанию должен быть без элементов управления, они
|
|
1092
|
+
должны появляться потом». Поэтому:
|
|
1093
|
+
- .kk-edit-only по умолчанию СКРЫТО.
|
|
1094
|
+
- body.edit-mode реstoreит дефолтный display.
|
|
1095
|
+
edit-mode выставляется в cloudProjects.open() когда canModify=true.
|
|
1096
|
+
Для view-only mode edit-mode НЕ выставляется → все edit-кнопки остаются
|
|
1097
|
+
спрятанными, в toolbar показывается inline-баннер (.kk-view-only-banner).
|
|
1098
|
+
*/
|
|
1099
|
+
.kk-edit-only { display: none !important; }
|
|
1100
|
+
body.edit-mode .kk-edit-only { display: revert !important; }
|
|
1101
|
+
|
|
1102
|
+
/* === View-only mode (template-share / canModify=false) =================
|
|
1103
|
+
Sidebar остаётся (юзер навигирует по сценам), но edit-секции уже скрыты
|
|
1104
|
+
через .kk-edit-only выше. В toolbar показываем inline-banner вместо
|
|
1105
|
+
отсутствующих edit-кнопок. */
|
|
1106
|
+
.kk-view-only-banner { display: none; }
|
|
1107
|
+
body.view-only-mode .kk-view-only-banner {
|
|
1108
|
+
display: flex; align-items: center; gap: 14px;
|
|
1109
|
+
flex: 1; min-width: 0;
|
|
1110
|
+
padding: 4px 14px; margin: -4px 0;
|
|
1111
|
+
background: linear-gradient(90deg,#3a5f8c,#4a8ad6);
|
|
1112
|
+
color: #fff; border-radius: 6px;
|
|
1113
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1114
|
+
font-size: 14px;
|
|
1115
|
+
}
|
|
1116
|
+
.kk-view-only-banner .kk-vo-label { opacity: 0.95; }
|
|
1117
|
+
.kk-view-only-banner #viewOnlyCloneBtn {
|
|
1118
|
+
padding: 6px 14px; background: #fff; color: #3a5f8c;
|
|
1119
|
+
border: none; border-radius: 5px; cursor: pointer;
|
|
1120
|
+
font-weight: 600; font-size: 13px;
|
|
1121
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
|
|
1122
|
+
}
|
|
1123
|
+
.kk-view-only-banner #viewOnlyCloneBtn:hover { background: #f0f4fa; }
|
|
1124
|
+
/* В view-only ноды некликабельны — без drag/edit/resize/anchor.
|
|
1125
|
+
Картинки и видео по-прежнему кликабельны для fullscreen-просмотра. */
|
|
1126
|
+
body.view-only-mode .node {
|
|
1127
|
+
cursor: default !important; pointer-events: none;
|
|
1128
|
+
}
|
|
1129
|
+
body.view-only-mode .node img,
|
|
1130
|
+
body.view-only-mode .node video { pointer-events: auto; cursor: zoom-in; }
|
|
1131
|
+
body.view-only-mode .connections-layer .hit { pointer-events: none !important; }
|
|
1132
|
+
/* Дополнительно прячем in-node edit-аффордансы (× delete, resize-handle,
|
|
1133
|
+
anchor, draft-«Запустить» кнопки) — они генерятся JS'ом и не получают
|
|
1134
|
+
.kk-edit-only автоматом. */
|
|
1135
|
+
body.view-only-mode .node .node-header .delete,
|
|
1136
|
+
body.view-only-mode .node .resize-handle,
|
|
1137
|
+
body.view-only-mode .node .label-width-handle,
|
|
1138
|
+
body.view-only-mode .node .anchor,
|
|
1139
|
+
body.view-only-mode .node .gen-pending,
|
|
1140
|
+
body.view-only-mode .node-footer { display: none !important; }
|
|
1057
1141
|
|
|
1058
1142
|
/* Меню «что добавить» */
|
|
1059
1143
|
.add-menu {
|
package/server.js
CHANGED
|
@@ -462,6 +462,33 @@ async function handleProjectDelete(res, id) {
|
|
|
462
462
|
} catch (e) { sendError(res, e, 502); }
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// Прокси /api/proj/<action> → https://kingkont.ru/proj_<action>. Использует
|
|
466
|
+
// сохранённый Bearer-токен юзера (тот же что для всех chatium-вызовов).
|
|
467
|
+
// Сохраняем method, query-string и тело, передаём response как есть.
|
|
468
|
+
async function handleChatiumProjProxy(req, res, action, url) {
|
|
469
|
+
const s = getSettings();
|
|
470
|
+
if (!s.useChatium || !s.chatium?.token || !s.chatium?.base) {
|
|
471
|
+
return sendError(res, new Error('Войдите в KingKont — нет токена'), 401);
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const target = `${s.chatium.base.replace(/\/$/, '')}/app/spaces/server/api/proj_${action}${url.search || ''}`;
|
|
475
|
+
const init = {
|
|
476
|
+
method: req.method,
|
|
477
|
+
headers: {
|
|
478
|
+
'Authorization': `Bearer ${s.chatium.token}`,
|
|
479
|
+
'Content-Type': 'application/json',
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
if (req.method === 'POST' || req.method === 'PUT') {
|
|
483
|
+
init.body = JSON.stringify(await readJson(req));
|
|
484
|
+
}
|
|
485
|
+
const r = await fetch(target, init);
|
|
486
|
+
const text = await r.text();
|
|
487
|
+
res.writeHead(r.status, { 'Content-Type': r.headers.get('content-type') || 'application/json' });
|
|
488
|
+
res.end(text);
|
|
489
|
+
} catch (e) { sendError(res, e, 502); }
|
|
490
|
+
}
|
|
491
|
+
|
|
465
492
|
// ---- Project texts (.md контент text-нод). --------------------------------
|
|
466
493
|
// Зачем отдельный API: см. lib/providers.js → CHATIUM_PATHS.projectTexts.
|
|
467
494
|
|
|
@@ -715,6 +742,14 @@ async function _requestHandler(req, res) {
|
|
|
715
742
|
if (req.method === 'DELETE') return handleProjectDelete(res, decodeURIComponent(m[1]));
|
|
716
743
|
}
|
|
717
744
|
}
|
|
745
|
+
// Share-management endpoints. Прозрачный прокси на chatium /proj_*
|
|
746
|
+
// (новые TS-файлы — обход модульного кеша исходного projects.ts).
|
|
747
|
+
// Действия: setPublic / share / unshare / shares / clone / sharedWithMe.
|
|
748
|
+
{
|
|
749
|
+
const m = url.pathname.match(/^\/api\/proj\/(setPublic|share|unshare|shares|clone|sharedWithMe)$/);
|
|
750
|
+
if (m) return handleChatiumProjProxy(req, res, m[1], url);
|
|
751
|
+
}
|
|
752
|
+
|
|
718
753
|
// Project texts (.md контент). Отдельная таблица на сервере; клиент
|
|
719
754
|
// ссылается на записи по id из manifest'а board.texts[relPath] = textId.
|
|
720
755
|
if (req.method === 'POST' && url.pathname === '/api/project-texts') return handleProjectTextCreate(req, res);
|