@restregular/dsh-notepad 0.3.0 → 0.4.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 +42 -21
- package/client.js +685 -31
- package/index.js +194 -9
- package/lib/image-store.js +50 -1
- package/lib/note-store.js +76 -9
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/client.js
CHANGED
|
@@ -50,8 +50,12 @@ 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_TITLE_CHARS = 200;
|
|
53
54
|
const MAX_IMAGES = 20;
|
|
54
55
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
56
|
+
const MAX_OPTIMIZE_CHARS = 60000;
|
|
57
|
+
const SIDEBAR_REFRESH_MS = 20000;
|
|
58
|
+
const IMAGE_DELETE_DELAY = 5000;
|
|
55
59
|
const IMAGE_MIME = {
|
|
56
60
|
'image/png': 'png',
|
|
57
61
|
'image/jpeg': 'jpg',
|
|
@@ -68,6 +72,9 @@ window.__ModuleLoader__.load({
|
|
|
68
72
|
editor: '分段内容',
|
|
69
73
|
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
70
74
|
segmentPlaceholder: '新分段…',
|
|
75
|
+
titlePlaceholder: '分段标题(可选)',
|
|
76
|
+
segmentTitle: '分段标题',
|
|
77
|
+
indexHint: '点击选择此段 · 拖动可排序',
|
|
71
78
|
loading: '读取中…',
|
|
72
79
|
empty: '还没有内容',
|
|
73
80
|
unsaved: '未保存',
|
|
@@ -102,14 +109,30 @@ window.__ModuleLoader__.load({
|
|
|
102
109
|
selectHint: '勾选分段可批量操作',
|
|
103
110
|
selected: '已选 {n} 段',
|
|
104
111
|
selectSegment: '选择此段',
|
|
112
|
+
merge: '合并分段',
|
|
113
|
+
mergeSelected: '合并选中的 {n} 段',
|
|
114
|
+
mergeHint: '至少选择 2 段才能合并',
|
|
115
|
+
mergedToast: '已合并 {n} 段',
|
|
116
|
+
optimizeSegment: 'AI 优化此段',
|
|
117
|
+
optimizeAll: 'AI 优化全部分段',
|
|
118
|
+
optimizeAllProgress: 'AI 优化中 {done}/{total}',
|
|
119
|
+
optimizing: '优化中…',
|
|
120
|
+
optimizedToast: '已优化此段',
|
|
121
|
+
optimizedCount: '已优化 {n} 段',
|
|
122
|
+
optimizeEmpty: '没有可优化的文字',
|
|
123
|
+
optimizeTooLong: '内容太长,无法优化',
|
|
124
|
+
optimizeUnavailable: 'AI 服务不可用',
|
|
125
|
+
optimizeFailed: '优化失败',
|
|
126
|
+
dragSegment: '拖动排序',
|
|
127
|
+
unsent: '{n} 条未发送',
|
|
105
128
|
archive: '归档',
|
|
106
129
|
archiveSegment: '归档此段',
|
|
107
130
|
archiveSelected: '归档选中的 {n} 段',
|
|
108
131
|
archived: '归档',
|
|
109
132
|
archivedToast: '已归档,可在归档中查看',
|
|
110
133
|
archivedCount: '已归档 {n} 段',
|
|
111
|
-
showArchive: '
|
|
112
|
-
hideArchive: '
|
|
134
|
+
showArchive: '显示归档',
|
|
135
|
+
hideArchive: '隐藏归档',
|
|
113
136
|
archiveCount: '归档 · {n}',
|
|
114
137
|
archiveEmpty: '还没有归档的分段',
|
|
115
138
|
noActive: '没有进行中的分段',
|
|
@@ -153,6 +176,9 @@ window.__ModuleLoader__.load({
|
|
|
153
176
|
editor: 'Segment content',
|
|
154
177
|
placeholder: 'Jot down problems, or what to build next…',
|
|
155
178
|
segmentPlaceholder: 'New segment…',
|
|
179
|
+
titlePlaceholder: 'Segment title (optional)',
|
|
180
|
+
segmentTitle: 'Segment title',
|
|
181
|
+
indexHint: 'Click to select · drag to reorder',
|
|
156
182
|
loading: 'Loading…',
|
|
157
183
|
empty: 'Nothing here yet',
|
|
158
184
|
unsaved: 'Unsaved',
|
|
@@ -187,14 +213,30 @@ window.__ModuleLoader__.load({
|
|
|
187
213
|
selectHint: 'Check segments to batch',
|
|
188
214
|
selected: '{n} selected',
|
|
189
215
|
selectSegment: 'Select this segment',
|
|
216
|
+
merge: 'Merge segments',
|
|
217
|
+
mergeSelected: 'Merge {n} selected',
|
|
218
|
+
mergeHint: 'Select at least 2 segments to merge',
|
|
219
|
+
mergedToast: 'Merged {n} segments',
|
|
220
|
+
optimizeSegment: 'Improve this segment with AI',
|
|
221
|
+
optimizeAll: 'Improve all segments with AI',
|
|
222
|
+
optimizeAllProgress: 'Improving {done}/{total}',
|
|
223
|
+
optimizing: 'Improving…',
|
|
224
|
+
optimizedToast: 'Segment improved',
|
|
225
|
+
optimizedCount: 'Improved {n} segments',
|
|
226
|
+
optimizeEmpty: 'Nothing to improve',
|
|
227
|
+
optimizeTooLong: 'Too long to improve',
|
|
228
|
+
optimizeUnavailable: 'AI service unavailable',
|
|
229
|
+
optimizeFailed: 'Improve failed',
|
|
230
|
+
dragSegment: 'Drag to reorder',
|
|
231
|
+
unsent: '{n} unsent',
|
|
190
232
|
archive: 'Archive',
|
|
191
233
|
archiveSegment: 'Archive this segment',
|
|
192
234
|
archiveSelected: 'Archive {n} selected',
|
|
193
235
|
archived: 'Archived',
|
|
194
236
|
archivedToast: 'Archived; available under Archive',
|
|
195
237
|
archivedCount: 'Archived {n}',
|
|
196
|
-
showArchive: 'Show archive
|
|
197
|
-
hideArchive: 'Hide archive
|
|
238
|
+
showArchive: 'Show archive',
|
|
239
|
+
hideArchive: 'Hide archive',
|
|
198
240
|
archiveCount: 'Archive · {n}',
|
|
199
241
|
archiveEmpty: 'Nothing archived yet',
|
|
200
242
|
noActive: 'No active segments',
|
|
@@ -488,6 +530,42 @@ window.__ModuleLoader__.load({
|
|
|
488
530
|
);
|
|
489
531
|
}
|
|
490
532
|
|
|
533
|
+
/** Four-point sparkle: an AI improvement action. */
|
|
534
|
+
function SparkleIcon({ size = 16 }) {
|
|
535
|
+
return h(
|
|
536
|
+
Svg,
|
|
537
|
+
{ size, viewBox: '0 0 16 16' },
|
|
538
|
+
h('path', {
|
|
539
|
+
d: 'M8 1.8c.5 2.7 1.5 3.7 4.2 4.2-2.7.5-3.7 1.5-4.2 4.2-.5-2.7-1.5-3.7-4.2-4.2C6.5 5.5 7.5 4.5 8 1.8Z',
|
|
540
|
+
stroke: 'currentColor', strokeWidth: 1.2,
|
|
541
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
542
|
+
}),
|
|
543
|
+
h('path', {
|
|
544
|
+
d: 'M12.4 9.6c.3 1.5.8 2 2.3 2.3-1.5.3-2 .8-2.3 2.3-.3-1.5-.8-2-2.3-2.3 1.5-.3 2-.8 2.3-2.3Z',
|
|
545
|
+
stroke: 'currentColor', strokeWidth: 1.1,
|
|
546
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Two arrows converging: merge selected segments into one. */
|
|
552
|
+
function MergeIcon({ size = 16 }) {
|
|
553
|
+
return h(
|
|
554
|
+
Svg,
|
|
555
|
+
{ size, viewBox: '0 0 16 16' },
|
|
556
|
+
h('path', {
|
|
557
|
+
d: 'M3.2 3.4v2.2c0 1.4 1.1 2.5 2.5 2.5h4.6c1.4 0 2.5 1.1 2.5 2.5v2.2',
|
|
558
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
559
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
560
|
+
}),
|
|
561
|
+
h('path', {
|
|
562
|
+
d: 'M3.2 12.6v-2.2c0-1.4 1.1-2.5 2.5-2.5h4.6c1.4 0 2.5-1.1 2.5-2.5V3.4',
|
|
563
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
564
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
491
569
|
/** Arrow towards the bottom-right corner: minimize to the corner icon. */
|
|
492
570
|
function MinimizeIcon({ size = 16 }) {
|
|
493
571
|
return h(
|
|
@@ -531,10 +609,9 @@ window.__ModuleLoader__.load({
|
|
|
531
609
|
.dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
|
|
532
610
|
.dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
533
611
|
.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}
|
|
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}
|
|
535
612
|
.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}
|
|
536
613
|
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
537
|
-
.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))}
|
|
614
|
+
.dsh-np-tools{display:flex;align-items:center;flex-wrap:wrap;gap:4px;flex:none;padding:0 2px 7px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08))}
|
|
538
615
|
.dsh-np-selectall{display:inline-flex;align-items:center;gap:6px;min-width:0;cursor:pointer;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;white-space:nowrap}
|
|
539
616
|
.dsh-np-selectall input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer;flex:none}
|
|
540
617
|
.dsh-np-selectall input:disabled{cursor:default;opacity:.45}
|
|
@@ -544,12 +621,23 @@ window.__ModuleLoader__.load({
|
|
|
544
621
|
.dsh-np-seg[data-selected=true]{border-color:var(--np-accent);background:color-mix(in srgb,var(--np-accent) 6%,var(--np-surface-2));box-shadow:0 0 0 1px var(--np-accent) inset}
|
|
545
622
|
.dsh-np-seg[data-archived=true]{border-style:dashed;background:transparent;opacity:.82;animation:none}
|
|
546
623
|
.dsh-np-seg-head{display:flex;align-items:center;gap:3px;flex:none;min-height:30px;padding:3px 5px 0 7px;box-sizing:border-box}
|
|
624
|
+
.dsh-np-seg[data-reorder-drag=true]{opacity:.45}
|
|
625
|
+
.dsh-np-seg[data-drop-before=true]{box-shadow:0 -2px 0 0 var(--np-accent)}
|
|
626
|
+
.dsh-np-seg[data-drop-after=true]{box-shadow:0 2px 0 0 var(--np-accent)}
|
|
547
627
|
.dsh-np-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;cursor:pointer}
|
|
548
628
|
.dsh-np-check input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer}
|
|
549
|
-
.dsh-np-index{flex:none;color:var(--dsw-alias-label-caption,#9aa3b2);font-size:10px;font-weight:
|
|
629
|
+
.dsh-np-index{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:20px;padding:0 2px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);font:inherit;font-size:10px;font-weight:700;letter-spacing:.04em;font-variant-numeric:tabular-nums;cursor:pointer;user-select:none}
|
|
630
|
+
.dsh-np-index:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
|
|
631
|
+
.dsh-np-index[aria-pressed=true]{background:color-mix(in srgb,var(--np-accent) 14%,transparent);color:var(--np-accent)}
|
|
632
|
+
.dsh-np-title-input{flex:1 1 auto;min-width:0;width:0;height:20px;padding:0 4px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:11px;font-weight:600;line-height:20px;outline:0}
|
|
633
|
+
.dsh-np-title-input::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2);font-weight:400}
|
|
634
|
+
.dsh-np-title-input:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
635
|
+
.dsh-np-title-input:focus{background:var(--np-surface);box-shadow:0 0 0 1px var(--np-accent) inset;color:var(--dsw-alias-label-primary,#1f2430)}
|
|
636
|
+
.dsh-np-seg-title{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#4b5563);font-size:11px;font-weight:600}
|
|
550
637
|
.dsh-np-archived-chip{flex:none;padding:0 6px;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10px;line-height:16px}
|
|
551
638
|
.dsh-np-segbtn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;transition:background .12s,color .12s,opacity .12s;opacity:.8}
|
|
552
639
|
.dsh-np-segbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430);opacity:1}
|
|
640
|
+
.dsh-np-segbtn[data-ai=true]{color:var(--np-accent);opacity:1}
|
|
553
641
|
.dsh-np-segbtn[data-danger=true]:hover:not(:disabled){color:var(--dsw-alias-state-error-primary,#e25555);background:var(--dsw-alias-interactive-bg-hover-danger,rgb(236 19 19/.05))}
|
|
554
642
|
.dsh-np-segbtn:disabled{opacity:.3;cursor:default}
|
|
555
643
|
.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}
|
|
@@ -567,6 +655,7 @@ window.__ModuleLoader__.load({
|
|
|
567
655
|
.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
656
|
.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
657
|
.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}
|
|
658
|
+
.dsh-np-mini-spinner{flex:none;width:12px;height:12px;border:1.8px 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}
|
|
570
659
|
.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
660
|
.dsh-np-seg[data-archived=true] .dsh-np-medias{padding:0 10px 8px}
|
|
572
661
|
.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}
|
|
@@ -598,6 +687,9 @@ window.__ModuleLoader__.load({
|
|
|
598
687
|
.dsh-np-restore{position:fixed;right:16px;bottom:16px;z-index:30;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;padding:0;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#4b5563);box-shadow:var(--dsw-elevation-lv2,0 8px 24px rgb(15 23 42/.16));cursor:pointer;opacity:0;visibility:hidden;pointer-events:none;transform:translateY(6px) scale(.9);transition:opacity .18s ease,transform .18s ease,visibility 0s linear .18s}
|
|
599
688
|
.dsh-np-restore[data-visible=true]{opacity:.9;visibility:visible;pointer-events:auto;transform:none;transition:opacity .18s ease,transform .18s ease,visibility 0s linear 0s}
|
|
600
689
|
.dsh-np-restore[data-visible=true]:hover{opacity:1;transform:translateY(-1px)}
|
|
690
|
+
.dsh-np-restore-badge{position:absolute;top:-5px;right:-5px;min-width:16px;height:16px;padding:0 4px;box-sizing:border-box;border-radius:9px;background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff;font-size:9.5px;font-weight:700;line-height:16px;text-align:center;font-variant-numeric:tabular-nums;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#fff))}
|
|
691
|
+
[data-row-key^="workspace:"][data-np-unsent]{position:relative}
|
|
692
|
+
[data-row-key^="workspace:"][data-np-unsent]::after{content:attr(data-np-unsent);position:absolute;top:-4px;left:1px;z-index:2;min-width:15px;height:15px;padding:0 4px;box-sizing:border-box;border-radius:8px;background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff;font-size:9.5px;font-weight:700;line-height:15px;text-align:center;font-variant-numeric:tabular-nums;pointer-events:none;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#fff))}
|
|
601
693
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
602
694
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
603
695
|
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
@@ -606,7 +698,7 @@ body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 4
|
|
|
606
698
|
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))}
|
|
607
699
|
body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
|
|
608
700
|
body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
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}}`;
|
|
701
|
+
@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,.dsh-np-mini-spinner{animation:none}}`;
|
|
610
702
|
|
|
611
703
|
// ------------------------------------------------------------------
|
|
612
704
|
// Helpers
|
|
@@ -626,14 +718,38 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
626
718
|
return `seg-${Date.now().toString(36)}-${segmentSequence.toString(36)}`;
|
|
627
719
|
}
|
|
628
720
|
|
|
629
|
-
function
|
|
630
|
-
|
|
721
|
+
function nowIso() {
|
|
722
|
+
try {
|
|
723
|
+
return new Date().toISOString();
|
|
724
|
+
} catch {
|
|
725
|
+
return '';
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function newSegment(text, options = {}) {
|
|
730
|
+
return {
|
|
731
|
+
id: newSegmentId(),
|
|
732
|
+
text,
|
|
733
|
+
title: typeof options.title === 'string' ? options.title.slice(0, MAX_TITLE_CHARS) : '',
|
|
734
|
+
createdAt: typeof options.createdAt === 'string' && options.createdAt !== ''
|
|
735
|
+
? options.createdAt
|
|
736
|
+
: nowIso(),
|
|
737
|
+
images: [],
|
|
738
|
+
archived: false,
|
|
739
|
+
archivedAt: null,
|
|
740
|
+
};
|
|
631
741
|
}
|
|
632
742
|
|
|
633
743
|
function serializeSegments(segments) {
|
|
634
744
|
return segments.map((segment) => ({
|
|
635
745
|
id: segment.id,
|
|
636
746
|
text: segment.text,
|
|
747
|
+
...(typeof segment.title === 'string' && segment.title !== ''
|
|
748
|
+
? { title: segment.title }
|
|
749
|
+
: {}),
|
|
750
|
+
...(typeof segment.createdAt === 'string' && segment.createdAt !== ''
|
|
751
|
+
? { createdAt: segment.createdAt }
|
|
752
|
+
: {}),
|
|
637
753
|
...(Array.isArray(segment.images) && segment.images.length > 0
|
|
638
754
|
? { images: segment.images.map((image) => ({ path: image.path, name: image.name })) }
|
|
639
755
|
: {}),
|
|
@@ -655,6 +771,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
655
771
|
out.push({
|
|
656
772
|
id,
|
|
657
773
|
text: item.text,
|
|
774
|
+
title: typeof item.title === 'string' ? item.title.slice(0, MAX_TITLE_CHARS) : '',
|
|
775
|
+
createdAt: typeof item.createdAt === 'string' && item.createdAt !== '' ? item.createdAt : null,
|
|
658
776
|
images: normalizeImages(item.images),
|
|
659
777
|
archived,
|
|
660
778
|
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
@@ -738,6 +856,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
738
856
|
.join('\n\n');
|
|
739
857
|
}
|
|
740
858
|
|
|
859
|
+
/** Whether a segment still counts as unsent work (active + has content). */
|
|
860
|
+
function isUnsent(segment) {
|
|
861
|
+
if (segment === null || segment === undefined || segment.archived === true) return false;
|
|
862
|
+
if (typeof segment.text === 'string' && segment.text.trim() !== '') return true;
|
|
863
|
+
return Array.isArray(segment.images) && segment.images.length > 0;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function countUnsent(segments) {
|
|
867
|
+
let count = 0;
|
|
868
|
+
for (const segment of segments) {
|
|
869
|
+
if (isUnsent(segment)) count += 1;
|
|
870
|
+
}
|
|
871
|
+
return count;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** Whether a whole server payload predates per-segment creation times. */
|
|
875
|
+
function isLegacyPayload(data) {
|
|
876
|
+
const segments = Array.isArray(data?.segments) ? data.segments : [];
|
|
877
|
+
return segments.length > 0 && segments.every((segment) =>
|
|
878
|
+
segment !== null && typeof segment === 'object'
|
|
879
|
+
&& (typeof segment.createdAt !== 'string' || segment.createdAt === ''));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Adopt server content without moving cards the user is looking at.
|
|
884
|
+
* Known ids keep the local order; cards created elsewhere land on top.
|
|
885
|
+
*/
|
|
886
|
+
function mergeServerOrder(current, incoming) {
|
|
887
|
+
const byId = new Map();
|
|
888
|
+
for (const segment of incoming) byId.set(segment.id, segment);
|
|
889
|
+
const kept = [];
|
|
890
|
+
for (const segment of current) {
|
|
891
|
+
const next = byId.get(segment.id);
|
|
892
|
+
if (next !== undefined) kept.push(next);
|
|
893
|
+
}
|
|
894
|
+
const known = new Set(kept.map((segment) => segment.id));
|
|
895
|
+
const added = incoming.filter((segment) => !known.has(segment.id));
|
|
896
|
+
return [...added, ...kept];
|
|
897
|
+
}
|
|
898
|
+
|
|
741
899
|
function isAbsolutePath(value) {
|
|
742
900
|
const text = String(value ?? '');
|
|
743
901
|
return text.startsWith('/') || /^[A-Za-z]:\//u.test(text) || text.startsWith('//');
|
|
@@ -951,6 +1109,32 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
951
1109
|
return readResponse(response);
|
|
952
1110
|
}
|
|
953
1111
|
|
|
1112
|
+
async function optimizeNoteText(root, sessionId, text, title) {
|
|
1113
|
+
const response = await fetch(`${API}/optimize`, {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1116
|
+
body: JSON.stringify({ root, sessionId, text, title }),
|
|
1117
|
+
});
|
|
1118
|
+
return readResponse(response);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function removeStoredImage(root, sessionId, pathValue) {
|
|
1122
|
+
const response = await fetch(`${API}/image`, {
|
|
1123
|
+
method: 'DELETE',
|
|
1124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1125
|
+
body: JSON.stringify({ root, sessionId, path: pathValue }),
|
|
1126
|
+
});
|
|
1127
|
+
return readResponse(response);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function fetchUnsentCounts(sessionId) {
|
|
1131
|
+
const response = await fetch(`${API}/unsent?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
1132
|
+
method: 'GET',
|
|
1133
|
+
cache: 'no-store',
|
|
1134
|
+
});
|
|
1135
|
+
return readResponse(response);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
954
1138
|
async function copyText(value) {
|
|
955
1139
|
try {
|
|
956
1140
|
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
@@ -1022,6 +1206,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1022
1206
|
const [resizing, setResizing] = useState(false);
|
|
1023
1207
|
const [uploading, setUploading] = useState({});
|
|
1024
1208
|
const [dragSegment, setDragSegment] = useState('');
|
|
1209
|
+
const [reorderDrag, setReorderDrag] = useState('');
|
|
1210
|
+
const [reorderOver, setReorderOver] = useState(null);
|
|
1211
|
+
const [optimizing, setOptimizing] = useState(() => new Set());
|
|
1212
|
+
const [optimizeAll, setOptimizeAll] = useState(null);
|
|
1213
|
+
const [wsCounts, setWsCounts] = useState({});
|
|
1214
|
+
const [currentWsId, setCurrentWsId] = useState('');
|
|
1025
1215
|
|
|
1026
1216
|
const bindingRef = useRef(null);
|
|
1027
1217
|
const rootSessionRef = useRef('');
|
|
@@ -1046,6 +1236,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1046
1236
|
const listRef = useRef(null);
|
|
1047
1237
|
const pendingFocusRef = useRef(null);
|
|
1048
1238
|
const undoRef = useRef(null);
|
|
1239
|
+
const optimizingRef = useRef(new Set());
|
|
1240
|
+
const optimizeAllRef = useRef(false);
|
|
1241
|
+
const reorderDragRef = useRef('');
|
|
1242
|
+
const reorderOverRef = useRef(null);
|
|
1243
|
+
const imageDeleteTimersRef = useRef(new Map());
|
|
1049
1244
|
|
|
1050
1245
|
const setSelection = (next) => {
|
|
1051
1246
|
selectedRef.current = next;
|
|
@@ -1228,6 +1423,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1228
1423
|
.then((data) => {
|
|
1229
1424
|
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
1230
1425
|
let value = normalizeServerSegments(data);
|
|
1426
|
+
// Notes written before 0.4 appended new cards at the bottom;
|
|
1427
|
+
// reverse that legacy order once so the newest card leads.
|
|
1428
|
+
if (isLegacyPayload(data)) value = value.slice().reverse();
|
|
1231
1429
|
if (value.length === 0) value = [newSegment('')];
|
|
1232
1430
|
segmentsRef.current = value;
|
|
1233
1431
|
setSegments(value);
|
|
@@ -1252,6 +1450,91 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1252
1450
|
};
|
|
1253
1451
|
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1254
1452
|
|
|
1453
|
+
// Sidebar workspace badges --------------------------------------------
|
|
1454
|
+
useEffect(() => {
|
|
1455
|
+
if (binding === null) {
|
|
1456
|
+
setCurrentWsId('');
|
|
1457
|
+
setWsCounts({});
|
|
1458
|
+
return undefined;
|
|
1459
|
+
}
|
|
1460
|
+
setCurrentWsId('');
|
|
1461
|
+
let disposed = false;
|
|
1462
|
+
const refresh = () => {
|
|
1463
|
+
const sessionId = binding.id;
|
|
1464
|
+
if (typeof sessionId !== 'string' || sessionId === '') return;
|
|
1465
|
+
fetchUnsentCounts(sessionId)
|
|
1466
|
+
.then((data) => {
|
|
1467
|
+
if (disposed || bindingRef.current?.id !== sessionId) return;
|
|
1468
|
+
const next = {};
|
|
1469
|
+
let matched = '';
|
|
1470
|
+
const target = normalizedCwd(binding.cwd);
|
|
1471
|
+
for (const workspace of Array.isArray(data?.workspaces) ? data.workspaces : []) {
|
|
1472
|
+
const id = typeof workspace?.id === 'string' ? workspace.id : '';
|
|
1473
|
+
if (id === '') continue;
|
|
1474
|
+
next[id] = Number.isFinite(workspace.count) ? Math.max(0, Math.floor(workspace.count)) : 0;
|
|
1475
|
+
if (matched === '' && normalizedCwd(workspace.path) === target) matched = id;
|
|
1476
|
+
}
|
|
1477
|
+
setWsCounts(next);
|
|
1478
|
+
if (matched !== '') setCurrentWsId(matched);
|
|
1479
|
+
})
|
|
1480
|
+
.catch(() => {});
|
|
1481
|
+
};
|
|
1482
|
+
refresh();
|
|
1483
|
+
const timer = setInterval(refresh, SIDEBAR_REFRESH_MS);
|
|
1484
|
+
const onFocus = () => refresh();
|
|
1485
|
+
const onVisibility = () => {
|
|
1486
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
1487
|
+
};
|
|
1488
|
+
window.addEventListener('focus', onFocus);
|
|
1489
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisibility);
|
|
1490
|
+
return () => {
|
|
1491
|
+
disposed = true;
|
|
1492
|
+
clearInterval(timer);
|
|
1493
|
+
window.removeEventListener('focus', onFocus);
|
|
1494
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisibility);
|
|
1495
|
+
};
|
|
1496
|
+
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1497
|
+
|
|
1498
|
+
const localUnsentCount = countUnsent(segments);
|
|
1499
|
+
|
|
1500
|
+
useEffect(() => {
|
|
1501
|
+
if (currentWsId === '') return;
|
|
1502
|
+
setWsCounts((previous) =>
|
|
1503
|
+
(previous[currentWsId] === localUnsentCount
|
|
1504
|
+
? previous
|
|
1505
|
+
: { ...previous, [currentWsId]: localUnsentCount }));
|
|
1506
|
+
}, [currentWsId, localUnsentCount]);
|
|
1507
|
+
|
|
1508
|
+
useEffect(() => {
|
|
1509
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return undefined;
|
|
1510
|
+
const apply = () => {
|
|
1511
|
+
let rows;
|
|
1512
|
+
try {
|
|
1513
|
+
rows = document.querySelectorAll('[data-row-key^="workspace:"]');
|
|
1514
|
+
} catch {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
for (const row of rows) {
|
|
1518
|
+
const key = typeof row.getAttribute === 'function' ? String(row.getAttribute('data-row-key') ?? '') : '';
|
|
1519
|
+
const id = key.startsWith('workspace:') ? key.slice('workspace:'.length) : '';
|
|
1520
|
+
const count = id === '' ? 0 : (wsCounts[id] ?? 0);
|
|
1521
|
+
if (count > 0) row.setAttribute('data-np-unsent', count > 99 ? '99+' : String(count));
|
|
1522
|
+
else row.removeAttribute('data-np-unsent');
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
apply();
|
|
1526
|
+
const Observer = typeof globalThis !== 'undefined' ? globalThis.MutationObserver : undefined;
|
|
1527
|
+
if (typeof Observer !== 'function' || typeof document.body === 'undefined' || document.body === null) {
|
|
1528
|
+
return () => apply();
|
|
1529
|
+
}
|
|
1530
|
+
const observer = new Observer(apply);
|
|
1531
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1532
|
+
return () => {
|
|
1533
|
+
observer.disconnect();
|
|
1534
|
+
apply();
|
|
1535
|
+
};
|
|
1536
|
+
}, [wsCounts]);
|
|
1537
|
+
|
|
1255
1538
|
// Focus a just-created or restored card ------------------------------
|
|
1256
1539
|
useEffect(() => {
|
|
1257
1540
|
const pending = pendingFocusRef.current;
|
|
@@ -1297,6 +1580,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1297
1580
|
window.removeEventListener('keydown', onKeyDown, true);
|
|
1298
1581
|
clearTimer();
|
|
1299
1582
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
1583
|
+
for (const timer of imageDeleteTimersRef.current.values()) clearTimeout(timer);
|
|
1584
|
+
imageDeleteTimersRef.current.clear();
|
|
1300
1585
|
};
|
|
1301
1586
|
}, []);
|
|
1302
1587
|
|
|
@@ -1335,11 +1620,13 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1335
1620
|
.then((data) => {
|
|
1336
1621
|
if (rootRef.current !== root || dirtyRef.current) return;
|
|
1337
1622
|
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) return;
|
|
1338
|
-
let
|
|
1339
|
-
if (
|
|
1623
|
+
let incoming = normalizeServerSegments(data);
|
|
1624
|
+
if (isLegacyPayload(data)) incoming = incoming.slice().reverse();
|
|
1625
|
+
if (incoming.length === 0) incoming = [newSegment('')];
|
|
1626
|
+
const value = mergeServerOrder(segmentsRef.current, incoming);
|
|
1340
1627
|
segmentsRef.current = value;
|
|
1341
1628
|
setSegments(value);
|
|
1342
|
-
|
|
1629
|
+
pruneSelectionFor(value);
|
|
1343
1630
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1344
1631
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1345
1632
|
setStatus('saved');
|
|
@@ -1372,6 +1659,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1372
1659
|
scheduleSave();
|
|
1373
1660
|
};
|
|
1374
1661
|
|
|
1662
|
+
const updateSegmentTitle = (id, value) => {
|
|
1663
|
+
const title = String(value ?? '').slice(0, MAX_TITLE_CHARS);
|
|
1664
|
+
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1665
|
+
if (target === undefined || target.title === title) return;
|
|
1666
|
+
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, title } : segment));
|
|
1667
|
+
segmentsRef.current = next;
|
|
1668
|
+
setSegments(next);
|
|
1669
|
+
dirtyRef.current = true;
|
|
1670
|
+
setStatus('dirty');
|
|
1671
|
+
setError('');
|
|
1672
|
+
scheduleSave();
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1375
1675
|
// Image attachments ---------------------------------------------------
|
|
1376
1676
|
const imageErrorText = (reason) => {
|
|
1377
1677
|
const code = reason?.code;
|
|
@@ -1446,13 +1746,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1446
1746
|
}
|
|
1447
1747
|
};
|
|
1448
1748
|
|
|
1749
|
+
const clearPendingImageDeletes = () => {
|
|
1750
|
+
for (const timer of imageDeleteTimersRef.current.values()) {
|
|
1751
|
+
clearTimeout(timer);
|
|
1752
|
+
}
|
|
1753
|
+
imageDeleteTimersRef.current.clear();
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
const deleteImageFile = (pathValue) => {
|
|
1757
|
+
const root = rootRef.current;
|
|
1758
|
+
const sessionId = rootSessionRef.current;
|
|
1759
|
+
if (root === '' || sessionId === '' || typeof pathValue !== 'string' || pathValue === '') return;
|
|
1760
|
+
removeStoredImage(root, sessionId, pathValue).catch(() => {});
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
const scheduleImageCleanup = (images) => {
|
|
1764
|
+
const list = Array.isArray(images) ? images : [];
|
|
1765
|
+
for (const image of list) {
|
|
1766
|
+
const pathValue = typeof image?.path === 'string' ? image.path : '';
|
|
1767
|
+
if (pathValue === '' || imageDeleteTimersRef.current.has(pathValue)) continue;
|
|
1768
|
+
const timer = setTimeout(() => {
|
|
1769
|
+
imageDeleteTimersRef.current.delete(pathValue);
|
|
1770
|
+
if (!segmentsRef.current.some((segment) =>
|
|
1771
|
+
(Array.isArray(segment.images) ? segment.images : []).some((item) => item.path === pathValue))) {
|
|
1772
|
+
deleteImageFile(pathValue);
|
|
1773
|
+
}
|
|
1774
|
+
}, IMAGE_DELETE_DELAY);
|
|
1775
|
+
imageDeleteTimersRef.current.set(pathValue, timer);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1449
1779
|
const removeImage = (id, pathValue) => {
|
|
1450
1780
|
const next = segmentsRef.current.map((segment) => {
|
|
1451
1781
|
if (segment.id !== id) return segment;
|
|
1452
1782
|
const images = Array.isArray(segment.images) ? segment.images.filter((image) => image.path !== pathValue) : [];
|
|
1453
1783
|
return { ...segment, images };
|
|
1454
1784
|
});
|
|
1785
|
+
const stillReferenced = next.some((segment) =>
|
|
1786
|
+
(Array.isArray(segment.images) ? segment.images : []).some((image) => image.path === pathValue));
|
|
1455
1787
|
commitSegments(next);
|
|
1788
|
+
if (!stillReferenced) deleteImageFile(pathValue);
|
|
1456
1789
|
};
|
|
1457
1790
|
|
|
1458
1791
|
const triggerFilePicker = (id) => {
|
|
@@ -1525,7 +1858,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1525
1858
|
return;
|
|
1526
1859
|
}
|
|
1527
1860
|
const segment = newSegment('');
|
|
1528
|
-
commitSegments([...segmentsRef.current
|
|
1861
|
+
commitSegments([segment, ...segmentsRef.current], { focus: { id: segment.id, caret: 0 } });
|
|
1529
1862
|
};
|
|
1530
1863
|
|
|
1531
1864
|
const splitSegment = (id, element) => {
|
|
@@ -1535,15 +1868,21 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1535
1868
|
const head = current.text.slice(0, start);
|
|
1536
1869
|
const tail = current.text.slice(start);
|
|
1537
1870
|
const nextSegment = newSegment(tail);
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1871
|
+
const updated = segmentsRef.current.map((segment) =>
|
|
1872
|
+
(segment.id === id ? { ...segment, text: head } : segment));
|
|
1873
|
+
// Enter at the end creates a fresh card, which belongs at the top.
|
|
1874
|
+
// Splitting in the middle keeps the tail next to its source card so
|
|
1875
|
+
// the caret does not jump away from what the user is editing.
|
|
1876
|
+
const next = tail.trim() === ''
|
|
1877
|
+
? [nextSegment, ...updated]
|
|
1878
|
+
: (() => {
|
|
1879
|
+
const out = [];
|
|
1880
|
+
for (const segment of updated) {
|
|
1881
|
+
out.push(segment);
|
|
1882
|
+
if (segment.id === id) out.push(nextSegment);
|
|
1883
|
+
}
|
|
1884
|
+
return out;
|
|
1885
|
+
})();
|
|
1547
1886
|
commitSegments(next, { focus: { id: nextSegment.id, caret: 0 } });
|
|
1548
1887
|
};
|
|
1549
1888
|
|
|
@@ -1595,6 +1934,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1595
1934
|
const record = undoRef.current;
|
|
1596
1935
|
if (record === null || record === undefined) return;
|
|
1597
1936
|
undoRef.current = null;
|
|
1937
|
+
clearPendingImageDeletes();
|
|
1598
1938
|
commitSegments(record.snapshot);
|
|
1599
1939
|
setToast(null);
|
|
1600
1940
|
};
|
|
@@ -1605,6 +1945,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1605
1945
|
undoRef.current = { snapshot: segmentsRef.current };
|
|
1606
1946
|
const next = segmentsRef.current.filter((segment) => segment.id !== id);
|
|
1607
1947
|
commitSegments(next);
|
|
1948
|
+
scheduleImageCleanup(target.images);
|
|
1608
1949
|
showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
|
|
1609
1950
|
};
|
|
1610
1951
|
|
|
@@ -1614,9 +1955,226 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1614
1955
|
undoRef.current = { snapshot: segmentsRef.current };
|
|
1615
1956
|
const next = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1616
1957
|
commitSegments(next);
|
|
1958
|
+
scheduleImageCleanup(archived.flatMap((segment) => (Array.isArray(segment.images) ? segment.images : [])));
|
|
1617
1959
|
showToast(t('clearedArchive', { n: archived.length }), { label: t('undo'), onClick: undoLast });
|
|
1618
1960
|
};
|
|
1619
1961
|
|
|
1962
|
+
// Merge / reorder -----------------------------------------------------
|
|
1963
|
+
const mergeSelected = () => {
|
|
1964
|
+
const ids = new Set(
|
|
1965
|
+
segmentsRef.current
|
|
1966
|
+
.filter((segment) => segment.archived !== true && selectedRef.current.has(segment.id))
|
|
1967
|
+
.map((segment) => segment.id),
|
|
1968
|
+
);
|
|
1969
|
+
if (ids.size < 2) {
|
|
1970
|
+
showToast(t('mergeHint'));
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const chosen = segmentsRef.current.filter((segment) => ids.has(segment.id));
|
|
1974
|
+
const firstIndex = segmentsRef.current.findIndex((segment) => ids.has(segment.id));
|
|
1975
|
+
let insertAt = 0;
|
|
1976
|
+
for (let index = 0; index < firstIndex; index += 1) {
|
|
1977
|
+
if (!ids.has(segmentsRef.current[index].id)) insertAt += 1;
|
|
1978
|
+
}
|
|
1979
|
+
const seenPaths = new Set();
|
|
1980
|
+
const images = [];
|
|
1981
|
+
for (const segment of chosen) {
|
|
1982
|
+
for (const image of Array.isArray(segment.images) ? segment.images : []) {
|
|
1983
|
+
if (seenPaths.has(image.path) || images.length >= MAX_IMAGES) continue;
|
|
1984
|
+
seenPaths.add(image.path);
|
|
1985
|
+
images.push(image);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
const merged = {
|
|
1989
|
+
id: newSegmentId(),
|
|
1990
|
+
text: chosen.map((segment) => segment.text).filter((text) => text.trim() !== '').join('\n\n'),
|
|
1991
|
+
title: chosen
|
|
1992
|
+
.map((segment) => (typeof segment.title === 'string' ? segment.title.trim() : ''))
|
|
1993
|
+
.filter((title) => title !== '')
|
|
1994
|
+
.join(' · ')
|
|
1995
|
+
.slice(0, MAX_TITLE_CHARS),
|
|
1996
|
+
createdAt: nowIso(),
|
|
1997
|
+
images,
|
|
1998
|
+
archived: false,
|
|
1999
|
+
archivedAt: null,
|
|
2000
|
+
};
|
|
2001
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
2002
|
+
const next = segmentsRef.current.filter((segment) => !ids.has(segment.id));
|
|
2003
|
+
next.splice(Math.min(insertAt, next.length), 0, merged);
|
|
2004
|
+
setSelection(new Set());
|
|
2005
|
+
commitSegments(next, { focus: { id: merged.id, caret: merged.text.length } });
|
|
2006
|
+
showToast(t('mergedToast', { n: chosen.length }), { label: t('undo'), onClick: undoLast });
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
const reorderSegments = (sourceId, targetId, after) => {
|
|
2010
|
+
if (sourceId === '' || sourceId === targetId) return;
|
|
2011
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
2012
|
+
const from = active.findIndex((segment) => segment.id === sourceId);
|
|
2013
|
+
const to = active.findIndex((segment) => segment.id === targetId);
|
|
2014
|
+
if (from < 0 || to < 0) return;
|
|
2015
|
+
const moved = active.splice(from, 1)[0];
|
|
2016
|
+
let insertAt = to + (after ? 1 : 0);
|
|
2017
|
+
if (from < insertAt) insertAt -= 1;
|
|
2018
|
+
insertAt = Math.max(0, Math.min(insertAt, active.length));
|
|
2019
|
+
active.splice(insertAt, 0, moved);
|
|
2020
|
+
let cursor = 0;
|
|
2021
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2022
|
+
(segment.archived === true ? segment : active[cursor++]));
|
|
2023
|
+
if (next.every((segment, index) => segment.id === segmentsRef.current[index].id)) return;
|
|
2024
|
+
commitSegments(next);
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
const setReorderTarget = (target) => {
|
|
2028
|
+
reorderOverRef.current = target;
|
|
2029
|
+
setReorderOver(target);
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
const onSegmentReorderStart = (event, id) => {
|
|
2033
|
+
reorderDragRef.current = id;
|
|
2034
|
+
setReorderDrag(id);
|
|
2035
|
+
try {
|
|
2036
|
+
event.dataTransfer?.setData('application/x-dsh-notepad-segment', id);
|
|
2037
|
+
if (event.dataTransfer !== undefined && event.dataTransfer !== null) event.dataTransfer.effectAllowed = 'move';
|
|
2038
|
+
} catch {
|
|
2039
|
+
/* Some browsers expose a read-only transfer. */
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
const onSegmentReorderOver = (event, id) => {
|
|
2044
|
+
const source = reorderDragRef.current;
|
|
2045
|
+
if (source === '' || source === id) return;
|
|
2046
|
+
const types = event?.dataTransfer?.types;
|
|
2047
|
+
if (types !== undefined && types !== null && !Array.from(types).includes('application/x-dsh-notepad-segment')) return;
|
|
2048
|
+
event.preventDefault?.();
|
|
2049
|
+
event.stopPropagation?.();
|
|
2050
|
+
const rect = typeof event.currentTarget?.getBoundingClientRect === 'function'
|
|
2051
|
+
? event.currentTarget.getBoundingClientRect()
|
|
2052
|
+
: null;
|
|
2053
|
+
const after = rect !== null && Number(event.clientY) > rect.top + rect.height / 2;
|
|
2054
|
+
if (reorderOverRef.current?.id !== id || reorderOverRef.current?.after !== after) {
|
|
2055
|
+
setReorderTarget({ id, after });
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
|
|
2059
|
+
const onSegmentReorderDrop = (event, id) => {
|
|
2060
|
+
const source = reorderDragRef.current
|
|
2061
|
+
|| (() => {
|
|
2062
|
+
try {
|
|
2063
|
+
return event?.dataTransfer?.getData('application/x-dsh-notepad-segment') ?? '';
|
|
2064
|
+
} catch {
|
|
2065
|
+
return '';
|
|
2066
|
+
}
|
|
2067
|
+
})();
|
|
2068
|
+
const target = reorderOverRef.current?.id === id ? reorderOverRef.current : null;
|
|
2069
|
+
reorderDragRef.current = '';
|
|
2070
|
+
setReorderDrag('');
|
|
2071
|
+
setReorderTarget(null);
|
|
2072
|
+
if (source === '') return;
|
|
2073
|
+
event.preventDefault?.();
|
|
2074
|
+
event.stopPropagation?.();
|
|
2075
|
+
reorderSegments(source, id, target?.after === true);
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
// AI optimization -----------------------------------------------------
|
|
2079
|
+
const markOptimizing = (id, active) => {
|
|
2080
|
+
const next = new Set(optimizingRef.current);
|
|
2081
|
+
if (active) next.add(id);
|
|
2082
|
+
else next.delete(id);
|
|
2083
|
+
optimizingRef.current = next;
|
|
2084
|
+
setOptimizing(next);
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
const requestOptimization = async (segment) => {
|
|
2088
|
+
const root = rootRef.current;
|
|
2089
|
+
const sessionId = rootSessionRef.current;
|
|
2090
|
+
if (root === '' || sessionId === '') throw new Error(t('optimizeUnavailable'));
|
|
2091
|
+
if (segment.text.length > MAX_OPTIMIZE_CHARS) throw new Error(t('optimizeTooLong'));
|
|
2092
|
+
const data = await optimizeNoteText(root, sessionId, segment.text, segment.title ?? '');
|
|
2093
|
+
const value = typeof data.text === 'string' ? data.text.trim() : '';
|
|
2094
|
+
if (value === '') throw new Error(t('optimizeFailed'));
|
|
2095
|
+
return value;
|
|
2096
|
+
};
|
|
2097
|
+
|
|
2098
|
+
const optimizeSegment = async (id) => {
|
|
2099
|
+
if (optimizingRef.current.has(id)) return;
|
|
2100
|
+
const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
|
|
2101
|
+
if (target === undefined || target.text.trim() === '') {
|
|
2102
|
+
showToast(t('optimizeEmpty'));
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const snapshot = segmentsRef.current;
|
|
2106
|
+
const original = target.text;
|
|
2107
|
+
markOptimizing(id, true);
|
|
2108
|
+
try {
|
|
2109
|
+
const value = await requestOptimization(target);
|
|
2110
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
2111
|
+
if (current === undefined || current.text !== original || value === original) return;
|
|
2112
|
+
undoRef.current = { snapshot };
|
|
2113
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2114
|
+
(segment.id === id ? { ...segment, text: value } : segment));
|
|
2115
|
+
commitSegments(next);
|
|
2116
|
+
showToast(t('optimizedToast'), { label: t('undo'), onClick: undoLast });
|
|
2117
|
+
} catch (reason) {
|
|
2118
|
+
const message = String(reason?.message ?? reason);
|
|
2119
|
+
showToast(message === t('optimizeTooLong') || message === t('optimizeUnavailable')
|
|
2120
|
+
? message
|
|
2121
|
+
: `${t('optimizeFailed')}: ${message}`);
|
|
2122
|
+
} finally {
|
|
2123
|
+
markOptimizing(id, false);
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
const optimizeAllSegments = async () => {
|
|
2128
|
+
if (optimizeAllRef.current) return;
|
|
2129
|
+
const targets = segmentsRef.current.filter((segment) =>
|
|
2130
|
+
segment.archived !== true && segment.text.trim() !== '');
|
|
2131
|
+
if (targets.length === 0) {
|
|
2132
|
+
showToast(t('optimizeEmpty'));
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
optimizeAllRef.current = true;
|
|
2136
|
+
const snapshot = segmentsRef.current;
|
|
2137
|
+
let done = 0;
|
|
2138
|
+
let changed = 0;
|
|
2139
|
+
let failed = 0;
|
|
2140
|
+
setOptimizeAll({ done, total: targets.length });
|
|
2141
|
+
try {
|
|
2142
|
+
for (const target of targets) {
|
|
2143
|
+
const current = segmentsRef.current.find((segment) =>
|
|
2144
|
+
segment.id === target.id && segment.archived !== true);
|
|
2145
|
+
if (current !== undefined && current.text === target.text) {
|
|
2146
|
+
markOptimizing(target.id, true);
|
|
2147
|
+
try {
|
|
2148
|
+
const value = await requestOptimization(current);
|
|
2149
|
+
if (value !== current.text) {
|
|
2150
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2151
|
+
(segment.id === target.id && segment.text === current.text
|
|
2152
|
+
? { ...segment, text: value }
|
|
2153
|
+
: segment));
|
|
2154
|
+
commitSegments(next);
|
|
2155
|
+
changed += 1;
|
|
2156
|
+
}
|
|
2157
|
+
} catch {
|
|
2158
|
+
failed += 1;
|
|
2159
|
+
} finally {
|
|
2160
|
+
markOptimizing(target.id, false);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
done += 1;
|
|
2164
|
+
setOptimizeAll({ done, total: targets.length });
|
|
2165
|
+
}
|
|
2166
|
+
if (changed > 0) {
|
|
2167
|
+
undoRef.current = { snapshot };
|
|
2168
|
+
showToast(t('optimizedCount', { n: changed }), { label: t('undo'), onClick: undoLast });
|
|
2169
|
+
} else {
|
|
2170
|
+
showToast(failed > 0 ? t('optimizeFailed') : t('optimizedToast'));
|
|
2171
|
+
}
|
|
2172
|
+
} finally {
|
|
2173
|
+
optimizeAllRef.current = false;
|
|
2174
|
+
setOptimizeAll(null);
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
|
|
1620
2178
|
// Selection -----------------------------------------------------------
|
|
1621
2179
|
const toggleSelected = (id) => {
|
|
1622
2180
|
const next = new Set(selectedRef.current);
|
|
@@ -1903,6 +2461,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1903
2461
|
const selectedActive = activeSegments.filter((segment) => selected.has(segment.id));
|
|
1904
2462
|
const selectedCount = selectedActive.length;
|
|
1905
2463
|
const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
|
|
2464
|
+
const unsentCount = countUnsent(segments);
|
|
1906
2465
|
const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
1907
2466
|
const displayWidth = ui.width;
|
|
1908
2467
|
const displayHeight = ui.minimized ? HEAD_H : ui.height;
|
|
@@ -1921,9 +2480,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1921
2480
|
statusText = clock === '' ? t('saved') : `${t('saved')} · ${clock}`;
|
|
1922
2481
|
} else statusText = t('empty');
|
|
1923
2482
|
|
|
1924
|
-
const archiveButtonTitle = ui.showArchive
|
|
1925
|
-
? t('hideArchive', { n: archivedSegments.length })
|
|
1926
|
-
: t('showArchive', { n: archivedSegments.length });
|
|
2483
|
+
const archiveButtonTitle = ui.showArchive ? t('hideArchive') : t('showArchive');
|
|
1927
2484
|
|
|
1928
2485
|
const header = h(
|
|
1929
2486
|
'div',
|
|
@@ -1956,9 +2513,6 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1956
2513
|
onClick: toggleArchive,
|
|
1957
2514
|
},
|
|
1958
2515
|
h(ArchiveIcon, { size: 15 }),
|
|
1959
|
-
archivedSegments.length > 0
|
|
1960
|
-
? h('span', { className: 'dsh-np-badge' }, String(archivedSegments.length))
|
|
1961
|
-
: null,
|
|
1962
2516
|
),
|
|
1963
2517
|
h(
|
|
1964
2518
|
'button',
|
|
@@ -2051,6 +2605,35 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2051
2605
|
},
|
|
2052
2606
|
h(SendIcon, { size: 14 }),
|
|
2053
2607
|
),
|
|
2608
|
+
h(
|
|
2609
|
+
'button',
|
|
2610
|
+
{
|
|
2611
|
+
type: 'button',
|
|
2612
|
+
className: 'dsh-np-btn',
|
|
2613
|
+
title: selectedCount > 1 ? t('mergeSelected', { n: selectedCount }) : t('mergeHint'),
|
|
2614
|
+
'aria-label': t('merge'),
|
|
2615
|
+
disabled: selectedCount < 2,
|
|
2616
|
+
onClick: mergeSelected,
|
|
2617
|
+
},
|
|
2618
|
+
h(MergeIcon, { size: 14 }),
|
|
2619
|
+
),
|
|
2620
|
+
h(
|
|
2621
|
+
'button',
|
|
2622
|
+
{
|
|
2623
|
+
type: 'button',
|
|
2624
|
+
className: 'dsh-np-btn',
|
|
2625
|
+
'data-active': optimizeAll !== null ? 'true' : 'false',
|
|
2626
|
+
title: optimizeAll !== null
|
|
2627
|
+
? t('optimizeAllProgress', { done: optimizeAll.done, total: optimizeAll.total })
|
|
2628
|
+
: t('optimizeAll'),
|
|
2629
|
+
'aria-label': t('optimizeAll'),
|
|
2630
|
+
disabled: activeSegments.length === 0 || optimizeAll !== null,
|
|
2631
|
+
onClick: optimizeAllSegments,
|
|
2632
|
+
},
|
|
2633
|
+
optimizeAll !== null
|
|
2634
|
+
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2635
|
+
: h(SparkleIcon, { size: 14 }),
|
|
2636
|
+
),
|
|
2054
2637
|
h(
|
|
2055
2638
|
'button',
|
|
2056
2639
|
{
|
|
@@ -2145,6 +2728,14 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2145
2728
|
'data-seg-id': segment.id,
|
|
2146
2729
|
'data-archived': 'false',
|
|
2147
2730
|
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
2731
|
+
'data-reorder-drag': reorderDrag === segment.id ? 'true' : 'false',
|
|
2732
|
+
'data-drop-before': reorderOver?.id === segment.id && reorderOver?.after === false ? 'true' : 'false',
|
|
2733
|
+
'data-drop-after': reorderOver?.id === segment.id && reorderOver?.after === true ? 'true' : 'false',
|
|
2734
|
+
onDragOver: (event) => onSegmentReorderOver(event, segment.id),
|
|
2735
|
+
onDragLeave: () => {
|
|
2736
|
+
if (reorderOverRef.current?.id === segment.id) setReorderTarget(null);
|
|
2737
|
+
},
|
|
2738
|
+
onDrop: (event) => onSegmentReorderDrop(event, segment.id),
|
|
2148
2739
|
},
|
|
2149
2740
|
h('input', {
|
|
2150
2741
|
key: 'file-input',
|
|
@@ -2176,8 +2767,47 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2176
2767
|
onChange: () => toggleSelected(segment.id),
|
|
2177
2768
|
}),
|
|
2178
2769
|
),
|
|
2179
|
-
h(
|
|
2180
|
-
|
|
2770
|
+
h(
|
|
2771
|
+
'button',
|
|
2772
|
+
{
|
|
2773
|
+
type: 'button',
|
|
2774
|
+
className: 'dsh-np-index',
|
|
2775
|
+
title: t('indexHint'),
|
|
2776
|
+
'aria-label': t('selectSegment'),
|
|
2777
|
+
'aria-pressed': selected.has(segment.id) ? 'true' : 'false',
|
|
2778
|
+
draggable: true,
|
|
2779
|
+
onDragStart: (event) => onSegmentReorderStart(event, segment.id),
|
|
2780
|
+
onDragEnd: () => {
|
|
2781
|
+
reorderDragRef.current = '';
|
|
2782
|
+
setReorderDrag('');
|
|
2783
|
+
setReorderTarget(null);
|
|
2784
|
+
},
|
|
2785
|
+
onClick: () => toggleSelected(segment.id),
|
|
2786
|
+
},
|
|
2787
|
+
String(index + 1).padStart(2, '0'),
|
|
2788
|
+
),
|
|
2789
|
+
h('input', {
|
|
2790
|
+
type: 'text',
|
|
2791
|
+
className: 'dsh-np-title-input',
|
|
2792
|
+
value: segment.title ?? '',
|
|
2793
|
+
placeholder: t('titlePlaceholder'),
|
|
2794
|
+
maxLength: MAX_TITLE_CHARS,
|
|
2795
|
+
'aria-label': t('segmentTitle'),
|
|
2796
|
+
spellCheck: false,
|
|
2797
|
+
onChange: (event) => updateSegmentTitle(segment.id, event.target?.value ?? ''),
|
|
2798
|
+
onKeyDown: (event) => {
|
|
2799
|
+
if (event.key !== 'Enter') return;
|
|
2800
|
+
event.preventDefault();
|
|
2801
|
+
try {
|
|
2802
|
+
event.currentTarget?.blur?.();
|
|
2803
|
+
} catch {
|
|
2804
|
+
/* ignore */
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
onBlur: () => {
|
|
2808
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
2809
|
+
},
|
|
2810
|
+
}),
|
|
2181
2811
|
h(
|
|
2182
2812
|
'button',
|
|
2183
2813
|
{
|
|
@@ -2225,6 +2855,21 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2225
2855
|
},
|
|
2226
2856
|
h(SendIcon, { size: 14 }),
|
|
2227
2857
|
),
|
|
2858
|
+
h(
|
|
2859
|
+
'button',
|
|
2860
|
+
{
|
|
2861
|
+
type: 'button',
|
|
2862
|
+
className: 'dsh-np-segbtn',
|
|
2863
|
+
'data-ai': 'true',
|
|
2864
|
+
title: optimizing.has(segment.id) ? t('optimizing') : t('optimizeSegment'),
|
|
2865
|
+
'aria-label': t('optimizeSegment'),
|
|
2866
|
+
disabled: empty || optimizing.has(segment.id) || optimizeAll !== null,
|
|
2867
|
+
onClick: () => optimizeSegment(segment.id),
|
|
2868
|
+
},
|
|
2869
|
+
optimizing.has(segment.id)
|
|
2870
|
+
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2871
|
+
: h(SparkleIcon, { size: 14 }),
|
|
2872
|
+
),
|
|
2228
2873
|
h(
|
|
2229
2874
|
'button',
|
|
2230
2875
|
{
|
|
@@ -2342,6 +2987,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2342
2987
|
'div',
|
|
2343
2988
|
{ className: 'dsh-np-seg-head' },
|
|
2344
2989
|
h('span', { className: 'dsh-np-archived-chip' }, t('archived')),
|
|
2990
|
+
typeof segment.title === 'string' && segment.title.trim() !== ''
|
|
2991
|
+
? h('span', { className: 'dsh-np-seg-title' }, segment.title)
|
|
2992
|
+
: null,
|
|
2345
2993
|
h('span', { className: 'dsh-np-spacer' }),
|
|
2346
2994
|
h(
|
|
2347
2995
|
'button',
|
|
@@ -2508,6 +3156,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2508
3156
|
onClick: toggleHidden,
|
|
2509
3157
|
},
|
|
2510
3158
|
h(NotepadIcon, { size: 16 }),
|
|
3159
|
+
ui.hidden && unsentCount > 0
|
|
3160
|
+
? h('span', {
|
|
3161
|
+
className: 'dsh-np-restore-badge',
|
|
3162
|
+
title: t('unsent', { n: unsentCount }),
|
|
3163
|
+
}, unsentCount > 99 ? '99+' : String(unsentCount))
|
|
3164
|
+
: null,
|
|
2511
3165
|
);
|
|
2512
3166
|
|
|
2513
3167
|
return [card, restore];
|