@restregular/dsh-notepad 0.2.0 → 0.3.1
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 +28 -13
- package/client.js +610 -109
- 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
|
|
@@ -100,8 +108,8 @@ window.__ModuleLoader__.load({
|
|
|
100
108
|
archived: '归档',
|
|
101
109
|
archivedToast: '已归档,可在归档中查看',
|
|
102
110
|
archivedCount: '已归档 {n} 段',
|
|
103
|
-
showArchive: '
|
|
104
|
-
hideArchive: '
|
|
111
|
+
showArchive: '显示归档',
|
|
112
|
+
hideArchive: '隐藏归档',
|
|
105
113
|
archiveCount: '归档 · {n}',
|
|
106
114
|
archiveEmpty: '还没有归档的分段',
|
|
107
115
|
noActive: '没有进行中的分段',
|
|
@@ -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',
|
|
@@ -173,8 +193,8 @@ window.__ModuleLoader__.load({
|
|
|
173
193
|
archived: 'Archived',
|
|
174
194
|
archivedToast: 'Archived; available under Archive',
|
|
175
195
|
archivedCount: 'Archived {n}',
|
|
176
|
-
showArchive: 'Show archive
|
|
177
|
-
hideArchive: 'Hide archive
|
|
196
|
+
showArchive: 'Show archive',
|
|
197
|
+
hideArchive: 'Hide archive',
|
|
178
198
|
archiveCount: 'Archive · {n}',
|
|
179
199
|
archiveEmpty: 'Nothing archived yet',
|
|
180
200
|
noActive: 'No active segments',
|
|
@@ -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,8 +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}
|
|
484
|
-
.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}
|
|
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}
|
|
485
534
|
.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
535
|
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
487
536
|
.dsh-np-tools{display:flex;align-items:center;gap:4px;flex:none;padding:0 2px 7px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08))}
|
|
@@ -505,7 +554,20 @@ window.__ModuleLoader__.load({
|
|
|
505
554
|
.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
555
|
.dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
|
|
507
556
|
.dsh-np-seg-text:disabled{opacity:.6}
|
|
557
|
+
.dsh-np-file{display:none}
|
|
558
|
+
.dsh-np-editor{display:flex;flex-direction:column;min-width:0;border-radius:0 0 9px 9px;transition:background .12s}
|
|
559
|
+
.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)}
|
|
560
|
+
.dsh-np-medias{display:flex;flex-wrap:wrap;gap:5px;padding:5px 9px 0}
|
|
561
|
+
.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)}
|
|
562
|
+
.dsh-np-img[data-pending=true]{padding-right:8px;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
563
|
+
.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}
|
|
564
|
+
.dsh-np-img-open img{display:block;width:100%;height:100%;object-fit:cover}
|
|
565
|
+
.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)}
|
|
566
|
+
.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}
|
|
567
|
+
.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)}
|
|
568
|
+
.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
569
|
.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}
|
|
570
|
+
.dsh-np-seg[data-archived=true] .dsh-np-medias{padding:0 10px 8px}
|
|
509
571
|
.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
572
|
.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
573
|
.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 +600,12 @@ window.__ModuleLoader__.load({
|
|
|
538
600
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
539
601
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
540
602
|
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
603
|
+
@keyframes dsh-np-spin{to{transform:rotate(360deg)}}
|
|
541
604
|
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
605
|
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
606
|
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
607
|
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}}`;
|
|
608
|
+
@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
609
|
|
|
547
610
|
// ------------------------------------------------------------------
|
|
548
611
|
// Helpers
|
|
@@ -563,13 +626,16 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
563
626
|
}
|
|
564
627
|
|
|
565
628
|
function newSegment(text) {
|
|
566
|
-
return { id: newSegmentId(), text, archived: false, archivedAt: null };
|
|
629
|
+
return { id: newSegmentId(), text, images: [], archived: false, archivedAt: null };
|
|
567
630
|
}
|
|
568
631
|
|
|
569
632
|
function serializeSegments(segments) {
|
|
570
633
|
return segments.map((segment) => ({
|
|
571
634
|
id: segment.id,
|
|
572
635
|
text: segment.text,
|
|
636
|
+
...(Array.isArray(segment.images) && segment.images.length > 0
|
|
637
|
+
? { images: segment.images.map((image) => ({ path: image.path, name: image.name })) }
|
|
638
|
+
: {}),
|
|
573
639
|
...(segment.archived === true ? { archived: true } : {}),
|
|
574
640
|
...(segment.archived === true && typeof segment.archivedAt === 'string' ? { archivedAt: segment.archivedAt } : {}),
|
|
575
641
|
}));
|
|
@@ -588,6 +654,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
588
654
|
out.push({
|
|
589
655
|
id,
|
|
590
656
|
text: item.text,
|
|
657
|
+
images: normalizeImages(item.images),
|
|
591
658
|
archived,
|
|
592
659
|
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
593
660
|
});
|
|
@@ -598,13 +665,196 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
598
665
|
return text === '' ? [] : [newSegment(text)];
|
|
599
666
|
}
|
|
600
667
|
|
|
668
|
+
function baseNameOf(value) {
|
|
669
|
+
const trimmed = String(value ?? '').replace(/[/\\]+$/u, '');
|
|
670
|
+
const separator = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
671
|
+
return trimmed.slice(separator + 1);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function normalizeImage(value) {
|
|
675
|
+
if (typeof value === 'string') value = { path: value };
|
|
676
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
677
|
+
if (typeof value.path !== 'string') return null;
|
|
678
|
+
const filePath = value.path.trim().replace(/\\/gu, '/');
|
|
679
|
+
if (filePath === '') return null;
|
|
680
|
+
const rawName = typeof value.name === 'string' && value.name.trim() !== ''
|
|
681
|
+
? value.name.trim().replace(/\s+/gu, ' ')
|
|
682
|
+
: baseNameOf(filePath);
|
|
683
|
+
return { path: filePath, name: rawName === '' ? 'image' : rawName.slice(0, 160) };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function normalizeImages(value) {
|
|
687
|
+
if (!Array.isArray(value)) return [];
|
|
688
|
+
const out = [];
|
|
689
|
+
for (const item of value) {
|
|
690
|
+
if (out.length >= MAX_IMAGES) break;
|
|
691
|
+
const image = normalizeImage(item);
|
|
692
|
+
if (image !== null) out.push(image);
|
|
693
|
+
}
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Markdown reference used in the note body and composer transfers. */
|
|
698
|
+
function imageMarkdown(image) {
|
|
699
|
+
const file = typeof image?.path === 'string' ? image.path.replace(/\\/gu, '/') : '';
|
|
700
|
+
if (file === '') return '';
|
|
701
|
+
const label = (typeof image?.name === 'string' ? image.name : '')
|
|
702
|
+
.replace(/[\r\n]+/gu, ' ')
|
|
703
|
+
.replace(/[[\]]/gu, ' ')
|
|
704
|
+
.trim() || baseNameOf(file) || 'image';
|
|
705
|
+
let target;
|
|
706
|
+
try {
|
|
707
|
+
target = encodeURI(file);
|
|
708
|
+
} catch {
|
|
709
|
+
target = file;
|
|
710
|
+
}
|
|
711
|
+
target = target
|
|
712
|
+
.replace(/#/gu, '%23')
|
|
713
|
+
.replace(/\?/gu, '%3F')
|
|
714
|
+
.replace(/\(/gu, '%28')
|
|
715
|
+
.replace(/\)/gu, '%29');
|
|
716
|
+
return ``;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** One segment as text plus one Markdown line per attached image. */
|
|
720
|
+
function segmentBody(segment) {
|
|
721
|
+
if (segment === null || segment === undefined) return '';
|
|
722
|
+
const text = typeof segment.text === 'string' ? segment.text : '';
|
|
723
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
724
|
+
const parts = [];
|
|
725
|
+
if (text.trim() !== '') parts.push(text);
|
|
726
|
+
for (const image of images) {
|
|
727
|
+
const markdown = imageMarkdown(image);
|
|
728
|
+
if (markdown !== '') parts.push(markdown);
|
|
729
|
+
}
|
|
730
|
+
return parts.join('\n');
|
|
731
|
+
}
|
|
732
|
+
|
|
601
733
|
function mergedText(segments) {
|
|
602
734
|
return segments
|
|
603
|
-
.map((segment) => (segment
|
|
604
|
-
.filter((text) => text
|
|
735
|
+
.map((segment) => segmentBody(segment))
|
|
736
|
+
.filter((text) => text !== '')
|
|
605
737
|
.join('\n\n');
|
|
606
738
|
}
|
|
607
739
|
|
|
740
|
+
function isAbsolutePath(value) {
|
|
741
|
+
const text = String(value ?? '');
|
|
742
|
+
return text.startsWith('/') || /^[A-Za-z]:\//u.test(text) || text.startsWith('//');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function normalizedCwd(value) {
|
|
746
|
+
return String(value ?? '').replace(/\\/gu, '/').replace(/\/+$/u, '');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Store a path relative to the workspace when it lives inside it. */
|
|
750
|
+
function imagePathForNote(pathValue, cwd) {
|
|
751
|
+
const file = String(pathValue ?? '').replace(/\\/gu, '/');
|
|
752
|
+
const root = normalizedCwd(cwd);
|
|
753
|
+
if (root === '') return file;
|
|
754
|
+
// Windows drive / UNC spellings are case-insensitive; POSIX ones are not.
|
|
755
|
+
const windows = /^[A-Za-z]:\//u.test(root) || root.startsWith('//');
|
|
756
|
+
const compareFile = windows ? file.toLowerCase() : file;
|
|
757
|
+
const compareRoot = windows ? root.toLowerCase() : root;
|
|
758
|
+
if (compareFile === compareRoot) return '.';
|
|
759
|
+
if (compareFile.startsWith(`${compareRoot}/`)) return file.slice(root.length + 1);
|
|
760
|
+
return file;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function absoluteImagePath(pathValue, cwd) {
|
|
764
|
+
const file = String(pathValue ?? '').replace(/\\/gu, '/');
|
|
765
|
+
if (isAbsolutePath(file)) return file;
|
|
766
|
+
const root = normalizedCwd(cwd);
|
|
767
|
+
return root === '' ? file : `${root}/${file.replace(/^\/+/u, '')}`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Authenticated host file route used by the browser image renderer. */
|
|
771
|
+
function imageUrl(pathValue, cwd) {
|
|
772
|
+
const target = absoluteImagePath(pathValue, cwd);
|
|
773
|
+
try {
|
|
774
|
+
const base = typeof document !== 'undefined' && typeof document.baseURI === 'string'
|
|
775
|
+
? document.baseURI
|
|
776
|
+
: 'http://localhost/';
|
|
777
|
+
const url = new URL('api/file', base);
|
|
778
|
+
url.searchParams.set('path', target);
|
|
779
|
+
return url.href;
|
|
780
|
+
} catch {
|
|
781
|
+
return `/api/file?path=${encodeURIComponent(target)}`;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function imageMimeOf(file) {
|
|
786
|
+
const type = typeof file?.type === 'string' ? file.type.toLowerCase() : '';
|
|
787
|
+
if (type !== '') return Object.prototype.hasOwnProperty.call(IMAGE_MIME, type) ? type : '';
|
|
788
|
+
const name = String(file?.name ?? '');
|
|
789
|
+
if (/\.png$/iu.test(name)) return 'image/png';
|
|
790
|
+
if (/\.jpe?g$/iu.test(name)) return 'image/jpeg';
|
|
791
|
+
if (/\.webp$/iu.test(name)) return 'image/webp';
|
|
792
|
+
if (/\.gif$/iu.test(name)) return 'image/gif';
|
|
793
|
+
return '';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function isSupportedImage(file) {
|
|
797
|
+
return imageMimeOf(file) !== '';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function hostPathOf(file) {
|
|
801
|
+
try {
|
|
802
|
+
const bridge = globalThis.__DSH_HOST_PATHS__;
|
|
803
|
+
if (bridge === null || bridge === undefined || typeof bridge.pathFor !== 'function') return '';
|
|
804
|
+
const value = bridge.pathFor(file);
|
|
805
|
+
return typeof value === 'string' ? value : '';
|
|
806
|
+
} catch {
|
|
807
|
+
return '';
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
class ImageError extends Error {
|
|
812
|
+
constructor(code, message) {
|
|
813
|
+
super(message ?? code);
|
|
814
|
+
this.name = 'ImageError';
|
|
815
|
+
this.code = code;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function readFileBase64(file) {
|
|
820
|
+
return new Promise((resolve, reject) => {
|
|
821
|
+
try {
|
|
822
|
+
const reader = new FileReader();
|
|
823
|
+
reader.onerror = () => reject(new ImageError('read-failed', 'Could not read the image file.'));
|
|
824
|
+
reader.onload = () => {
|
|
825
|
+
const result = typeof reader.result === 'string' ? reader.result : '';
|
|
826
|
+
const comma = result.indexOf(',');
|
|
827
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
828
|
+
};
|
|
829
|
+
reader.readAsDataURL(file);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
reject(error);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function uploadImage(root, sessionId, file) {
|
|
837
|
+
if (Number(file?.size) > MAX_IMAGE_BYTES) throw new ImageError('too-large');
|
|
838
|
+
const mime = imageMimeOf(file);
|
|
839
|
+
if (mime === '') throw new ImageError('unsupported');
|
|
840
|
+
const data = await readFileBase64(file);
|
|
841
|
+
const response = await fetch(`${API}/image`, {
|
|
842
|
+
method: 'POST',
|
|
843
|
+
headers: { 'Content-Type': 'application/json' },
|
|
844
|
+
body: JSON.stringify({
|
|
845
|
+
root,
|
|
846
|
+
sessionId,
|
|
847
|
+
name: typeof file?.name === 'string' && file.name !== '' ? file.name : 'image.png',
|
|
848
|
+
mime,
|
|
849
|
+
data,
|
|
850
|
+
}),
|
|
851
|
+
});
|
|
852
|
+
const payload = await readResponse(response);
|
|
853
|
+
const image = normalizeImage(payload);
|
|
854
|
+
if (image === null) throw new ImageError('upload-failed', 'The host did not return an image path.');
|
|
855
|
+
return image;
|
|
856
|
+
}
|
|
857
|
+
|
|
608
858
|
function autoGrow(element) {
|
|
609
859
|
if (element === null || element === undefined || element.style === undefined) return;
|
|
610
860
|
try {
|
|
@@ -769,6 +1019,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
769
1019
|
const [ui, setUi] = useState(readStoredUi);
|
|
770
1020
|
const [sending, setSending] = useState(false);
|
|
771
1021
|
const [resizing, setResizing] = useState(false);
|
|
1022
|
+
const [uploading, setUploading] = useState({});
|
|
1023
|
+
const [dragSegment, setDragSegment] = useState('');
|
|
772
1024
|
|
|
773
1025
|
const bindingRef = useRef(null);
|
|
774
1026
|
const rootSessionRef = useRef('');
|
|
@@ -789,6 +1041,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
789
1041
|
const resizeRef = useRef(null);
|
|
790
1042
|
const sendingRef = useRef(false);
|
|
791
1043
|
const textareaRefs = useRef(new Map());
|
|
1044
|
+
const fileInputRefs = useRef(new Map());
|
|
792
1045
|
const listRef = useRef(null);
|
|
793
1046
|
const pendingFocusRef = useRef(null);
|
|
794
1047
|
const undoRef = useRef(null);
|
|
@@ -1118,6 +1371,153 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1118
1371
|
scheduleSave();
|
|
1119
1372
|
};
|
|
1120
1373
|
|
|
1374
|
+
// Image attachments ---------------------------------------------------
|
|
1375
|
+
const imageErrorText = (reason) => {
|
|
1376
|
+
const code = reason?.code;
|
|
1377
|
+
if (code === 'too-large') return t('imageTooLarge');
|
|
1378
|
+
if (code === 'unsupported') return t('imageUnsupported');
|
|
1379
|
+
if (code === 'too-many') return t('imageTooMany', { n: MAX_IMAGES });
|
|
1380
|
+
return `${t('imageFailed')}: ${String(reason?.message ?? reason)}`;
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
const appendImages = (id, images) => {
|
|
1384
|
+
if (!Array.isArray(images) || images.length === 0) return 0;
|
|
1385
|
+
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1386
|
+
if (target === undefined || target.archived === true) return 0;
|
|
1387
|
+
const existing = Array.isArray(target.images) ? target.images : [];
|
|
1388
|
+
const room = Math.max(0, MAX_IMAGES - existing.length);
|
|
1389
|
+
if (room === 0) {
|
|
1390
|
+
showToast(t('imageTooMany', { n: MAX_IMAGES }));
|
|
1391
|
+
return 0;
|
|
1392
|
+
}
|
|
1393
|
+
const accepted = images.slice(0, room);
|
|
1394
|
+
const next = segmentsRef.current.map((segment) =>
|
|
1395
|
+
(segment.id === id ? { ...segment, images: [...existing, ...accepted] } : segment));
|
|
1396
|
+
commitSegments(next);
|
|
1397
|
+
if (accepted.length < images.length) showToast(t('imageTooMany', { n: MAX_IMAGES }));
|
|
1398
|
+
else showToast(t('imageAdded', { n: accepted.length }));
|
|
1399
|
+
return accepted.length;
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Add browser files to one segment. Pasted images are always written into
|
|
1404
|
+
* the plugin directory; selected images use their host path when the
|
|
1405
|
+
* desktop bridge exposes it and fall back to the same upload otherwise.
|
|
1406
|
+
* @param {string} id segment id
|
|
1407
|
+
* @param {ArrayLike<File>} files
|
|
1408
|
+
* @param {'paste'|'pick'} origin
|
|
1409
|
+
*/
|
|
1410
|
+
const attachImages = async (id, files, origin) => {
|
|
1411
|
+
const list = Array.from(files ?? []).filter(isSupportedImage);
|
|
1412
|
+
if (list.length === 0) {
|
|
1413
|
+
showToast(t('imageUnsupported'));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const root = rootRef.current;
|
|
1417
|
+
const sessionId = rootSessionRef.current;
|
|
1418
|
+
if (root === '' || sessionId === '') return;
|
|
1419
|
+
setUploading((previous) => ({ ...previous, [id]: (previous[id] ?? 0) + list.length }));
|
|
1420
|
+
const collected = [];
|
|
1421
|
+
try {
|
|
1422
|
+
for (const file of list) {
|
|
1423
|
+
const directPath = origin === 'pick' ? hostPathOf(file) : '';
|
|
1424
|
+
if (directPath !== '') {
|
|
1425
|
+
collected.push({
|
|
1426
|
+
path: imagePathForNote(directPath, bindingRef.current?.cwd ?? root),
|
|
1427
|
+
name: baseNameOf(directPath) || file?.name || 'image',
|
|
1428
|
+
});
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
collected.push(await uploadImage(root, sessionId, file));
|
|
1432
|
+
}
|
|
1433
|
+
if (collected.length > 0) appendImages(id, collected);
|
|
1434
|
+
} catch (reason) {
|
|
1435
|
+
if (collected.length > 0) appendImages(id, collected);
|
|
1436
|
+
showToast(imageErrorText(reason));
|
|
1437
|
+
} finally {
|
|
1438
|
+
setUploading((previous) => {
|
|
1439
|
+
const next = { ...previous };
|
|
1440
|
+
const remaining = (next[id] ?? 0) - list.length;
|
|
1441
|
+
if (remaining > 0) next[id] = remaining;
|
|
1442
|
+
else delete next[id];
|
|
1443
|
+
return next;
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const removeImage = (id, pathValue) => {
|
|
1449
|
+
const next = segmentsRef.current.map((segment) => {
|
|
1450
|
+
if (segment.id !== id) return segment;
|
|
1451
|
+
const images = Array.isArray(segment.images) ? segment.images.filter((image) => image.path !== pathValue) : [];
|
|
1452
|
+
return { ...segment, images };
|
|
1453
|
+
});
|
|
1454
|
+
commitSegments(next);
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
const triggerFilePicker = (id) => {
|
|
1458
|
+
const input = fileInputRefs.current.get(id);
|
|
1459
|
+
if (input !== undefined && input !== null && typeof input.click === 'function') input.click();
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
const onFileInputChange = (event, id) => {
|
|
1463
|
+
const input = event?.currentTarget ?? event?.target;
|
|
1464
|
+
const files = input?.files;
|
|
1465
|
+
if (files !== undefined && files !== null && files.length > 0) attachImages(id, files, 'pick');
|
|
1466
|
+
try {
|
|
1467
|
+
if (input !== null && input !== undefined) input.value = '';
|
|
1468
|
+
} catch {
|
|
1469
|
+
/* A read-only file input value must not break the flow. */
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
const clipboardImageFiles = (data) => {
|
|
1474
|
+
const files = data?.files;
|
|
1475
|
+
if (files === undefined || files === null) return [];
|
|
1476
|
+
return Array.from(files).filter(isSupportedImage);
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
const onSegmentPaste = (event, id) => {
|
|
1480
|
+
const data = event?.clipboardData ?? event?.nativeEvent?.clipboardData;
|
|
1481
|
+
const files = clipboardImageFiles(data);
|
|
1482
|
+
if (files.length === 0) return;
|
|
1483
|
+
event.preventDefault?.();
|
|
1484
|
+
event.stopPropagation?.();
|
|
1485
|
+
attachImages(id, files, 'paste');
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
const onSegmentDrop = (event, id) => {
|
|
1489
|
+
const files = clipboardImageFiles(event?.dataTransfer);
|
|
1490
|
+
setDragSegment('');
|
|
1491
|
+
if (files.length === 0) return;
|
|
1492
|
+
event.preventDefault?.();
|
|
1493
|
+
event.stopPropagation?.();
|
|
1494
|
+
attachImages(id, files, 'pick');
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
const onSegmentDragOver = (event, id) => {
|
|
1498
|
+
const types = event?.dataTransfer?.types;
|
|
1499
|
+
if (types !== undefined && types !== null && !Array.from(types).includes('Files')) return;
|
|
1500
|
+
event.preventDefault?.();
|
|
1501
|
+
if (dragSegment !== id) setDragSegment(id);
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
const onSegmentDragLeave = (event, id) => {
|
|
1505
|
+
const related = event?.relatedTarget;
|
|
1506
|
+
const current = event?.currentTarget;
|
|
1507
|
+
if (related !== null && related !== undefined && current !== null && current !== undefined
|
|
1508
|
+
&& typeof current.contains === 'function' && current.contains(related)) return;
|
|
1509
|
+
if (dragSegment === id) setDragSegment('');
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
const openImage = (image) => {
|
|
1513
|
+
const url = imageUrl(image.path, rootRef.current);
|
|
1514
|
+
try {
|
|
1515
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1516
|
+
} catch {
|
|
1517
|
+
/* Popups may be blocked; the path remains available in the title. */
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1121
1521
|
const addSegment = () => {
|
|
1122
1522
|
if (segmentsRef.current.length >= MAX_SEGMENTS) {
|
|
1123
1523
|
showToast(t('tooManySegments', { n: MAX_SEGMENTS }));
|
|
@@ -1520,9 +1920,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1520
1920
|
statusText = clock === '' ? t('saved') : `${t('saved')} · ${clock}`;
|
|
1521
1921
|
} else statusText = t('empty');
|
|
1522
1922
|
|
|
1523
|
-
const archiveButtonTitle = ui.showArchive
|
|
1524
|
-
? t('hideArchive', { n: archivedSegments.length })
|
|
1525
|
-
: t('showArchive', { n: archivedSegments.length });
|
|
1923
|
+
const archiveButtonTitle = ui.showArchive ? t('hideArchive') : t('showArchive');
|
|
1526
1924
|
|
|
1527
1925
|
const header = h(
|
|
1528
1926
|
'div',
|
|
@@ -1555,9 +1953,6 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1555
1953
|
onClick: toggleArchive,
|
|
1556
1954
|
},
|
|
1557
1955
|
h(ArchiveIcon, { size: 15 }),
|
|
1558
|
-
archivedSegments.length > 0
|
|
1559
|
-
? h('span', { className: 'dsh-np-badge' }, String(archivedSegments.length))
|
|
1560
|
-
: null,
|
|
1561
1956
|
),
|
|
1562
1957
|
h(
|
|
1563
1958
|
'button',
|
|
@@ -1677,101 +2072,201 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1677
2072
|
: null,
|
|
1678
2073
|
);
|
|
1679
2074
|
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
},
|
|
1689
|
-
h(
|
|
2075
|
+
const totalImages = segments.reduce(
|
|
2076
|
+
(sum, segment) => sum + (Array.isArray(segment.images) ? segment.images.length : 0), 0);
|
|
2077
|
+
|
|
2078
|
+
const renderMedia = (segment, options = {}) => {
|
|
2079
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2080
|
+
const pending = options.pending === true;
|
|
2081
|
+
if (images.length === 0 && !pending) return null;
|
|
2082
|
+
return h(
|
|
1690
2083
|
'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
|
-
|
|
2084
|
+
{ className: 'dsh-np-medias' },
|
|
2085
|
+
pending
|
|
2086
|
+
? h(
|
|
2087
|
+
'div',
|
|
2088
|
+
{ key: 'pending', className: 'dsh-np-img', 'data-pending': 'true' },
|
|
2089
|
+
h('span', { className: 'dsh-np-img-spinner' }),
|
|
2090
|
+
h('span', { className: 'dsh-np-img-name' }, t('imageUploading')),
|
|
2091
|
+
)
|
|
2092
|
+
: null,
|
|
2093
|
+
images.map((image, imageIndex) => h(
|
|
2094
|
+
'div',
|
|
2095
|
+
{ key: `${imageIndex}-${image.path}`, className: 'dsh-np-img', title: image.path },
|
|
2096
|
+
h(
|
|
2097
|
+
'button',
|
|
2098
|
+
{
|
|
2099
|
+
type: 'button',
|
|
2100
|
+
className: 'dsh-np-img-open',
|
|
2101
|
+
title: `${t('openImage')}: ${image.path}`,
|
|
2102
|
+
'aria-label': t('openImage'),
|
|
2103
|
+
onClick: () => openImage(image),
|
|
2104
|
+
},
|
|
2105
|
+
h('img', {
|
|
2106
|
+
src: imageUrl(image.path, binding.cwd),
|
|
2107
|
+
alt: image.name,
|
|
2108
|
+
loading: 'lazy',
|
|
2109
|
+
decoding: 'async',
|
|
2110
|
+
draggable: false,
|
|
2111
|
+
}),
|
|
2112
|
+
),
|
|
2113
|
+
h('span', { className: 'dsh-np-img-name', title: image.path }, image.name),
|
|
2114
|
+
options.readOnly === true
|
|
2115
|
+
? null
|
|
2116
|
+
: h(
|
|
2117
|
+
'button',
|
|
2118
|
+
{
|
|
2119
|
+
type: 'button',
|
|
2120
|
+
className: 'dsh-np-img-remove',
|
|
2121
|
+
title: t('removeImage'),
|
|
2122
|
+
'aria-label': t('removeImage'),
|
|
2123
|
+
onClick: () => removeImage(segment.id, image.path),
|
|
2124
|
+
},
|
|
2125
|
+
h(CloseIcon, { size: 11 }),
|
|
2126
|
+
),
|
|
2127
|
+
)),
|
|
2128
|
+
);
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
const activeCards = activeSegments.map((segment, index) => {
|
|
2132
|
+
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2133
|
+
const empty = segment.text.trim() === '' && images.length === 0;
|
|
2134
|
+
return h(
|
|
2135
|
+
'article',
|
|
2136
|
+
{
|
|
2137
|
+
key: segment.id,
|
|
2138
|
+
className: 'dsh-np-seg',
|
|
2139
|
+
'data-seg-id': segment.id,
|
|
2140
|
+
'data-archived': 'false',
|
|
2141
|
+
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
2142
|
+
},
|
|
2143
|
+
h('input', {
|
|
2144
|
+
key: 'file-input',
|
|
2145
|
+
ref: (element) => {
|
|
2146
|
+
if (element === null || element === undefined) {
|
|
2147
|
+
fileInputRefs.current.delete(segment.id);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
fileInputRefs.current.set(segment.id, element);
|
|
1725
2151
|
},
|
|
1726
|
-
|
|
1727
|
-
|
|
2152
|
+
className: 'dsh-np-file',
|
|
2153
|
+
type: 'file',
|
|
2154
|
+
accept: 'image/png,image/jpeg,image/webp,image/gif',
|
|
2155
|
+
multiple: true,
|
|
2156
|
+
tabIndex: -1,
|
|
2157
|
+
'aria-hidden': 'true',
|
|
2158
|
+
onChange: (event) => onFileInputChange(event, segment.id),
|
|
2159
|
+
}),
|
|
1728
2160
|
h(
|
|
1729
|
-
'
|
|
1730
|
-
{
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
title: t('
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2161
|
+
'div',
|
|
2162
|
+
{ className: 'dsh-np-seg-head' },
|
|
2163
|
+
h(
|
|
2164
|
+
'label',
|
|
2165
|
+
{ className: 'dsh-np-check', title: t('selectSegment') },
|
|
2166
|
+
h('input', {
|
|
2167
|
+
type: 'checkbox',
|
|
2168
|
+
checked: selected.has(segment.id),
|
|
2169
|
+
'aria-label': t('selectSegment'),
|
|
2170
|
+
onChange: () => toggleSelected(segment.id),
|
|
2171
|
+
}),
|
|
2172
|
+
),
|
|
2173
|
+
h('span', { className: 'dsh-np-index' }, String(index + 1).padStart(2, '0')),
|
|
2174
|
+
h('span', { className: 'dsh-np-spacer' }),
|
|
2175
|
+
h(
|
|
2176
|
+
'button',
|
|
2177
|
+
{
|
|
2178
|
+
type: 'button',
|
|
2179
|
+
className: 'dsh-np-segbtn',
|
|
2180
|
+
title: t('addImage'),
|
|
2181
|
+
'aria-label': t('addImage'),
|
|
2182
|
+
onClick: () => triggerFilePicker(segment.id),
|
|
2183
|
+
},
|
|
2184
|
+
h(ImageIcon, { size: 14 }),
|
|
2185
|
+
),
|
|
2186
|
+
h(
|
|
2187
|
+
'button',
|
|
2188
|
+
{
|
|
2189
|
+
type: 'button',
|
|
2190
|
+
className: 'dsh-np-segbtn',
|
|
2191
|
+
title: t('insertSegment'),
|
|
2192
|
+
'aria-label': t('insertSegment'),
|
|
2193
|
+
disabled: empty,
|
|
2194
|
+
onClick: () => insertSegments([segment]),
|
|
2195
|
+
},
|
|
2196
|
+
h(InsertIcon, { size: 14 }),
|
|
2197
|
+
),
|
|
2198
|
+
h(
|
|
2199
|
+
'button',
|
|
2200
|
+
{
|
|
2201
|
+
type: 'button',
|
|
2202
|
+
className: 'dsh-np-segbtn',
|
|
2203
|
+
title: t('replaceSegment'),
|
|
2204
|
+
'aria-label': t('replaceSegment'),
|
|
2205
|
+
disabled: empty,
|
|
2206
|
+
onClick: () => replaceSegments([segment]),
|
|
2207
|
+
},
|
|
2208
|
+
h(ReplaceIcon, { size: 14 }),
|
|
2209
|
+
),
|
|
2210
|
+
h(
|
|
2211
|
+
'button',
|
|
2212
|
+
{
|
|
2213
|
+
type: 'button',
|
|
2214
|
+
className: 'dsh-np-segbtn',
|
|
2215
|
+
title: t('sendSegment'),
|
|
2216
|
+
'aria-label': t('sendSegment'),
|
|
2217
|
+
disabled: empty || sending,
|
|
2218
|
+
onClick: () => sendSegments([segment]),
|
|
2219
|
+
},
|
|
2220
|
+
h(SendIcon, { size: 14 }),
|
|
2221
|
+
),
|
|
2222
|
+
h(
|
|
2223
|
+
'button',
|
|
2224
|
+
{
|
|
2225
|
+
type: 'button',
|
|
2226
|
+
className: 'dsh-np-segbtn',
|
|
2227
|
+
title: t('archiveSegment'),
|
|
2228
|
+
'aria-label': t('archiveSegment'),
|
|
2229
|
+
onClick: () => archiveSegment(segment.id),
|
|
2230
|
+
},
|
|
2231
|
+
h(ArchiveIcon, { size: 14 }),
|
|
2232
|
+
),
|
|
1739
2233
|
),
|
|
1740
2234
|
h(
|
|
1741
|
-
'
|
|
2235
|
+
'div',
|
|
1742
2236
|
{
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
2237
|
+
className: 'dsh-np-editor',
|
|
2238
|
+
'data-drag': dragSegment === segment.id ? 'true' : 'false',
|
|
2239
|
+
onPaste: (event) => onSegmentPaste(event, segment.id),
|
|
2240
|
+
onDrop: (event) => onSegmentDrop(event, segment.id),
|
|
2241
|
+
onDragOver: (event) => onSegmentDragOver(event, segment.id),
|
|
2242
|
+
onDragLeave: (event) => onSegmentDragLeave(event, segment.id),
|
|
1748
2243
|
},
|
|
1749
|
-
|
|
2244
|
+
renderMedia(segment, { pending: (uploading[segment.id] ?? 0) > 0 }),
|
|
2245
|
+
h('textarea', {
|
|
2246
|
+
ref: (element) => {
|
|
2247
|
+
if (element === null || element === undefined) {
|
|
2248
|
+
textareaRefs.current.delete(segment.id);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
textareaRefs.current.set(segment.id, element);
|
|
2252
|
+
autoGrow(element);
|
|
2253
|
+
},
|
|
2254
|
+
className: 'dsh-np-seg-text',
|
|
2255
|
+
rows: 1,
|
|
2256
|
+
value: segment.text,
|
|
2257
|
+
placeholder: index === 0 ? t('placeholder') : t('segmentPlaceholder'),
|
|
2258
|
+
spellCheck: false,
|
|
2259
|
+
maxLength: MAX_SEGMENT_CHARS,
|
|
2260
|
+
'aria-label': t('editor'),
|
|
2261
|
+
onChange: (event) => onSegmentChange(event, segment.id),
|
|
2262
|
+
onKeyDown: (event) => onSegmentKeyDown(event, segment.id),
|
|
2263
|
+
onBlur: () => {
|
|
2264
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
2265
|
+
},
|
|
2266
|
+
}),
|
|
1750
2267
|
),
|
|
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
|
-
));
|
|
2268
|
+
);
|
|
2269
|
+
});
|
|
1775
2270
|
|
|
1776
2271
|
const activeEmpty = activeSegments.length === 0
|
|
1777
2272
|
? h(
|
|
@@ -1849,7 +2344,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1849
2344
|
className: 'dsh-np-segbtn',
|
|
1850
2345
|
title: t('insertSegment'),
|
|
1851
2346
|
'aria-label': t('insertSegment'),
|
|
1852
|
-
disabled: segment.text.trim() === ''
|
|
2347
|
+
disabled: segment.text.trim() === ''
|
|
2348
|
+
&& !(Array.isArray(segment.images) && segment.images.length > 0),
|
|
1853
2349
|
onClick: () => insertSegments([segment]),
|
|
1854
2350
|
},
|
|
1855
2351
|
h(InsertIcon, { size: 14 }),
|
|
@@ -1861,7 +2357,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1861
2357
|
className: 'dsh-np-segbtn',
|
|
1862
2358
|
title: t('replaceSegment'),
|
|
1863
2359
|
'aria-label': t('replaceSegment'),
|
|
1864
|
-
disabled: segment.text.trim() === ''
|
|
2360
|
+
disabled: segment.text.trim() === ''
|
|
2361
|
+
&& !(Array.isArray(segment.images) && segment.images.length > 0),
|
|
1865
2362
|
onClick: () => replaceSegments([segment]),
|
|
1866
2363
|
},
|
|
1867
2364
|
h(ReplaceIcon, { size: 14 }),
|
|
@@ -1891,7 +2388,10 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1891
2388
|
),
|
|
1892
2389
|
),
|
|
1893
2390
|
h('div', { className: 'dsh-np-seg-archived-text' },
|
|
1894
|
-
segment.text === ''
|
|
2391
|
+
segment.text === ''
|
|
2392
|
+
? (Array.isArray(segment.images) && segment.images.length > 0 ? null : t('emptySegment'))
|
|
2393
|
+
: segment.text),
|
|
2394
|
+
renderMedia(segment, { readOnly: true }),
|
|
1895
2395
|
)),
|
|
1896
2396
|
),
|
|
1897
2397
|
),
|
|
@@ -1921,7 +2421,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1921
2421
|
}, t('retry'))
|
|
1922
2422
|
: null,
|
|
1923
2423
|
h('span', { className: 'dsh-np-count' },
|
|
1924
|
-
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
|
|
2424
|
+
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
|
|
2425
|
+
+ (totalImages > 0 ? ` · ${t('images', { n: totalImages })}` : '')),
|
|
1925
2426
|
h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
|
|
1926
2427
|
);
|
|
1927
2428
|
|