kingkont 0.20.61 → 0.20.62
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/package.json +1 -1
- package/renderer/board.js +53 -11
- package/renderer/settings.js +17 -118
- package/renderer/styles.css +32 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.62",
|
|
4
4
|
"description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
package/renderer/board.js
CHANGED
|
@@ -2603,10 +2603,13 @@ async function editNodeDescription(node) {
|
|
|
2603
2603
|
if (next == null) return;
|
|
2604
2604
|
node.description = next;
|
|
2605
2605
|
scheduleSave();
|
|
2606
|
-
|
|
2607
|
-
// Перерасчёт node.height — описание участвует в bbox'е (см.
|
|
2608
|
-
// fitNodeHeightToMedia в settings.js → расширяет node.height на descH).
|
|
2606
|
+
// Обновляем inline-description внутри ноды.
|
|
2609
2607
|
const el = canvas.querySelector(`.node[data-id="${node.id}"]`);
|
|
2608
|
+
const desc = el?.querySelector('.node-description-inline');
|
|
2609
|
+
if (desc) refreshInlineDescription(desc, node);
|
|
2610
|
+
// Подчищаем legacy outside-sidecar (если остался от старых версий).
|
|
2611
|
+
if (typeof removeNodeDescriptionEl === 'function') removeNodeDescriptionEl(node.id);
|
|
2612
|
+
// Перерасчёт node.height — inline-description участвует в chrome'е.
|
|
2610
2613
|
const media = el?.querySelector('img, video');
|
|
2611
2614
|
if (typeof fitNodeHeightToMedia === 'function' && el && media) {
|
|
2612
2615
|
fitNodeHeightToMedia(node, el, media);
|
|
@@ -2614,6 +2617,36 @@ async function editNodeDescription(node) {
|
|
|
2614
2617
|
}
|
|
2615
2618
|
window.editNodeDescription = editNodeDescription;
|
|
2616
2619
|
|
|
2620
|
+
// Inline-description: HTMLElement-helper. Видим только когда у юзера canModify
|
|
2621
|
+
// — placeholder «Добавить описание» имеет смысл только если он может его
|
|
2622
|
+
// заполнить (в view-only-template-режиме скрываем через CSS body.view-only-
|
|
2623
|
+
// mode .node-description-inline.empty).
|
|
2624
|
+
function refreshInlineDescription(el, node) {
|
|
2625
|
+
const text = (node.description || '').trim();
|
|
2626
|
+
if (text) {
|
|
2627
|
+
el.classList.remove('empty');
|
|
2628
|
+
if (el.textContent !== text) el.textContent = text;
|
|
2629
|
+
} else {
|
|
2630
|
+
el.classList.add('empty');
|
|
2631
|
+
if (el.textContent !== 'Добавить описание') el.textContent = 'Добавить описание';
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function attachInlineDescription(el, node) {
|
|
2635
|
+
refreshInlineDescription(el, node);
|
|
2636
|
+
el.addEventListener('mousedown', e => e.stopPropagation());
|
|
2637
|
+
el.addEventListener('click', e => {
|
|
2638
|
+
if (!el.classList.contains('empty')) return;
|
|
2639
|
+
e.stopPropagation();
|
|
2640
|
+
e.preventDefault();
|
|
2641
|
+
editNodeDescription(node);
|
|
2642
|
+
});
|
|
2643
|
+
el.addEventListener('dblclick', e => {
|
|
2644
|
+
e.stopPropagation();
|
|
2645
|
+
e.preventDefault();
|
|
2646
|
+
editNodeDescription(node);
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2617
2650
|
// Скопировать / вставить настройки сцены (aspectRatio + defaultPrompts).
|
|
2618
2651
|
// Хранение — in-memory state.clipboardSceneSettings (живёт до closeProject).
|
|
2619
2652
|
// Юзер: «сделай возможность скопировать настройки из одной сцены в другую».
|
|
@@ -4020,6 +4053,19 @@ async function createNodeEl(node) {
|
|
|
4020
4053
|
e.stopPropagation();
|
|
4021
4054
|
deleteNode(node, el);
|
|
4022
4055
|
});
|
|
4056
|
+
// Описание для image/video — теперь ВНУТРИ .node, как отдельная строка
|
|
4057
|
+
// между body и footer'ом. Раньше был outside-sidecar (.node-description-
|
|
4058
|
+
// outside) на canvas'е, который позиционировался у нижней грани bbox'а —
|
|
4059
|
+
// но при overlap'е соседних нод он визуально naehzhal на соседку (юзер:
|
|
4060
|
+
// «блок описания все еще наезжает на ноду которая лежит выше»). Inline-
|
|
4061
|
+
// вариант подчиняется flex-layout'у .node так же как image-body и
|
|
4062
|
+
// footer-row → не может вылезти за bbox.
|
|
4063
|
+
if (node.type === 'image' || node.type === 'video') {
|
|
4064
|
+
const desc = document.createElement('div');
|
|
4065
|
+
desc.className = 'node-description-inline';
|
|
4066
|
+
attachInlineDescription(desc, node);
|
|
4067
|
+
el.appendChild(desc);
|
|
4068
|
+
}
|
|
4023
4069
|
if (['image', 'video', 'audio'].includes(node.type) && node.generated) {
|
|
4024
4070
|
const footer = document.createElement('div');
|
|
4025
4071
|
footer.className = 'node-footer';
|
|
@@ -4115,14 +4161,10 @@ async function createNodeEl(node) {
|
|
|
4115
4161
|
e.stopPropagation();
|
|
4116
4162
|
showNodeContextMenu(node, e.clientX, e.clientY);
|
|
4117
4163
|
});
|
|
4118
|
-
//
|
|
4119
|
-
//
|
|
4120
|
-
//
|
|
4121
|
-
|
|
4122
|
-
// Передаём el-hint — .node ещё не в canvas DOM (caller сделает appendChild
|
|
4123
|
-
// после return), и без hint'а measure'а .node-footer не сработает →
|
|
4124
|
-
// placeholder перекрыл бы regen-кнопки.
|
|
4125
|
-
if (typeof ensureNodeDescriptionEl === 'function') ensureNodeDescriptionEl(node, el);
|
|
4164
|
+
// Подчищаем legacy outside-sidecar (мог остаться от старой версии этой
|
|
4165
|
+
// же сцены). Текущий рендер описания — inline-блок внутри .node (см.
|
|
4166
|
+
// attachInlineDescription выше; добавляется между body и footer).
|
|
4167
|
+
if (typeof removeNodeDescriptionEl === 'function') removeNodeDescriptionEl(node.id);
|
|
4126
4168
|
return el;
|
|
4127
4169
|
}
|
|
4128
4170
|
// =================== Контекстное меню ноды (ПКМ) ===================
|
package/renderer/settings.js
CHANGED
|
@@ -587,105 +587,19 @@ async function renderNodeBody(node, body) {
|
|
|
587
587
|
// «попробуем показать описание ВНЕ этой ноды, но так чтобы оно с ней
|
|
588
588
|
// перемещалось… совпадало с ней по ширине, а высота — сколько займет текст».
|
|
589
589
|
// Идемпотентно: пересоздаёт DOM, если описание изменилось/добавилось/удалилось.
|
|
590
|
-
|
|
590
|
+
// Legacy: outside-sidecar (`.node-description-outside`). Заменён на inline-
|
|
591
|
+
// description ВНУТРИ .node (см. board.js: attachInlineDescription). Сохраняем
|
|
592
|
+
// функцию как no-op (cleanup существующих) для обратной совместимости —
|
|
593
|
+
// другие модули могут вызывать. Inline-блок подчиняется flex-layout'у .node,
|
|
594
|
+
// не выходит за bbox и не наезжает на соседние ноды (юзер: «блок описания все
|
|
595
|
+
// еще наезжает на ноду которая лежит выше»).
|
|
596
|
+
function ensureNodeDescriptionEl(node /* , nodeElHint */) {
|
|
591
597
|
if (!node) return null;
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (node.type !== 'image' && node.type !== 'video') {
|
|
595
|
-
const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
596
|
-
if (existing) existing.remove();
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
const text = (node.description || '').trim();
|
|
600
|
-
// В view-only-режиме (share-link template) placeholder «Добавить описание»
|
|
601
|
-
// не показываем — он имеет смысл только если юзер может редактировать.
|
|
602
|
-
const canModify = (typeof state !== 'undefined' && state)
|
|
603
|
-
? (state.cloudCanModify !== false)
|
|
604
|
-
: true;
|
|
605
|
-
const isPlaceholder = !text;
|
|
606
|
-
if (isPlaceholder && !canModify) {
|
|
607
|
-
const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
608
|
-
if (existing) existing.remove();
|
|
609
|
-
return null;
|
|
610
|
-
}
|
|
611
|
-
// Если description (placeholder ИЛИ filled) спатиально перекрылся бы с
|
|
612
|
-
// bbox'ом другой ноды — не показываем его. Юзер: «блок описания наезжает
|
|
613
|
-
// на ноду которая лежит выше». Текст остаётся в node.description, виден
|
|
614
|
-
// через ПКМ-меню / edit-модалку.
|
|
615
|
-
if (Array.isArray(state?.currentBoard?.metadata?.nodes)) {
|
|
616
|
-
const footerHGuess = 28;
|
|
617
|
-
const descHGuess = isPlaceholder ? 29 : 50; // filled может быть выше
|
|
618
|
-
const descTop = node.y + node.height - footerHGuess - descHGuess - 2;
|
|
619
|
-
const descBot = node.y + node.height;
|
|
620
|
-
const descLeft = node.x;
|
|
621
|
-
const descRight = node.x + (node.width || 280);
|
|
622
|
-
const overlapsOther = state.currentBoard.metadata.nodes.some(m => {
|
|
623
|
-
if (m.id === node.id) return false;
|
|
624
|
-
const mW = m.width || 280, mH = m.height || 200;
|
|
625
|
-
return descLeft < m.x + mW && descRight > m.x
|
|
626
|
-
&& descTop < m.y + mH && descBot > m.y;
|
|
627
|
-
});
|
|
628
|
-
if (overlapsOther) {
|
|
629
|
-
const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
630
|
-
if (existing) existing.remove();
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
const sel = `.node-description-outside[data-desc-for="${node.id}"]`;
|
|
635
|
-
let el = canvasEl.querySelector(sel);
|
|
636
|
-
if (!el) {
|
|
637
|
-
el = document.createElement('div');
|
|
638
|
-
el.className = 'node-description-outside';
|
|
639
|
-
el.dataset.descFor = node.id;
|
|
640
|
-
canvasEl.appendChild(el);
|
|
641
|
-
el.addEventListener('dblclick', e => {
|
|
642
|
-
e.stopPropagation();
|
|
643
|
-
e.preventDefault();
|
|
644
|
-
if (typeof window.editNodeDescription === 'function') {
|
|
645
|
-
window.editNodeDescription(node);
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
el.addEventListener('click', e => {
|
|
649
|
-
if (!el.classList.contains('empty')) return;
|
|
650
|
-
e.stopPropagation();
|
|
651
|
-
e.preventDefault();
|
|
652
|
-
if (typeof window.editNodeDescription === 'function') {
|
|
653
|
-
window.editNodeDescription(node);
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
if (isPlaceholder) {
|
|
658
|
-
el.classList.add('empty');
|
|
659
|
-
if (el.textContent !== 'Добавить описание') el.textContent = 'Добавить описание';
|
|
660
|
-
} else {
|
|
661
|
-
el.classList.remove('empty');
|
|
662
|
-
if (el.textContent !== text) el.textContent = text;
|
|
663
|
-
}
|
|
664
|
-
positionNodeDescriptionEl(node, el, nodeElHint);
|
|
665
|
-
// Если в этот момент .node ещё не вставлен в canvas — querySelector
|
|
666
|
-
// вернёт null → footerH=0 → placeholder сел поверх footer'а. Через rAF
|
|
667
|
-
// делаем повторный pass: к этому моменту caller сделал appendChild,
|
|
668
|
-
// footer измерим, sidecar сдвинется чуть выше.
|
|
669
|
-
requestAnimationFrame(() => positionNodeDescriptionEl(node, el));
|
|
670
|
-
return el;
|
|
671
|
-
}
|
|
672
|
-
function positionNodeDescriptionEl(node, el, nodeElHint) {
|
|
673
|
-
if (!el) el = document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
674
|
-
if (!el) return;
|
|
675
|
-
const w = node.width || 280;
|
|
676
|
-
const h = node.height || 200;
|
|
677
|
-
// У image/video с node.generated есть .node-footer (regen-btn, история ←/→)
|
|
678
|
-
// — она у нижней грани. Поднимаем sidecar НАД footer'ом на footerH+2
|
|
679
|
-
// выше — иначе placeholder «Добавить описание» перекрывал бы regen-кнопки
|
|
680
|
-
// (юзер: «после появления кнопки добавить описание из ноды пропал ряд
|
|
681
|
-
// кнопок который делал перегенерацию»).
|
|
682
|
-
const nodeEl = nodeElHint || document.querySelector(`.node[data-id="${node.id}"]`);
|
|
683
|
-
const footerH = nodeEl?.querySelector('.node-footer')?.offsetHeight || 0;
|
|
684
|
-
const descH = el.offsetHeight || 0;
|
|
685
|
-
el.style.left = node.x + 'px';
|
|
686
|
-
el.style.top = (node.y + h - footerH - descH - 2) + 'px';
|
|
687
|
-
el.style.width = w + 'px';
|
|
598
|
+
removeNodeDescriptionEl(node.id);
|
|
599
|
+
return null;
|
|
688
600
|
}
|
|
601
|
+
// Legacy: no-op (inline-блок позиционируется flex-layout'ом).
|
|
602
|
+
function positionNodeDescriptionEl(/* node, el, nodeElHint */) { }
|
|
689
603
|
function removeNodeDescriptionEl(nodeId) {
|
|
690
604
|
const el = document.querySelector(`.node-description-outside[data-desc-for="${nodeId}"]`);
|
|
691
605
|
if (el) el.remove();
|
|
@@ -700,36 +614,21 @@ function fitNodeHeightToMedia(node, nodeEl, mediaEl) {
|
|
|
700
614
|
const natW = mediaEl.naturalWidth || mediaEl.videoWidth || 0;
|
|
701
615
|
const natH = mediaEl.naturalHeight || mediaEl.videoHeight || 0;
|
|
702
616
|
if (!natW || !natH) return;
|
|
703
|
-
// Chrome — header + footer (
|
|
617
|
+
// Chrome — header + inline-description + footer (всё что НЕ image-body).
|
|
618
|
+
// image-node padding=0, ничего не отъедает.
|
|
704
619
|
const headerH = nodeEl.querySelector('.node-header')?.offsetHeight || 0;
|
|
705
620
|
const footerH = nodeEl.querySelector('.node-footer')?.offsetHeight || 0;
|
|
706
|
-
const
|
|
621
|
+
const descInlineH = nodeEl.querySelector('.node-description-inline')?.offsetHeight || 0;
|
|
622
|
+
const chromeH = headerH + descInlineH + footerH;
|
|
707
623
|
// Берём актуальную width — node.width если задано, иначе текущий offsetWidth.
|
|
708
624
|
const w = node.width || nodeEl.offsetWidth;
|
|
709
625
|
if (!w) return;
|
|
710
|
-
|
|
711
|
-
// описание. Для placeholder'а «Добавить описание» bbox не расширяем —
|
|
712
|
-
// иначе нижняя грань ноды лезла бы в соседнюю ноду снизу (юзер: «блок
|
|
713
|
-
// описания все еще наезжает на ноду которая лежит выше»). Placeholder
|
|
714
|
-
// сидит у нижней грани в любом случае, но без расширения — слегка
|
|
715
|
-
// перекрывает картинку, что OK для подсказки.
|
|
716
|
-
const hasRealDesc = !!(node.description && node.description.trim());
|
|
717
|
-
const descEl = hasRealDesc
|
|
718
|
-
? document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`)
|
|
719
|
-
: null;
|
|
720
|
-
const descH = descEl ? descEl.offsetHeight + 4 : 0; // +4 чтобы зрительно отделять
|
|
721
|
-
const newH = Math.round(w * (natH / natW) + chromeH + descH);
|
|
626
|
+
const newH = Math.round(w * (natH / natW) + chromeH);
|
|
722
627
|
// 2px tolerance — не шумим в save/connections при суб-пиксельных колебаниях.
|
|
723
|
-
if (Math.abs(newH - (node.height || 0)) <= 2)
|
|
724
|
-
// Высоту не меняем, но позицию sidecar'а всё равно обновим — а вдруг
|
|
725
|
-
// описание появилось/изменилось до того как media догрузилось.
|
|
726
|
-
positionNodeDescriptionEl(node, null, nodeEl);
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
628
|
+
if (Math.abs(newH - (node.height || 0)) <= 2) return;
|
|
729
629
|
node.height = newH;
|
|
730
630
|
nodeEl.style.height = newH + 'px';
|
|
731
631
|
if (typeof renderConnections === 'function') renderConnections();
|
|
732
|
-
positionNodeDescriptionEl(node, null, nodeEl);
|
|
733
632
|
scheduleSave();
|
|
734
633
|
}
|
|
735
634
|
|
package/renderer/styles.css
CHANGED
|
@@ -110,45 +110,50 @@
|
|
|
110
110
|
width: 36px; height: 36px; flex-shrink: 0; object-fit: contain;
|
|
111
111
|
background: #1a1a1a; border-radius: 8px; padding: 4px;
|
|
112
112
|
}
|
|
113
|
-
/* Описание
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
/* Описание image/video-ноды — отдельная строка ВНУТРИ .node между
|
|
114
|
+
image-body и footer'ом. Раньше был outside-sidecar на canvas'е, но при
|
|
115
|
+
overlap'е соседних нод он визуально naehzhal на соседку (юзер: «блок
|
|
116
|
+
описания все еще наезжает на ноду которая лежит выше»). Inline-блок
|
|
117
|
+
подчиняется flex-layout'у .node так же как image-body и footer-row →
|
|
118
|
+
никогда не выходит за bbox.
|
|
119
|
+
fitNodeHeightToMedia учитывает .node-description-inline в chromeH
|
|
120
|
+
(header + descInline + footer), так что node.height = aspect-image-H +
|
|
121
|
+
chromeH. Положение в DOM: header → body → description-inline → footer. */
|
|
122
|
+
.node-description-inline {
|
|
123
|
+
padding: 5px 8px;
|
|
123
124
|
background: #1e1e1e; color: #ccc;
|
|
124
|
-
border-
|
|
125
|
-
font-size: 12px; line-height: 1.
|
|
125
|
+
border-top: 1px solid #383838;
|
|
126
|
+
font-size: 12px; line-height: 1.3;
|
|
126
127
|
white-space: pre-wrap; word-wrap: break-word;
|
|
127
128
|
box-sizing: border-box;
|
|
128
129
|
user-select: text; -webkit-user-select: text; cursor: text;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
/* Если real-описание длиннее, чем разумно для inline-блока — обрезаем
|
|
132
|
+
и показываем ellipsis. Полный текст — через ПКМ → «Изменить описание»
|
|
133
|
+
или edit-модалку. */
|
|
134
|
+
max-height: 4.2em;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
text-overflow: ellipsis;
|
|
132
137
|
}
|
|
133
|
-
/* Placeholder
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
показываем (см. ensureNodeDescriptionEl). */
|
|
138
|
-
.node-description-outside.empty {
|
|
138
|
+
/* Placeholder «Добавить описание» — приглушённый italic. В view-only-
|
|
139
|
+
template-режиме скрываем (placeholder имеет смысл только когда юзер
|
|
140
|
+
может его заполнить). */
|
|
141
|
+
.node-description-inline.empty {
|
|
139
142
|
color: #6a6a6a;
|
|
140
143
|
font-style: italic;
|
|
141
|
-
|
|
142
|
-
background: #1a1a1a;
|
|
143
|
-
box-shadow: none;
|
|
144
|
+
background: transparent;
|
|
144
145
|
cursor: pointer;
|
|
145
|
-
transition: color 0.12s,
|
|
146
|
+
transition: color 0.12s, background 0.12s;
|
|
146
147
|
}
|
|
147
|
-
.node-description-
|
|
148
|
+
.node-description-inline.empty:hover {
|
|
148
149
|
color: #aac8e6;
|
|
149
|
-
border-left-color: #4a6a9a;
|
|
150
150
|
background: #1e1e1e;
|
|
151
151
|
}
|
|
152
|
+
body.view-only-mode .node-description-inline.empty { display: none; }
|
|
153
|
+
/* Legacy: outside-sidecar теперь не создаётся, но если остался от старой
|
|
154
|
+
сессии в DOM — прячем. settings.js removeNodeDescriptionEl его удаляет;
|
|
155
|
+
это дополнительная страховка на случай race condition'ов. */
|
|
156
|
+
.node-description-outside { display: none !important; }
|
|
152
157
|
/* Legacy: оставляем класс для совместимости (вдруг где-то ещё ссылается). */
|
|
153
158
|
.node .node-description { display: none; }
|
|
154
159
|
|