kingkont 0.7.59 → 0.7.61
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 +2 -0
- package/package.json +1 -1
- package/renderer/board.js +86 -21
- package/renderer/media.js +65 -5
- package/renderer/styles.css +13 -0
package/index.html
CHANGED
|
@@ -457,6 +457,8 @@
|
|
|
457
457
|
<!-- ===== Полноэкранный просмотр (image/video) ===== -->
|
|
458
458
|
<div class="fs-modal hidden" id="fsModal">
|
|
459
459
|
<div class="fs-stage" id="fsStage"></div>
|
|
460
|
+
<button id="fsPrev" title="Предыдущая (←)" class="fs-nav fs-nav-prev">‹</button>
|
|
461
|
+
<button id="fsNext" title="Следующая (→)" class="fs-nav fs-nav-next">›</button>
|
|
460
462
|
<button id="fsClose" title="Закрыть (Esc)">×</button>
|
|
461
463
|
</div>
|
|
462
464
|
|
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -1638,33 +1638,94 @@ function showNodeLogs(node) {
|
|
|
1638
1638
|
$('logsClose').addEventListener('click', () => $('logsModal').classList.add('hidden'));
|
|
1639
1639
|
|
|
1640
1640
|
// =================== Полноэкранный просмотр ===================
|
|
1641
|
+
// Текущая нода в полноэкранном просмотре + список соседей того же type
|
|
1642
|
+
// для навигации стрелками. Список считаем один раз при openFullscreen
|
|
1643
|
+
// чтобы не пересортировывать на каждом prev/next.
|
|
1644
|
+
let _fsCurrent = null;
|
|
1645
|
+
let _fsSiblings = [];
|
|
1646
|
+
|
|
1647
|
+
// Сортировка нод «слева направо, сверху вниз» row-major.
|
|
1648
|
+
// Группируем по строкам через round(y/ROW_HEIGHT) bucket — sort transitive
|
|
1649
|
+
// (важно, иначе порядок непредсказуемый при больших спредах по y).
|
|
1650
|
+
const FS_ROW_HEIGHT = 100;
|
|
1651
|
+
function _fsSortRowMajor(nodes) {
|
|
1652
|
+
return nodes.slice().sort((a, b) => {
|
|
1653
|
+
const ra = Math.round((a.y || 0) / FS_ROW_HEIGHT);
|
|
1654
|
+
const rb = Math.round((b.y || 0) / FS_ROW_HEIGHT);
|
|
1655
|
+
if (ra !== rb) return ra - rb;
|
|
1656
|
+
return (a.x || 0) - (b.x || 0);
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1641
1660
|
async function openFullscreen(node) {
|
|
1642
1661
|
const stage = $('fsStage');
|
|
1643
1662
|
stage.innerHTML = '';
|
|
1644
1663
|
try {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
img.src = url;
|
|
1655
|
-
stage.appendChild(img);
|
|
1656
|
-
} else if (node.type === 'video') {
|
|
1657
|
-
const v = document.createElement('video');
|
|
1658
|
-
v.src = url;
|
|
1659
|
-
v.controls = true;
|
|
1660
|
-
v.autoplay = true;
|
|
1661
|
-
stage.appendChild(v);
|
|
1664
|
+
// Собираем siblings того же type на текущей доске для навигации.
|
|
1665
|
+
// image-нода → все остальные image; video → video. Без файла исключаем.
|
|
1666
|
+
if (state.currentBoard?.metadata?.nodes) {
|
|
1667
|
+
const sameType = state.currentBoard.metadata.nodes.filter(n =>
|
|
1668
|
+
n.type === node.type && n.file
|
|
1669
|
+
);
|
|
1670
|
+
_fsSiblings = _fsSortRowMajor(sameType);
|
|
1671
|
+
} else {
|
|
1672
|
+
_fsSiblings = [node];
|
|
1662
1673
|
}
|
|
1674
|
+
_fsCurrent = node;
|
|
1675
|
+
await _fsLoadCurrent();
|
|
1676
|
+
_fsUpdateNavButtons();
|
|
1663
1677
|
$('fsModal').classList.remove('hidden');
|
|
1664
1678
|
} catch (e) {
|
|
1665
1679
|
console.error('openFullscreen failed:', e);
|
|
1666
1680
|
}
|
|
1667
1681
|
}
|
|
1682
|
+
|
|
1683
|
+
async function _fsLoadCurrent() {
|
|
1684
|
+
const node = _fsCurrent;
|
|
1685
|
+
const stage = $('fsStage');
|
|
1686
|
+
stage.innerHTML = '';
|
|
1687
|
+
if (!node) return;
|
|
1688
|
+
let url = state.currentBoard?.urls?.[node.file];
|
|
1689
|
+
if (!url && state.currentBoard?.handle) {
|
|
1690
|
+
const fh = await resolveBoardFile(state.currentBoard.handle, node.file);
|
|
1691
|
+
url = URL.createObjectURL(await fh.getFile());
|
|
1692
|
+
state.currentBoard.urls[node.file] = url;
|
|
1693
|
+
}
|
|
1694
|
+
if (!url) return;
|
|
1695
|
+
if (node.type === 'image') {
|
|
1696
|
+
const img = document.createElement('img');
|
|
1697
|
+
img.src = url;
|
|
1698
|
+
stage.appendChild(img);
|
|
1699
|
+
} else if (node.type === 'video') {
|
|
1700
|
+
const v = document.createElement('video');
|
|
1701
|
+
v.src = url;
|
|
1702
|
+
v.controls = true;
|
|
1703
|
+
v.autoplay = true;
|
|
1704
|
+
stage.appendChild(v);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function _fsUpdateNavButtons() {
|
|
1709
|
+
const prev = $('fsPrev');
|
|
1710
|
+
const next = $('fsNext');
|
|
1711
|
+
const idx = _fsCurrent ? _fsSiblings.indexOf(_fsCurrent) : -1;
|
|
1712
|
+
if (prev) prev.disabled = idx <= 0;
|
|
1713
|
+
if (next) next.disabled = idx < 0 || idx >= _fsSiblings.length - 1;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async function fsNavigate(direction /* -1 или +1 */) {
|
|
1717
|
+
if (!_fsCurrent) return;
|
|
1718
|
+
const idx = _fsSiblings.indexOf(_fsCurrent);
|
|
1719
|
+
const next = idx + direction;
|
|
1720
|
+
if (next < 0 || next >= _fsSiblings.length) return;
|
|
1721
|
+
_fsCurrent = _fsSiblings[next];
|
|
1722
|
+
// Останавливаем текущее video если было.
|
|
1723
|
+
const v = $('fsStage').querySelector('video');
|
|
1724
|
+
if (v) { try { v.pause(); } catch {} }
|
|
1725
|
+
await _fsLoadCurrent();
|
|
1726
|
+
_fsUpdateNavButtons();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1668
1729
|
function closeFullscreen() {
|
|
1669
1730
|
const modal = $('fsModal');
|
|
1670
1731
|
if (modal.classList.contains('hidden')) return;
|
|
@@ -1672,16 +1733,20 @@ function closeFullscreen() {
|
|
|
1672
1733
|
if (v) { try { v.pause(); } catch {} }
|
|
1673
1734
|
$('fsStage').innerHTML = '';
|
|
1674
1735
|
modal.classList.add('hidden');
|
|
1736
|
+
_fsCurrent = null;
|
|
1737
|
+
_fsSiblings = [];
|
|
1675
1738
|
}
|
|
1676
1739
|
$('fsClose').addEventListener('click', closeFullscreen);
|
|
1740
|
+
$('fsPrev')?.addEventListener('click', e => { e.stopPropagation(); fsNavigate(-1); });
|
|
1741
|
+
$('fsNext')?.addEventListener('click', e => { e.stopPropagation(); fsNavigate(+1); });
|
|
1677
1742
|
$('fsModal').addEventListener('click', e => {
|
|
1678
1743
|
if (e.target.id === 'fsModal' || e.target.id === 'fsStage') closeFullscreen();
|
|
1679
1744
|
});
|
|
1680
1745
|
window.addEventListener('keydown', e => {
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1746
|
+
if ($('fsModal').classList.contains('hidden')) return;
|
|
1747
|
+
if (e.key === 'Escape') { e.stopPropagation(); closeFullscreen(); }
|
|
1748
|
+
else if (e.key === 'ArrowLeft') { e.stopPropagation(); fsNavigate(-1); }
|
|
1749
|
+
else if (e.key === 'ArrowRight') { e.stopPropagation(); fsNavigate(+1); }
|
|
1685
1750
|
}, true);
|
|
1686
1751
|
// Универсальный copy-helper: clipboard API → fallback на execCommand.
|
|
1687
1752
|
async function copyText(text) {
|
package/renderer/media.js
CHANGED
|
@@ -886,6 +886,20 @@ function getMentionSuggestions(kind) {
|
|
|
886
886
|
});
|
|
887
887
|
}
|
|
888
888
|
}
|
|
889
|
+
// Локации: @<location> → location.sheet; @<location>.<имя> → именованная картинка локации.
|
|
890
|
+
for (const l of state.locationsInfo) {
|
|
891
|
+
if (state.currentBoard.kind === 'location' && state.currentBoard.name === l.name) continue;
|
|
892
|
+
if (l.sheet) {
|
|
893
|
+
out.push({ key: l.name, label: l.name + ' · sheet', type: 'image', scope: 'loc-sheet' });
|
|
894
|
+
}
|
|
895
|
+
for (const img of (l.imageNodes || [])) {
|
|
896
|
+
out.push({
|
|
897
|
+
key: `${l.name}.${img.name}`,
|
|
898
|
+
label: `${l.name}.${img.name}`,
|
|
899
|
+
type: 'image', scope: 'loc-image',
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
889
903
|
}
|
|
890
904
|
return out;
|
|
891
905
|
}
|
|
@@ -926,6 +940,21 @@ function gatherMediaRefs(rawPrompt) {
|
|
|
926
940
|
});
|
|
927
941
|
}
|
|
928
942
|
}
|
|
943
|
+
for (const l of state.locationsInfo) {
|
|
944
|
+
if (state.currentBoard?.kind === 'location' && state.currentBoard.name === l.name) continue;
|
|
945
|
+
if (l.sheet) {
|
|
946
|
+
candidates.push({
|
|
947
|
+
key: l.name, name: l.name, type: 'image',
|
|
948
|
+
file: l.sheet, boardHandle: l.handle, locName: l.name,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
for (const img of (l.imageNodes || [])) {
|
|
952
|
+
candidates.push({
|
|
953
|
+
key: `${l.name}.${img.name}`, name: img.name, type: 'image',
|
|
954
|
+
file: img.file, boardHandle: l.handle, locName: l.name,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
929
958
|
// Длинные ключи раньше — на этапе матчинга
|
|
930
959
|
candidates.sort((a, b) => b.key.length - a.key.length);
|
|
931
960
|
// Маскируем уже найденные позиции, чтобы @алиса.happy не «находил» @алиса повторно
|
|
@@ -1080,10 +1109,10 @@ function updateMentionPopup() {
|
|
|
1080
1109
|
let items = [];
|
|
1081
1110
|
|
|
1082
1111
|
if (dotIdx >= 0) {
|
|
1083
|
-
// Уже выбран
|
|
1084
|
-
const
|
|
1112
|
+
// Уже выбран character ИЛИ location — показываем его картинки.
|
|
1113
|
+
const ownerName = query.slice(0, dotIdx);
|
|
1085
1114
|
const imgQuery = query.slice(dotIdx + 1);
|
|
1086
|
-
const c = state.charactersInfo.find(x => x.name.toLowerCase() ===
|
|
1115
|
+
const c = state.charactersInfo.find(x => x.name.toLowerCase() === ownerName);
|
|
1087
1116
|
if (c) {
|
|
1088
1117
|
for (const img of (c.imageNodes || [])) {
|
|
1089
1118
|
if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
|
|
@@ -1096,6 +1125,21 @@ function updateMentionPopup() {
|
|
|
1096
1125
|
});
|
|
1097
1126
|
}
|
|
1098
1127
|
}
|
|
1128
|
+
} else {
|
|
1129
|
+
const l = state.locationsInfo.find(x => x.name.toLowerCase() === ownerName);
|
|
1130
|
+
if (l) {
|
|
1131
|
+
for (const img of (l.imageNodes || [])) {
|
|
1132
|
+
if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
|
|
1133
|
+
items.push({
|
|
1134
|
+
key: `${l.name}.${img.name}`,
|
|
1135
|
+
label: img.name,
|
|
1136
|
+
type: 'image',
|
|
1137
|
+
scope: 'loc-image',
|
|
1138
|
+
locName: l.name,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1099
1143
|
}
|
|
1100
1144
|
} else if (isCharOnly) {
|
|
1101
1145
|
// @@ — только персонажи
|
|
@@ -1136,6 +1180,19 @@ function updateMentionPopup() {
|
|
|
1136
1180
|
});
|
|
1137
1181
|
}
|
|
1138
1182
|
}
|
|
1183
|
+
for (const l of state.locationsInfo) {
|
|
1184
|
+
if (state.currentBoard?.kind === 'location' && state.currentBoard.name === l.name) continue;
|
|
1185
|
+
const hasImages = (l.imageNodes || []).length > 0;
|
|
1186
|
+
if (l.sheet || hasImages) {
|
|
1187
|
+
items.push({
|
|
1188
|
+
key: l.name,
|
|
1189
|
+
label: l.name + (hasImages && l.sheet ? '' : (hasImages ? ' …' : ' · sheet')),
|
|
1190
|
+
type: 'image',
|
|
1191
|
+
scope: 'loc',
|
|
1192
|
+
hasImages, hasSheet: !!l.sheet,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1139
1196
|
}
|
|
1140
1197
|
items = items.filter(s => s.key.toLowerCase().includes(query)).slice(0, 16);
|
|
1141
1198
|
}
|
|
@@ -1159,6 +1216,8 @@ function updateMentionPopup() {
|
|
|
1159
1216
|
t.className = `mtype ${s.type}`;
|
|
1160
1217
|
t.textContent = s.scope === 'char' ? 'персонаж'
|
|
1161
1218
|
: s.scope === 'char-image' ? `персонаж·${s.charName}`
|
|
1219
|
+
: s.scope === 'loc' ? 'локация'
|
|
1220
|
+
: s.scope === 'loc-image' ? `локация·${s.locName}`
|
|
1162
1221
|
: s.type;
|
|
1163
1222
|
const nm = document.createElement('span');
|
|
1164
1223
|
nm.textContent = s.label;
|
|
@@ -1183,10 +1242,11 @@ function selectMention(idx = mentionState.selected) {
|
|
|
1183
1242
|
const cursor = ta.selectionStart;
|
|
1184
1243
|
const before = ta.value.slice(0, mentionState.anchor);
|
|
1185
1244
|
const after = ta.value.slice(cursor);
|
|
1186
|
-
// Если выбрали
|
|
1245
|
+
// Если выбрали персонажа/локацию и у них есть картинки — авто-drill:
|
|
1246
|
+
// оставляем @name. для дальнейшего ввода имени картинки.
|
|
1187
1247
|
let insert;
|
|
1188
1248
|
let reopen = false;
|
|
1189
|
-
if (item.scope === 'char' && item.hasImages) {
|
|
1249
|
+
if ((item.scope === 'char' || item.scope === 'loc') && item.hasImages) {
|
|
1190
1250
|
insert = '[@' + item.key + '.';
|
|
1191
1251
|
reopen = true;
|
|
1192
1252
|
} else {
|
package/renderer/styles.css
CHANGED
|
@@ -463,6 +463,19 @@
|
|
|
463
463
|
}
|
|
464
464
|
.fs-modal #fsClose:hover { background: rgba(255,68,68,0.7); }
|
|
465
465
|
|
|
466
|
+
/* Кнопки навигации prev/next в полноэкранном просмотре */
|
|
467
|
+
.fs-modal .fs-nav {
|
|
468
|
+
position: absolute; top: 50%; transform: translateY(-50%);
|
|
469
|
+
background: rgba(0,0,0,0.55); border: 1px solid #444; color: #fff;
|
|
470
|
+
font-size: 36px; line-height: 1; padding: 4px 14px; border-radius: 6px;
|
|
471
|
+
cursor: pointer; user-select: none;
|
|
472
|
+
transition: background 0.12s;
|
|
473
|
+
}
|
|
474
|
+
.fs-modal .fs-nav:hover { background: rgba(0,0,0,0.85); }
|
|
475
|
+
.fs-modal .fs-nav:disabled { opacity: 0.3; cursor: default; }
|
|
476
|
+
.fs-modal .fs-nav-prev { left: 16px; }
|
|
477
|
+
.fs-modal .fs-nav-next { right: 16px; }
|
|
478
|
+
|
|
466
479
|
/* === Реплики (боковая панель справа) === */
|
|
467
480
|
.repliques-panel {
|
|
468
481
|
width: 380px; background: #1c1c1c; border-left: 1px solid #333;
|