@restregular/dsh-notepad 0.2.0 → 0.3.0
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/README.md +27 -12
- package/client.js +605 -98
- package/index.js +50 -8
- package/lib/image-store.js +176 -0
- package/lib/note-store.js +139 -9
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +4 -2
package/client.js
CHANGED
|
@@ -50,6 +50,14 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
const SAVE_DELAY = 500;
|
|
51
51
|
const MAX_SEGMENTS = 200;
|
|
52
52
|
const MAX_SEGMENT_CHARS = 160000;
|
|
53
|
+
const MAX_IMAGES = 20;
|
|
54
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
55
|
+
const IMAGE_MIME = {
|
|
56
|
+
'image/png': 'png',
|
|
57
|
+
'image/jpeg': 'jpg',
|
|
58
|
+
'image/webp': 'webp',
|
|
59
|
+
'image/gif': 'gif',
|
|
60
|
+
};
|
|
53
61
|
|
|
54
62
|
// ------------------------------------------------------------------
|
|
55
63
|
// Copy
|
|
@@ -124,9 +132,21 @@ window.__ModuleLoader__.load({
|
|
|
124
132
|
show: '显示记事本(Ctrl+Shift+N)',
|
|
125
133
|
chars: '{n} 字符',
|
|
126
134
|
segments: '{n} 段',
|
|
127
|
-
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入',
|
|
135
|
+
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入 · 支持粘贴图片',
|
|
128
136
|
workspace: '工作区',
|
|
129
137
|
resize: '拖动调整大小',
|
|
138
|
+
addImage: '添加图片',
|
|
139
|
+
removeImage: '移除图片',
|
|
140
|
+
openImage: '打开图片',
|
|
141
|
+
imageAdded: '已添加 {n} 张图片',
|
|
142
|
+
imageFailed: '图片添加失败',
|
|
143
|
+
imageUnsupported: '只支持 PNG / JPEG / WebP / GIF 图片',
|
|
144
|
+
imageTooLarge: '图片太大(最大 20 MB)',
|
|
145
|
+
imageTooMany: '每段最多 {n} 张图片',
|
|
146
|
+
imageUploading: '图片处理中…',
|
|
147
|
+
images: '{n} 张图片',
|
|
148
|
+
dropImage: '松开以添加图片',
|
|
149
|
+
imagePasteSaved: '已保存到 .dsh/notepad/images',
|
|
130
150
|
},
|
|
131
151
|
en: {
|
|
132
152
|
title: 'Notepad',
|
|
@@ -197,9 +217,21 @@ window.__ModuleLoader__.load({
|
|
|
197
217
|
show: 'Show notepad (Ctrl+Shift+N)',
|
|
198
218
|
chars: '{n} chars',
|
|
199
219
|
segments: '{n} segments',
|
|
200
|
-
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert',
|
|
220
|
+
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert · Paste images',
|
|
201
221
|
workspace: 'Workspace',
|
|
202
222
|
resize: 'Drag to resize',
|
|
223
|
+
addImage: 'Add image',
|
|
224
|
+
removeImage: 'Remove image',
|
|
225
|
+
openImage: 'Open image',
|
|
226
|
+
imageAdded: 'Added {n} image(s)',
|
|
227
|
+
imageFailed: 'Could not add image',
|
|
228
|
+
imageUnsupported: 'Only PNG / JPEG / WebP / GIF images are supported',
|
|
229
|
+
imageTooLarge: 'Image is too large (20 MB max)',
|
|
230
|
+
imageTooMany: 'At most {n} images per segment',
|
|
231
|
+
imageUploading: 'Processing image…',
|
|
232
|
+
images: '{n} image(s)',
|
|
233
|
+
dropImage: 'Drop to add the image',
|
|
234
|
+
imagePasteSaved: 'Saved under .dsh/notepad/images',
|
|
203
235
|
},
|
|
204
236
|
};
|
|
205
237
|
|
|
@@ -438,6 +470,24 @@ window.__ModuleLoader__.load({
|
|
|
438
470
|
);
|
|
439
471
|
}
|
|
440
472
|
|
|
473
|
+
/** Picture frame with a mountain: image attachment. */
|
|
474
|
+
function ImageIcon({ size = 16 }) {
|
|
475
|
+
return h(
|
|
476
|
+
Svg,
|
|
477
|
+
{ size, viewBox: '0 0 16 16' },
|
|
478
|
+
h('rect', {
|
|
479
|
+
x: 2.2, y: 3.2, width: 11.6, height: 9.6, rx: 1.6,
|
|
480
|
+
stroke: 'currentColor', strokeWidth: 1.25, fill: 'none',
|
|
481
|
+
}),
|
|
482
|
+
h('circle', { cx: 5.9, cy: 6.5, r: 1.05, fill: 'currentColor' }),
|
|
483
|
+
h('path', {
|
|
484
|
+
d: 'M3.4 11.5 6.6 8.5l2 1.8 1.9-2.1 2.3 2.4',
|
|
485
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
486
|
+
strokeLinecap: 'round', strokeLinejoin: 'round', fill: 'none',
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
441
491
|
/** Arrow towards the bottom-right corner: minimize to the corner icon. */
|
|
442
492
|
function MinimizeIcon({ size = 16 }) {
|
|
443
493
|
return h(
|
|
@@ -480,7 +530,7 @@ window.__ModuleLoader__.load({
|
|
|
480
530
|
.dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
|
|
481
531
|
.dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
|
|
482
532
|
.dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
483
|
-
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-restore:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
533
|
+
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-restore:focus-visible,.dsh-np-img-open:focus-visible,.dsh-np-img-remove:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
484
534
|
.dsh-np-badge{position:absolute;top:2px;right:2px;min-width:13px;height:13px;padding:0 3px;box-sizing:border-box;border-radius:7px;background:var(--np-accent);color:var(--np-accent-fg);font-size:9px;font-weight:600;line-height:13px;text-align:center;font-variant-numeric:tabular-nums}
|
|
485
535
|
.dsh-np-body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;padding:8px 10px 0;opacity:1;transition:opacity .16s ease,visibility 0s linear 0s}
|
|
486
536
|
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
@@ -505,7 +555,20 @@ window.__ModuleLoader__.load({
|
|
|
505
555
|
.dsh-np-seg-text{display:block;width:100%;box-sizing:border-box;padding:2px 10px 8px;border:0;outline:0;resize:none;background:transparent;color:inherit;font-family:${FONT_STACK};font-size:13px;line-height:1.6;min-height:26px;max-height:240px;overflow-y:auto;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
|
|
506
556
|
.dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
|
|
507
557
|
.dsh-np-seg-text:disabled{opacity:.6}
|
|
558
|
+
.dsh-np-file{display:none}
|
|
559
|
+
.dsh-np-editor{display:flex;flex-direction:column;min-width:0;border-radius:0 0 9px 9px;transition:background .12s}
|
|
560
|
+
.dsh-np-editor[data-drag=true]{background:color-mix(in srgb,var(--np-accent) 8%,transparent);box-shadow:inset 0 0 0 1.5px var(--np-accent)}
|
|
561
|
+
.dsh-np-medias{display:flex;flex-wrap:wrap;gap:5px;padding:5px 9px 0}
|
|
562
|
+
.dsh-np-img{display:inline-flex;align-items:center;gap:4px;max-width:100%;height:38px;padding:2px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:8px;background:var(--np-surface);box-shadow:0 1px 2px rgb(15 23 42/.05)}
|
|
563
|
+
.dsh-np-img[data-pending=true]{padding-right:8px;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
564
|
+
.dsh-np-img-open{display:inline-flex;align-items:center;justify-content:center;flex:none;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));overflow:hidden;cursor:zoom-in}
|
|
565
|
+
.dsh-np-img-open img{display:block;width:100%;height:100%;object-fit:cover}
|
|
566
|
+
.dsh-np-img-name{max-width:118px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;line-height:13px;color:var(--dsw-alias-label-secondary,#4b5563)}
|
|
567
|
+
.dsh-np-img-remove{display:inline-flex;align-items:center;justify-content:center;flex:none;width:17px;height:17px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);cursor:pointer}
|
|
568
|
+
.dsh-np-img-remove:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-state-error-primary,#e25555)}
|
|
569
|
+
.dsh-np-img-spinner{flex:none;width:14px;height:14px;border:2px solid color-mix(in srgb,var(--np-accent) 30%,transparent);border-top-color:var(--np-accent);border-radius:50%;animation:dsh-np-spin .8s linear infinite}
|
|
508
570
|
.dsh-np-seg-archived-text{padding:2px 10px 9px;color:var(--dsw-alias-label-secondary,#4b5563);font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
571
|
+
.dsh-np-seg[data-archived=true] .dsh-np-medias{padding:0 10px 8px}
|
|
509
572
|
.dsh-np-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;flex:none;padding:22px 12px;text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px}
|
|
510
573
|
.dsh-np-empty-add{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:7px;background:var(--np-surface-2);color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;cursor:pointer}
|
|
511
574
|
.dsh-np-empty-add:hover{color:var(--dsw-alias-label-primary,#1f2430);background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
@@ -538,11 +601,12 @@ window.__ModuleLoader__.load({
|
|
|
538
601
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
539
602
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
540
603
|
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
604
|
+
@keyframes dsh-np-spin{to{transform:rotate(360deg)}}
|
|
541
605
|
body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 48px rgb(0 0 0/.55),0 0 0 .5px rgb(255 255 255/.07)}
|
|
542
606
|
body[data-ds-dark-theme] .dsh-np-seg[data-selected=true]{background:color-mix(in srgb,var(--np-accent) 14%,var(--dsw-alias-bg-layer-2,#2c2c2e))}
|
|
543
607
|
body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
|
|
544
608
|
body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
545
|
-
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}}`;
|
|
609
|
+
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}.dsh-np-img-spinner{animation:none}}`;
|
|
546
610
|
|
|
547
611
|
// ------------------------------------------------------------------
|
|
548
612
|
// Helpers
|
|
@@ -563,13 +627,16 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
563
627
|
}
|
|
564
628
|
|
|
565
629
|
function newSegment(text) {
|
|
566
|
-
return { id: newSegmentId(), text, archived: false, archivedAt: null };
|
|
630
|
+
return { id: newSegmentId(), text, images: [], archived: false, archivedAt: null };
|
|
567
631
|
}
|
|
568
632
|
|
|
569
633
|
function serializeSegments(segments) {
|
|
570
634
|
return segments.map((segment) => ({
|
|
571
635
|
id: segment.id,
|
|
572
636
|
text: segment.text,
|
|
637
|
+
...(Array.isArray(segment.images) && segment.images.length > 0
|
|
638
|
+
? { images: segment.images.map((image) => ({ path: image.path, name: image.name })) }
|
|
639
|
+
: {}),
|
|
573
640
|
...(segment.archived === true ? { archived: true } : {}),
|
|
574
641
|
...(segment.archived === true && typeof segment.archivedAt === 'string' ? { archivedAt: segment.archivedAt } : {}),
|
|
575
642
|
}));
|
|
@@ -588,6 +655,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
588
655
|
out.push({
|
|
589
656
|
id,
|
|
590
657
|
text: item.text,
|
|
658
|
+
images: normalizeImages(item.images),
|
|
591
659
|
archived,
|
|
592
660
|
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
593
661
|
});
|
|
@@ -598,13 +666,196 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
598
666
|
return text === '' ? [] : [newSegment(text)];
|
|
599
667
|
}
|
|
600
668
|
|
|
669
|
+
function baseNameOf(value) {
|
|
670
|
+
const trimmed = String(value ?? '').replace(/[/\\]+$/u, '');
|
|
671
|
+
const separator = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
672
|
+
return trimmed.slice(separator + 1);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function normalizeImage(value) {
|
|
676
|
+
if (typeof value === 'string') value = { path: value };
|
|
677
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
678
|
+
if (typeof value.path !== 'string') return null;
|
|
679
|
+
const filePath = value.path.trim().replace(/\\/gu, '/');
|
|
680
|
+
if (filePath === '') return null;
|
|
681
|
+
const rawName = typeof value.name === 'string' && value.name.trim() !== ''
|
|
682
|
+
? value.name.trim().replace(/\s+/gu, ' ')
|
|
683
|
+
: baseNameOf(filePath);
|
|
684
|
+
return { path: filePath, name: rawName === '' ? 'image' : rawName.slice(0, 160) };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function normalizeImages(value) {
|
|
688
|
+
if (!Array.isArray(value)) return [];
|
|
689
|
+
const out = [];
|
|
690
|
+
for (const item of value) {
|
|
691
|
+
if (out.length >= MAX_IMAGES) break;
|
|
692
|
+
const image = normalizeImage(item);
|
|
693
|
+
if (image !== null) out.push(image);
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Markdown reference used in the note body and composer transfers. */
|
|
699
|
+
function imageMarkdown(image) {
|
|
700
|
+
const file = typeof image?.path === 'string' ? image.path.replace(/\\/gu, '/') : '';
|
|
701
|
+
if (file === '') return '';
|
|
702
|
+
const label = (typeof image?.name === 'string' ? image.name : '')
|
|
703
|
+
.replace(/[\r\n]+/gu, ' ')
|
|
704
|
+
.replace(/[[\]]/gu, ' ')
|
|
705
|
+
.trim() || baseNameOf(file) || 'image';
|
|
706
|
+
let target;
|
|
707
|
+
try {
|
|
708
|
+
target = encodeURI(file);
|
|
709
|
+
} catch {
|
|
710
|
+
target = file;
|
|
711
|
+
}
|
|
712
|
+
target = target
|
|
713
|
+
.replace(/#/gu, '%23')
|
|
714
|
+
.replace(/\?/gu, '%3F')
|
|
715
|
+
.replace(/\(/gu, '%28')
|
|
716
|
+
.replace(/\)/gu, '%29');
|
|
717
|
+
return ``;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** One segment as text plus one Markdown line per attached image. */
|
|
721
|
+
function segmentBody(segment) {
|
|
722
|
+
if (segment === null || segment === undefined) return '';
|
|
723
|
+
const text = typeof segment.text === 'string' ? segment.text : '';
|
|
724
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
725
|
+
const parts = [];
|
|
726
|
+
if (text.trim() !== '') parts.push(text);
|
|
727
|
+
for (const image of images) {
|
|
728
|
+
const markdown = imageMarkdown(image);
|
|
729
|
+
if (markdown !== '') parts.push(markdown);
|
|
730
|
+
}
|
|
731
|
+
return parts.join('\n');
|
|
732
|
+
}
|
|
733
|
+
|
|
601
734
|
function mergedText(segments) {
|
|
602
735
|
return segments
|
|
603
|
-
.map((segment) => (segment
|
|
604
|
-
.filter((text) => text
|
|
736
|
+
.map((segment) => segmentBody(segment))
|
|
737
|
+
.filter((text) => text !== '')
|
|
605
738
|
.join('\n\n');
|
|
606
739
|
}
|
|
607
740
|
|
|
741
|
+
function isAbsolutePath(value) {
|
|
742
|
+
const text = String(value ?? '');
|
|
743
|
+
return text.startsWith('/') || /^[A-Za-z]:\//u.test(text) || text.startsWith('//');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function normalizedCwd(value) {
|
|
747
|
+
return String(value ?? '').replace(/\\/gu, '/').replace(/\/+$/u, '');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Store a path relative to the workspace when it lives inside it. */
|
|
751
|
+
function imagePathForNote(pathValue, cwd) {
|
|
752
|
+
const file = String(pathValue ?? '').replace(/\\/gu, '/');
|
|
753
|
+
const root = normalizedCwd(cwd);
|
|
754
|
+
if (root === '') return file;
|
|
755
|
+
// Windows drive / UNC spellings are case-insensitive; POSIX ones are not.
|
|
756
|
+
const windows = /^[A-Za-z]:\//u.test(root) || root.startsWith('//');
|
|
757
|
+
const compareFile = windows ? file.toLowerCase() : file;
|
|
758
|
+
const compareRoot = windows ? root.toLowerCase() : root;
|
|
759
|
+
if (compareFile === compareRoot) return '.';
|
|
760
|
+
if (compareFile.startsWith(`${compareRoot}/`)) return file.slice(root.length + 1);
|
|
761
|
+
return file;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function absoluteImagePath(pathValue, cwd) {
|
|
765
|
+
const file = String(pathValue ?? '').replace(/\\/gu, '/');
|
|
766
|
+
if (isAbsolutePath(file)) return file;
|
|
767
|
+
const root = normalizedCwd(cwd);
|
|
768
|
+
return root === '' ? file : `${root}/${file.replace(/^\/+/u, '')}`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Authenticated host file route used by the browser image renderer. */
|
|
772
|
+
function imageUrl(pathValue, cwd) {
|
|
773
|
+
const target = absoluteImagePath(pathValue, cwd);
|
|
774
|
+
try {
|
|
775
|
+
const base = typeof document !== 'undefined' && typeof document.baseURI === 'string'
|
|
776
|
+
? document.baseURI
|
|
777
|
+
: 'http://localhost/';
|
|
778
|
+
const url = new URL('api/file', base);
|
|
779
|
+
url.searchParams.set('path', target);
|
|
780
|
+
return url.href;
|
|
781
|
+
} catch {
|
|
782
|
+
return `/api/file?path=${encodeURIComponent(target)}`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function imageMimeOf(file) {
|
|
787
|
+
const type = typeof file?.type === 'string' ? file.type.toLowerCase() : '';
|
|
788
|
+
if (type !== '') return Object.prototype.hasOwnProperty.call(IMAGE_MIME, type) ? type : '';
|
|
789
|
+
const name = String(file?.name ?? '');
|
|
790
|
+
if (/\.png$/iu.test(name)) return 'image/png';
|
|
791
|
+
if (/\.jpe?g$/iu.test(name)) return 'image/jpeg';
|
|
792
|
+
if (/\.webp$/iu.test(name)) return 'image/webp';
|
|
793
|
+
if (/\.gif$/iu.test(name)) return 'image/gif';
|
|
794
|
+
return '';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function isSupportedImage(file) {
|
|
798
|
+
return imageMimeOf(file) !== '';
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function hostPathOf(file) {
|
|
802
|
+
try {
|
|
803
|
+
const bridge = globalThis.__DSH_HOST_PATHS__;
|
|
804
|
+
if (bridge === null || bridge === undefined || typeof bridge.pathFor !== 'function') return '';
|
|
805
|
+
const value = bridge.pathFor(file);
|
|
806
|
+
return typeof value === 'string' ? value : '';
|
|
807
|
+
} catch {
|
|
808
|
+
return '';
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
class ImageError extends Error {
|
|
813
|
+
constructor(code, message) {
|
|
814
|
+
super(message ?? code);
|
|
815
|
+
this.name = 'ImageError';
|
|
816
|
+
this.code = code;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function readFileBase64(file) {
|
|
821
|
+
return new Promise((resolve, reject) => {
|
|
822
|
+
try {
|
|
823
|
+
const reader = new FileReader();
|
|
824
|
+
reader.onerror = () => reject(new ImageError('read-failed', 'Could not read the image file.'));
|
|
825
|
+
reader.onload = () => {
|
|
826
|
+
const result = typeof reader.result === 'string' ? reader.result : '';
|
|
827
|
+
const comma = result.indexOf(',');
|
|
828
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
829
|
+
};
|
|
830
|
+
reader.readAsDataURL(file);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
reject(error);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function uploadImage(root, sessionId, file) {
|
|
838
|
+
if (Number(file?.size) > MAX_IMAGE_BYTES) throw new ImageError('too-large');
|
|
839
|
+
const mime = imageMimeOf(file);
|
|
840
|
+
if (mime === '') throw new ImageError('unsupported');
|
|
841
|
+
const data = await readFileBase64(file);
|
|
842
|
+
const response = await fetch(`${API}/image`, {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: { 'Content-Type': 'application/json' },
|
|
845
|
+
body: JSON.stringify({
|
|
846
|
+
root,
|
|
847
|
+
sessionId,
|
|
848
|
+
name: typeof file?.name === 'string' && file.name !== '' ? file.name : 'image.png',
|
|
849
|
+
mime,
|
|
850
|
+
data,
|
|
851
|
+
}),
|
|
852
|
+
});
|
|
853
|
+
const payload = await readResponse(response);
|
|
854
|
+
const image = normalizeImage(payload);
|
|
855
|
+
if (image === null) throw new ImageError('upload-failed', 'The host did not return an image path.');
|
|
856
|
+
return image;
|
|
857
|
+
}
|
|
858
|
+
|
|
608
859
|
function autoGrow(element) {
|
|
609
860
|
if (element === null || element === undefined || element.style === undefined) return;
|
|
610
861
|
try {
|
|
@@ -769,6 +1020,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
769
1020
|
const [ui, setUi] = useState(readStoredUi);
|
|
770
1021
|
const [sending, setSending] = useState(false);
|
|
771
1022
|
const [resizing, setResizing] = useState(false);
|
|
1023
|
+
const [uploading, setUploading] = useState({});
|
|
1024
|
+
const [dragSegment, setDragSegment] = useState('');
|
|
772
1025
|
|
|
773
1026
|
const bindingRef = useRef(null);
|
|
774
1027
|
const rootSessionRef = useRef('');
|
|
@@ -789,6 +1042,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
789
1042
|
const resizeRef = useRef(null);
|
|
790
1043
|
const sendingRef = useRef(false);
|
|
791
1044
|
const textareaRefs = useRef(new Map());
|
|
1045
|
+
const fileInputRefs = useRef(new Map());
|
|
792
1046
|
const listRef = useRef(null);
|
|
793
1047
|
const pendingFocusRef = useRef(null);
|
|
794
1048
|
const undoRef = useRef(null);
|
|
@@ -1118,6 +1372,153 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1118
1372
|
scheduleSave();
|
|
1119
1373
|
};
|
|
1120
1374
|
|
|
1375
|
+
// Image attachments ---------------------------------------------------
|
|
1376
|
+
const imageErrorText = (reason) => {
|
|
1377
|
+
const code = reason?.code;
|
|
1378
|
+
if (code === 'too-large') return t('imageTooLarge');
|
|
1379
|
+
if (code === 'unsupported') return t('imageUnsupported');
|
|
1380
|
+
if (code === 'too-many') return t('imageTooMany', { n: MAX_IMAGES });
|
|
1381
|
+
return `${t('imageFailed')}: ${String(reason?.message ?? reason)}`;
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const appendImages = (id, images) => {
|
|
1385
|
+
if (!Array.isArray(images) || images.length === 0) return 0;
|
|
1386
|
+
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1387
|
+
if (target === undefined || target.archived === true) return 0;
|
|
1388
|
+
const existing = Array.isArray(target.images) ? target.images : [];
|
|
1389
|
+
const room = Math.max(0, MAX_IMAGES - existing.length);
|
|
1390
|
+
if (room === 0) {
|
|
1391
|
+
showToast(t('imageTooMany', { n: MAX_IMAGES }));
|
|
1392
|
+
return 0;
|
|
1393
|
+
}
|
|
1394
|
+
const accepted = images.slice(0, room);
|
|
1395
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1396
|
+
(segment.id === id ? { ...segment, images: [...existing, ...accepted] } : segment));
|
|
1397
|
+
commitSegments(next);
|
|
1398
|
+
if (accepted.length < images.length) showToast(t('imageTooMany', { n: MAX_IMAGES }));
|
|
1399
|
+
else showToast(t('imageAdded', { n: accepted.length }));
|
|
1400
|
+
return accepted.length;
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Add browser files to one segment. Pasted images are always written into
|
|
1405
|
+
* the plugin directory; selected images use their host path when the
|
|
1406
|
+
* desktop bridge exposes it and fall back to the same upload otherwise.
|
|
1407
|
+
* @param {string} id segment id
|
|
1408
|
+
* @param {ArrayLike<File>} files
|
|
1409
|
+
* @param {'paste'|'pick'} origin
|
|
1410
|
+
*/
|
|
1411
|
+
const attachImages = async (id, files, origin) => {
|
|
1412
|
+
const list = Array.from(files ?? []).filter(isSupportedImage);
|
|
1413
|
+
if (list.length === 0) {
|
|
1414
|
+
showToast(t('imageUnsupported'));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const root = rootRef.current;
|
|
1418
|
+
const sessionId = rootSessionRef.current;
|
|
1419
|
+
if (root === '' || sessionId === '') return;
|
|
1420
|
+
setUploading((previous) => ({ ...previous, [id]: (previous[id] ?? 0) + list.length }));
|
|
1421
|
+
const collected = [];
|
|
1422
|
+
try {
|
|
1423
|
+
for (const file of list) {
|
|
1424
|
+
const directPath = origin === 'pick' ? hostPathOf(file) : '';
|
|
1425
|
+
if (directPath !== '') {
|
|
1426
|
+
collected.push({
|
|
1427
|
+
path: imagePathForNote(directPath, bindingRef.current?.cwd ?? root),
|
|
1428
|
+
name: baseNameOf(directPath) || file?.name || 'image',
|
|
1429
|
+
});
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
collected.push(await uploadImage(root, sessionId, file));
|
|
1433
|
+
}
|
|
1434
|
+
if (collected.length > 0) appendImages(id, collected);
|
|
1435
|
+
} catch (reason) {
|
|
1436
|
+
if (collected.length > 0) appendImages(id, collected);
|
|
1437
|
+
showToast(imageErrorText(reason));
|
|
1438
|
+
} finally {
|
|
1439
|
+
setUploading((previous) => {
|
|
1440
|
+
const next = { ...previous };
|
|
1441
|
+
const remaining = (next[id] ?? 0) - list.length;
|
|
1442
|
+
if (remaining > 0) next[id] = remaining;
|
|
1443
|
+
else delete next[id];
|
|
1444
|
+
return next;
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
const removeImage = (id, pathValue) => {
|
|
1450
|
+
const next = segmentsRef.current.map((segment) => {
|
|
1451
|
+
if (segment.id !== id) return segment;
|
|
1452
|
+
const images = Array.isArray(segment.images) ? segment.images.filter((image) => image.path !== pathValue) : [];
|
|
1453
|
+
return { ...segment, images };
|
|
1454
|
+
});
|
|
1455
|
+
commitSegments(next);
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
const triggerFilePicker = (id) => {
|
|
1459
|
+
const input = fileInputRefs.current.get(id);
|
|
1460
|
+
if (input !== undefined && input !== null && typeof input.click === 'function') input.click();
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const onFileInputChange = (event, id) => {
|
|
1464
|
+
const input = event?.currentTarget ?? event?.target;
|
|
1465
|
+
const files = input?.files;
|
|
1466
|
+
if (files !== undefined && files !== null && files.length > 0) attachImages(id, files, 'pick');
|
|
1467
|
+
try {
|
|
1468
|
+
if (input !== null && input !== undefined) input.value = '';
|
|
1469
|
+
} catch {
|
|
1470
|
+
/* A read-only file input value must not break the flow. */
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
const clipboardImageFiles = (data) => {
|
|
1475
|
+
const files = data?.files;
|
|
1476
|
+
if (files === undefined || files === null) return [];
|
|
1477
|
+
return Array.from(files).filter(isSupportedImage);
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
const onSegmentPaste = (event, id) => {
|
|
1481
|
+
const data = event?.clipboardData ?? event?.nativeEvent?.clipboardData;
|
|
1482
|
+
const files = clipboardImageFiles(data);
|
|
1483
|
+
if (files.length === 0) return;
|
|
1484
|
+
event.preventDefault?.();
|
|
1485
|
+
event.stopPropagation?.();
|
|
1486
|
+
attachImages(id, files, 'paste');
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
const onSegmentDrop = (event, id) => {
|
|
1490
|
+
const files = clipboardImageFiles(event?.dataTransfer);
|
|
1491
|
+
setDragSegment('');
|
|
1492
|
+
if (files.length === 0) return;
|
|
1493
|
+
event.preventDefault?.();
|
|
1494
|
+
event.stopPropagation?.();
|
|
1495
|
+
attachImages(id, files, 'pick');
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
const onSegmentDragOver = (event, id) => {
|
|
1499
|
+
const types = event?.dataTransfer?.types;
|
|
1500
|
+
if (types !== undefined && types !== null && !Array.from(types).includes('Files')) return;
|
|
1501
|
+
event.preventDefault?.();
|
|
1502
|
+
if (dragSegment !== id) setDragSegment(id);
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
const onSegmentDragLeave = (event, id) => {
|
|
1506
|
+
const related = event?.relatedTarget;
|
|
1507
|
+
const current = event?.currentTarget;
|
|
1508
|
+
if (related !== null && related !== undefined && current !== null && current !== undefined
|
|
1509
|
+
&& typeof current.contains === 'function' && current.contains(related)) return;
|
|
1510
|
+
if (dragSegment === id) setDragSegment('');
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
const openImage = (image) => {
|
|
1514
|
+
const url = imageUrl(image.path, rootRef.current);
|
|
1515
|
+
try {
|
|
1516
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1517
|
+
} catch {
|
|
1518
|
+
/* Popups may be blocked; the path remains available in the title. */
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1121
1522
|
const addSegment = () => {
|
|
1122
1523
|
if (segmentsRef.current.length >= MAX_SEGMENTS) {
|
|
1123
1524
|
showToast(t('tooManySegments', { n: MAX_SEGMENTS }));
|
|
@@ -1677,101 +2078,201 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1677
2078
|
: null,
|
|
1678
2079
|
);
|
|
1679
2080
|
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
},
|
|
1689
|
-
h(
|
|
2081
|
+
const totalImages = segments.reduce(
|
|
2082
|
+
(sum, segment) => sum + (Array.isArray(segment.images) ? segment.images.length : 0), 0);
|
|
2083
|
+
|
|
2084
|
+
const renderMedia = (segment, options = {}) => {
|
|
2085
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2086
|
+
const pending = options.pending === true;
|
|
2087
|
+
if (images.length === 0 && !pending) return null;
|
|
2088
|
+
return h(
|
|
1690
2089
|
'div',
|
|
1691
|
-
{ className: 'dsh-np-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
2090
|
+
{ className: 'dsh-np-medias' },
|
|
2091
|
+
pending
|
|
2092
|
+
? h(
|
|
2093
|
+
'div',
|
|
2094
|
+
{ key: 'pending', className: 'dsh-np-img', 'data-pending': 'true' },
|
|
2095
|
+
h('span', { className: 'dsh-np-img-spinner' }),
|
|
2096
|
+
h('span', { className: 'dsh-np-img-name' }, t('imageUploading')),
|
|
2097
|
+
)
|
|
2098
|
+
: null,
|
|
2099
|
+
images.map((image, imageIndex) => h(
|
|
2100
|
+
'div',
|
|
2101
|
+
{ key: `${imageIndex}-${image.path}`, className: 'dsh-np-img', title: image.path },
|
|
2102
|
+
h(
|
|
2103
|
+
'button',
|
|
2104
|
+
{
|
|
2105
|
+
type: 'button',
|
|
2106
|
+
className: 'dsh-np-img-open',
|
|
2107
|
+
title: `${t('openImage')}: ${image.path}`,
|
|
2108
|
+
'aria-label': t('openImage'),
|
|
2109
|
+
onClick: () => openImage(image),
|
|
2110
|
+
},
|
|
2111
|
+
h('img', {
|
|
2112
|
+
src: imageUrl(image.path, binding.cwd),
|
|
2113
|
+
alt: image.name,
|
|
2114
|
+
loading: 'lazy',
|
|
2115
|
+
decoding: 'async',
|
|
2116
|
+
draggable: false,
|
|
2117
|
+
}),
|
|
2118
|
+
),
|
|
2119
|
+
h('span', { className: 'dsh-np-img-name', title: image.path }, image.name),
|
|
2120
|
+
options.readOnly === true
|
|
2121
|
+
? null
|
|
2122
|
+
: h(
|
|
2123
|
+
'button',
|
|
2124
|
+
{
|
|
2125
|
+
type: 'button',
|
|
2126
|
+
className: 'dsh-np-img-remove',
|
|
2127
|
+
title: t('removeImage'),
|
|
2128
|
+
'aria-label': t('removeImage'),
|
|
2129
|
+
onClick: () => removeImage(segment.id, image.path),
|
|
2130
|
+
},
|
|
2131
|
+
h(CloseIcon, { size: 11 }),
|
|
2132
|
+
),
|
|
2133
|
+
)),
|
|
2134
|
+
);
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
const activeCards = activeSegments.map((segment, index) => {
|
|
2138
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2139
|
+
const empty = segment.text.trim() === '' && images.length === 0;
|
|
2140
|
+
return h(
|
|
2141
|
+
'article',
|
|
2142
|
+
{
|
|
2143
|
+
key: segment.id,
|
|
2144
|
+
className: 'dsh-np-seg',
|
|
2145
|
+
'data-seg-id': segment.id,
|
|
2146
|
+
'data-archived': 'false',
|
|
2147
|
+
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
2148
|
+
},
|
|
2149
|
+
h('input', {
|
|
2150
|
+
key: 'file-input',
|
|
2151
|
+
ref: (element) => {
|
|
2152
|
+
if (element === null || element === undefined) {
|
|
2153
|
+
fileInputRefs.current.delete(segment.id);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
fileInputRefs.current.set(segment.id, element);
|
|
1725
2157
|
},
|
|
1726
|
-
|
|
1727
|
-
|
|
2158
|
+
className: 'dsh-np-file',
|
|
2159
|
+
type: 'file',
|
|
2160
|
+
accept: 'image/png,image/jpeg,image/webp,image/gif',
|
|
2161
|
+
multiple: true,
|
|
2162
|
+
tabIndex: -1,
|
|
2163
|
+
'aria-hidden': 'true',
|
|
2164
|
+
onChange: (event) => onFileInputChange(event, segment.id),
|
|
2165
|
+
}),
|
|
1728
2166
|
h(
|
|
1729
|
-
'
|
|
1730
|
-
{
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
title: t('
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2167
|
+
'div',
|
|
2168
|
+
{ className: 'dsh-np-seg-head' },
|
|
2169
|
+
h(
|
|
2170
|
+
'label',
|
|
2171
|
+
{ className: 'dsh-np-check', title: t('selectSegment') },
|
|
2172
|
+
h('input', {
|
|
2173
|
+
type: 'checkbox',
|
|
2174
|
+
checked: selected.has(segment.id),
|
|
2175
|
+
'aria-label': t('selectSegment'),
|
|
2176
|
+
onChange: () => toggleSelected(segment.id),
|
|
2177
|
+
}),
|
|
2178
|
+
),
|
|
2179
|
+
h('span', { className: 'dsh-np-index' }, String(index + 1).padStart(2, '0')),
|
|
2180
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
2181
|
+
h(
|
|
2182
|
+
'button',
|
|
2183
|
+
{
|
|
2184
|
+
type: 'button',
|
|
2185
|
+
className: 'dsh-np-segbtn',
|
|
2186
|
+
title: t('addImage'),
|
|
2187
|
+
'aria-label': t('addImage'),
|
|
2188
|
+
onClick: () => triggerFilePicker(segment.id),
|
|
2189
|
+
},
|
|
2190
|
+
h(ImageIcon, { size: 14 }),
|
|
2191
|
+
),
|
|
2192
|
+
h(
|
|
2193
|
+
'button',
|
|
2194
|
+
{
|
|
2195
|
+
type: 'button',
|
|
2196
|
+
className: 'dsh-np-segbtn',
|
|
2197
|
+
title: t('insertSegment'),
|
|
2198
|
+
'aria-label': t('insertSegment'),
|
|
2199
|
+
disabled: empty,
|
|
2200
|
+
onClick: () => insertSegments([segment]),
|
|
2201
|
+
},
|
|
2202
|
+
h(InsertIcon, { size: 14 }),
|
|
2203
|
+
),
|
|
2204
|
+
h(
|
|
2205
|
+
'button',
|
|
2206
|
+
{
|
|
2207
|
+
type: 'button',
|
|
2208
|
+
className: 'dsh-np-segbtn',
|
|
2209
|
+
title: t('replaceSegment'),
|
|
2210
|
+
'aria-label': t('replaceSegment'),
|
|
2211
|
+
disabled: empty,
|
|
2212
|
+
onClick: () => replaceSegments([segment]),
|
|
2213
|
+
},
|
|
2214
|
+
h(ReplaceIcon, { size: 14 }),
|
|
2215
|
+
),
|
|
2216
|
+
h(
|
|
2217
|
+
'button',
|
|
2218
|
+
{
|
|
2219
|
+
type: 'button',
|
|
2220
|
+
className: 'dsh-np-segbtn',
|
|
2221
|
+
title: t('sendSegment'),
|
|
2222
|
+
'aria-label': t('sendSegment'),
|
|
2223
|
+
disabled: empty || sending,
|
|
2224
|
+
onClick: () => sendSegments([segment]),
|
|
2225
|
+
},
|
|
2226
|
+
h(SendIcon, { size: 14 }),
|
|
2227
|
+
),
|
|
2228
|
+
h(
|
|
2229
|
+
'button',
|
|
2230
|
+
{
|
|
2231
|
+
type: 'button',
|
|
2232
|
+
className: 'dsh-np-segbtn',
|
|
2233
|
+
title: t('archiveSegment'),
|
|
2234
|
+
'aria-label': t('archiveSegment'),
|
|
2235
|
+
onClick: () => archiveSegment(segment.id),
|
|
2236
|
+
},
|
|
2237
|
+
h(ArchiveIcon, { size: 14 }),
|
|
2238
|
+
),
|
|
1739
2239
|
),
|
|
1740
2240
|
h(
|
|
1741
|
-
'
|
|
2241
|
+
'div',
|
|
1742
2242
|
{
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
2243
|
+
className: 'dsh-np-editor',
|
|
2244
|
+
'data-drag': dragSegment === segment.id ? 'true' : 'false',
|
|
2245
|
+
onPaste: (event) => onSegmentPaste(event, segment.id),
|
|
2246
|
+
onDrop: (event) => onSegmentDrop(event, segment.id),
|
|
2247
|
+
onDragOver: (event) => onSegmentDragOver(event, segment.id),
|
|
2248
|
+
onDragLeave: (event) => onSegmentDragLeave(event, segment.id),
|
|
1748
2249
|
},
|
|
1749
|
-
|
|
2250
|
+
renderMedia(segment, { pending: (uploading[segment.id] ?? 0) > 0 }),
|
|
2251
|
+
h('textarea', {
|
|
2252
|
+
ref: (element) => {
|
|
2253
|
+
if (element === null || element === undefined) {
|
|
2254
|
+
textareaRefs.current.delete(segment.id);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
textareaRefs.current.set(segment.id, element);
|
|
2258
|
+
autoGrow(element);
|
|
2259
|
+
},
|
|
2260
|
+
className: 'dsh-np-seg-text',
|
|
2261
|
+
rows: 1,
|
|
2262
|
+
value: segment.text,
|
|
2263
|
+
placeholder: index === 0 ? t('placeholder') : t('segmentPlaceholder'),
|
|
2264
|
+
spellCheck: false,
|
|
2265
|
+
maxLength: MAX_SEGMENT_CHARS,
|
|
2266
|
+
'aria-label': t('editor'),
|
|
2267
|
+
onChange: (event) => onSegmentChange(event, segment.id),
|
|
2268
|
+
onKeyDown: (event) => onSegmentKeyDown(event, segment.id),
|
|
2269
|
+
onBlur: () => {
|
|
2270
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
2271
|
+
},
|
|
2272
|
+
}),
|
|
1750
2273
|
),
|
|
1751
|
-
)
|
|
1752
|
-
|
|
1753
|
-
ref: (element) => {
|
|
1754
|
-
if (element === null || element === undefined) {
|
|
1755
|
-
textareaRefs.current.delete(segment.id);
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
textareaRefs.current.set(segment.id, element);
|
|
1759
|
-
autoGrow(element);
|
|
1760
|
-
},
|
|
1761
|
-
className: 'dsh-np-seg-text',
|
|
1762
|
-
rows: 1,
|
|
1763
|
-
value: segment.text,
|
|
1764
|
-
placeholder: index === 0 ? t('placeholder') : t('segmentPlaceholder'),
|
|
1765
|
-
spellCheck: false,
|
|
1766
|
-
maxLength: MAX_SEGMENT_CHARS,
|
|
1767
|
-
'aria-label': t('editor'),
|
|
1768
|
-
onChange: (event) => onSegmentChange(event, segment.id),
|
|
1769
|
-
onKeyDown: (event) => onSegmentKeyDown(event, segment.id),
|
|
1770
|
-
onBlur: () => {
|
|
1771
|
-
if (dirtyRef.current) saveNowRef.current?.();
|
|
1772
|
-
},
|
|
1773
|
-
}),
|
|
1774
|
-
));
|
|
2274
|
+
);
|
|
2275
|
+
});
|
|
1775
2276
|
|
|
1776
2277
|
const activeEmpty = activeSegments.length === 0
|
|
1777
2278
|
? h(
|
|
@@ -1849,7 +2350,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1849
2350
|
className: 'dsh-np-segbtn',
|
|
1850
2351
|
title: t('insertSegment'),
|
|
1851
2352
|
'aria-label': t('insertSegment'),
|
|
1852
|
-
disabled: segment.text.trim() === ''
|
|
2353
|
+
disabled: segment.text.trim() === ''
|
|
2354
|
+
&& !(Array.isArray(segment.images) && segment.images.length > 0),
|
|
1853
2355
|
onClick: () => insertSegments([segment]),
|
|
1854
2356
|
},
|
|
1855
2357
|
h(InsertIcon, { size: 14 }),
|
|
@@ -1861,7 +2363,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1861
2363
|
className: 'dsh-np-segbtn',
|
|
1862
2364
|
title: t('replaceSegment'),
|
|
1863
2365
|
'aria-label': t('replaceSegment'),
|
|
1864
|
-
disabled: segment.text.trim() === ''
|
|
2366
|
+
disabled: segment.text.trim() === ''
|
|
2367
|
+
&& !(Array.isArray(segment.images) && segment.images.length > 0),
|
|
1865
2368
|
onClick: () => replaceSegments([segment]),
|
|
1866
2369
|
},
|
|
1867
2370
|
h(ReplaceIcon, { size: 14 }),
|
|
@@ -1891,7 +2394,10 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1891
2394
|
),
|
|
1892
2395
|
),
|
|
1893
2396
|
h('div', { className: 'dsh-np-seg-archived-text' },
|
|
1894
|
-
segment.text === ''
|
|
2397
|
+
segment.text === ''
|
|
2398
|
+
? (Array.isArray(segment.images) && segment.images.length > 0 ? null : t('emptySegment'))
|
|
2399
|
+
: segment.text),
|
|
2400
|
+
renderMedia(segment, { readOnly: true }),
|
|
1895
2401
|
)),
|
|
1896
2402
|
),
|
|
1897
2403
|
),
|
|
@@ -1921,7 +2427,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1921
2427
|
}, t('retry'))
|
|
1922
2428
|
: null,
|
|
1923
2429
|
h('span', { className: 'dsh-np-count' },
|
|
1924
|
-
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
|
|
2430
|
+
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
|
|
2431
|
+
+ (totalImages > 0 ? ` · ${t('images', { n: totalImages })}` : '')),
|
|
1925
2432
|
h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
|
|
1926
2433
|
);
|
|
1927
2434
|
|