jsmdcui 0.5.0 → 0.7.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/CHANGELOG.md +228 -0
- package/README.md +305 -25
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/demos/image-processor.md +361 -0
- package/demos/image-processor.zh-TW.md +361 -0
- package/demos/select.md +63 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +110 -17
- package/runtime/help/help.md +305 -25
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +227 -1
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +60 -0
- package/src/index.js +297 -23
- package/src/plugins/js-bridge.js +346 -4
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +203 -4
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +147 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +147 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +30 -0
- package/tui +4 -2
- package/wui +6 -2
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env jsmdcui
|
|
2
|
+
|
|
3
|
+
# Bun.Image Processor
|
|
4
|
+
|
|
5
|
+
先把本機圖片路徑貼到下方(例如 `/home/me/photo.jpg`)。輸出檔會放在原圖旁邊,檔名為 `original.resized.jpg` 或 `original.resized.png`;原圖不會被覆寫。
|
|
6
|
+
|
|
7
|
+
```text#image-path
|
|
8
|
+
demo.jpg
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- [讀取圖片 metadata](javascript:readMetadata())
|
|
12
|
+
|
|
13
|
+
```text#image-metadata
|
|
14
|
+
尚未讀取 metadata
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 尺寸
|
|
18
|
+
|
|
19
|
+
寬度(必填,正整數):
|
|
20
|
+
|
|
21
|
+
```text#resize-width
|
|
22
|
+
800
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
高度(留白會保持原始長寬比):
|
|
26
|
+
|
|
27
|
+
```text#resize-height
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Select Fit
|
|
32
|
+
|
|
33
|
+
- [ ] fill(精確填滿寬高,可能變形)
|
|
34
|
+
- [x] inside(保持比例,縮放到指定範圍內)
|
|
35
|
+
|
|
36
|
+
## Select Filter
|
|
37
|
+
|
|
38
|
+
- [x] lanczos3(照片通用,預設)
|
|
39
|
+
- [ ] lanczos2(較柔和、較少光暈)
|
|
40
|
+
- [ ] mitchell(平滑漸層)
|
|
41
|
+
- [ ] cubic(較銳利)
|
|
42
|
+
- [ ] mks2013(Magic Kernel Sharp)
|
|
43
|
+
- [ ] mks2021(Magic Kernel Sharp)
|
|
44
|
+
- [ ] bilinear(快速、柔和)
|
|
45
|
+
- [ ] linear(快速、柔和)
|
|
46
|
+
- [ ] box(大倍率整數縮小)
|
|
47
|
+
- [ ] nearest(像素圖、硬邊緣)
|
|
48
|
+
|
|
49
|
+
## Select Without Enlargement
|
|
50
|
+
|
|
51
|
+
- [x] yes(不放大較小的原圖)
|
|
52
|
+
- [ ] no(允許放大)
|
|
53
|
+
|
|
54
|
+
## 方向與翻轉
|
|
55
|
+
|
|
56
|
+
## Select Auto Orient
|
|
57
|
+
|
|
58
|
+
- [x] yes(依 JPEG EXIF 自動校正方向)
|
|
59
|
+
- [ ] no
|
|
60
|
+
|
|
61
|
+
## Select Rotate
|
|
62
|
+
|
|
63
|
+
- [x] 0°
|
|
64
|
+
- [ ] 90°
|
|
65
|
+
- [ ] 180°
|
|
66
|
+
- [ ] 270°
|
|
67
|
+
|
|
68
|
+
## Select Flip
|
|
69
|
+
|
|
70
|
+
- [ ] yes(上下翻轉)
|
|
71
|
+
- [x] no
|
|
72
|
+
|
|
73
|
+
## Select Flop
|
|
74
|
+
|
|
75
|
+
- [ ] yes(左右翻轉)
|
|
76
|
+
- [x] no
|
|
77
|
+
|
|
78
|
+
## 色彩
|
|
79
|
+
|
|
80
|
+
亮度倍率(`1` 不變):
|
|
81
|
+
|
|
82
|
+
```text#brightness
|
|
83
|
+
1
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
飽和度倍率(`0` 灰階、`1` 不變、大於 `1` 增豔):
|
|
87
|
+
|
|
88
|
+
```text#saturation
|
|
89
|
+
1
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Select Output Format
|
|
93
|
+
|
|
94
|
+
- [x] JPEG (.jpg)
|
|
95
|
+
- [ ] PNG (.png)
|
|
96
|
+
|
|
97
|
+
## JPEG 選項
|
|
98
|
+
|
|
99
|
+
品質(`1`–`100`,預設 `80`):
|
|
100
|
+
|
|
101
|
+
```text#jpeg-quality
|
|
102
|
+
80
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Select Progressive JPEG
|
|
106
|
+
|
|
107
|
+
- [ ] yes
|
|
108
|
+
- [x] no
|
|
109
|
+
|
|
110
|
+
## PNG 選項
|
|
111
|
+
|
|
112
|
+
壓縮等級(`0`–`9`,預設 `6`):
|
|
113
|
+
|
|
114
|
+
```text#png-compression
|
|
115
|
+
6
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Select PNG Palette
|
|
119
|
+
|
|
120
|
+
- [ ] yes(索引色 PNG)
|
|
121
|
+
- [x] no(全彩 PNG)
|
|
122
|
+
|
|
123
|
+
調色盤色數(啟用 Palette 時使用,`2`–`256`):
|
|
124
|
+
|
|
125
|
+
```text#png-colors
|
|
126
|
+
256
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Select PNG Dither
|
|
130
|
+
|
|
131
|
+
- [x] yes
|
|
132
|
+
- [ ] no
|
|
133
|
+
|
|
134
|
+
- [Resize and write image](javascript:resizeAndWrite())
|
|
135
|
+
|
|
136
|
+
## 寫入狀態
|
|
137
|
+
|
|
138
|
+
```text#write-status
|
|
139
|
+
尚未執行
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```js front
|
|
143
|
+
function firstWord(value) {
|
|
144
|
+
return String(value ?? '').trim().split(/\s+/)[0].toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function includesChoice(id, choices, fallback) {
|
|
148
|
+
const value = String($('#' + id).val() ?? '').toLowerCase();
|
|
149
|
+
return choices.find(choice => value.includes(choice)) ?? fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function yes(id) {
|
|
153
|
+
return String($('#' + id).val() ?? '').toLowerCase().includes('yes');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function numberText(id) {
|
|
157
|
+
return $('#' + id).val().trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function describeError(error) {
|
|
161
|
+
if (error == null) return '未知錯誤(沒有錯誤內容)';
|
|
162
|
+
if (typeof error === 'string') return error;
|
|
163
|
+
const details = [];
|
|
164
|
+
if (error?.code) details.push(`錯誤代碼:${error.code}`);
|
|
165
|
+
if (error?.name && error.name !== 'Error') details.push(`類型:${error.name}`);
|
|
166
|
+
if (error?.message) details.push(`訊息:${error.message}`);
|
|
167
|
+
if (error?.error && error.error !== error) details.push(`錯誤:${describeError(error.error)}`);
|
|
168
|
+
if (error?.cause && error.cause !== error) details.push(`原因:${describeError(error.cause)}`);
|
|
169
|
+
if (error?.stack && String(error.stack) !== String(error.message ?? ''))
|
|
170
|
+
details.push(`Stack:${error.stack}`);
|
|
171
|
+
if (details.length) return details.join('\n');
|
|
172
|
+
try {
|
|
173
|
+
const json = JSON.stringify(error, null, 2);
|
|
174
|
+
if (json && json !== '{}') return json;
|
|
175
|
+
} catch {}
|
|
176
|
+
return String(error);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function readMetadata() {
|
|
180
|
+
const output = $('#image-metadata');
|
|
181
|
+
output.val('正在讀取 metadata…');
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const inputPath = $('#image-path').val().trim();
|
|
185
|
+
const result = await rpc.readImageMetadata(inputPath);
|
|
186
|
+
if (!result || typeof result !== 'object' || result.ok !== true) {
|
|
187
|
+
const error = result && typeof result === 'object' && 'error' in result
|
|
188
|
+
? result.error
|
|
189
|
+
: result;
|
|
190
|
+
output.val(`讀取 metadata 失敗:\n${describeError(error)}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
output.val(`圖片路徑:${result.inputPath}\n${JSON.stringify(result.metadata, null, 2)}`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
output.val(`讀取 metadata 失敗:\n${describeError(error)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function resizeAndWrite() {
|
|
200
|
+
const status = $('#write-status');
|
|
201
|
+
status.val('處理中…');
|
|
202
|
+
let options;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
options = {
|
|
206
|
+
inputPath: $('#image-path').val().trim(),
|
|
207
|
+
width: numberText('resize-width'),
|
|
208
|
+
height: numberText('resize-height'),
|
|
209
|
+
fit: includesChoice('select-fit', ['fill', 'inside'], 'inside'),
|
|
210
|
+
filter: includesChoice('select-filter', [
|
|
211
|
+
'nearest', 'box', 'bilinear', 'cubic', 'mitchell',
|
|
212
|
+
'lanczos2', 'lanczos3', 'mks2013', 'mks2021', 'linear',
|
|
213
|
+
], 'lanczos3'),
|
|
214
|
+
withoutEnlargement: yes('select-without-enlargement'),
|
|
215
|
+
autoOrient: yes('select-auto-orient'),
|
|
216
|
+
rotate: Number.parseInt(firstWord($('#select-rotate').val()), 10),
|
|
217
|
+
flip: yes('select-flip'),
|
|
218
|
+
flop: yes('select-flop'),
|
|
219
|
+
brightness: numberText('brightness'),
|
|
220
|
+
saturation: numberText('saturation'),
|
|
221
|
+
format: includesChoice('select-output-format', ['png', 'jpeg'], 'jpeg'),
|
|
222
|
+
jpegQuality: numberText('jpeg-quality'),
|
|
223
|
+
progressive: yes('select-progressive-jpeg'),
|
|
224
|
+
pngCompression: numberText('png-compression'),
|
|
225
|
+
pngPalette: yes('select-png-palette'),
|
|
226
|
+
pngColors: numberText('png-colors'),
|
|
227
|
+
pngDither: yes('select-png-dither'),
|
|
228
|
+
};
|
|
229
|
+
const result = await rpc.resizeImage(options);
|
|
230
|
+
|
|
231
|
+
if (!result || typeof result !== 'object' || result.ok !== true) {
|
|
232
|
+
const error = result && typeof result === 'object' && 'error' in result
|
|
233
|
+
? result.error
|
|
234
|
+
: result;
|
|
235
|
+
status.val(`寫入失敗:\n${describeError(error)}\n讀取選項:\n${JSON.stringify(options, null, 2)}`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
status.val(`成功寫入:${result.outputPath}\n${result.width}×${result.height},${result.bytes} bytes\n讀取選項:\n${JSON.stringify(options, null, 2)}`);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const optionText = options ? `\n讀取選項:\n${JSON.stringify(options, null, 2)}` : '';
|
|
242
|
+
status.val(`寫入失敗:\n${describeError(error)}${optionText}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```js back
|
|
248
|
+
import { dirname, extname, join, basename, resolve } from 'node:path';
|
|
249
|
+
|
|
250
|
+
function integer(value, name, { min = 1, max = Number.MAX_SAFE_INTEGER, optional = false } = {}) {
|
|
251
|
+
if (optional && String(value ?? '').trim() === '') return undefined;
|
|
252
|
+
const result = Number(value);
|
|
253
|
+
if (!Number.isInteger(result) || result < min || result > max)
|
|
254
|
+
throw new Error(`${name} 必須是 ${min}–${max} 的整數`);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function finite(value, name, { min = 0 } = {}) {
|
|
259
|
+
const result = Number(value);
|
|
260
|
+
if (!Number.isFinite(result) || result < min)
|
|
261
|
+
throw new Error(`${name} 必須是至少 ${min} 的數字`);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function describeBackendError(error) {
|
|
266
|
+
if (error == null) return '未知錯誤(沒有錯誤內容)';
|
|
267
|
+
if (typeof error === 'string') return error;
|
|
268
|
+
const details = [];
|
|
269
|
+
if (error?.code) details.push(`[${error.code}]`);
|
|
270
|
+
if (error?.name && error.name !== 'Error') details.push(error.name);
|
|
271
|
+
if (error?.message) details.push(error.message);
|
|
272
|
+
if (error?.cause && error.cause !== error)
|
|
273
|
+
details.push(`原因:${describeBackendError(error.cause)}`);
|
|
274
|
+
if (error?.stack) details.push(`Stack:\n${error.stack}`);
|
|
275
|
+
if (details.length) return details.join('\n');
|
|
276
|
+
try {
|
|
277
|
+
const json = JSON.stringify(error, null, 2);
|
|
278
|
+
if (json && json !== '{}') return json;
|
|
279
|
+
} catch {}
|
|
280
|
+
return String(error);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function readImageMetadata(inputText) {
|
|
284
|
+
try {
|
|
285
|
+
const pathText = String(inputText ?? '').trim();
|
|
286
|
+
if (!pathText) throw new Error('請先貼上圖片路徑');
|
|
287
|
+
|
|
288
|
+
const inputPath = resolve(pathText);
|
|
289
|
+
const inputFile = Bun.file(inputPath);
|
|
290
|
+
if (!await inputFile.exists()) throw new Error(`找不到輸入檔:${inputPath}`);
|
|
291
|
+
|
|
292
|
+
const metadata = await new Bun.Image(inputFile).metadata();
|
|
293
|
+
return { ok: true, inputPath, metadata };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return { ok: false, error: describeBackendError(error) };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function resizeImage(options = {}) {
|
|
300
|
+
try {
|
|
301
|
+
const inputText = String(options.inputPath ?? '').trim();
|
|
302
|
+
if (!inputText) throw new Error('請先貼上圖片路徑');
|
|
303
|
+
|
|
304
|
+
const inputPath = resolve(inputText);
|
|
305
|
+
const inputFile = Bun.file(inputPath);
|
|
306
|
+
if (!await inputFile.exists()) throw new Error(`找不到輸入檔:${inputPath}`);
|
|
307
|
+
|
|
308
|
+
const width = integer(options.width, '寬度');
|
|
309
|
+
const height = integer(options.height, '高度', { optional: true });
|
|
310
|
+
const fit = options.fit === 'fill' ? 'fill' : 'inside';
|
|
311
|
+
const filters = new Set([
|
|
312
|
+
'nearest', 'box', 'bilinear', 'linear', 'cubic', 'mitchell',
|
|
313
|
+
'lanczos2', 'lanczos3', 'mks2013', 'mks2021',
|
|
314
|
+
]);
|
|
315
|
+
const filter = filters.has(options.filter) ? options.filter : 'lanczos3';
|
|
316
|
+
const rotate = [0, 90, 180, 270].includes(options.rotate) ? options.rotate : 0;
|
|
317
|
+
const brightness = finite(options.brightness, '亮度');
|
|
318
|
+
const saturation = finite(options.saturation, '飽和度');
|
|
319
|
+
const format = options.format === 'png' ? 'png' : 'jpeg';
|
|
320
|
+
|
|
321
|
+
let image = new Bun.Image(inputFile, { autoOrient: options.autoOrient !== false });
|
|
322
|
+
if (rotate) image = image.rotate(rotate);
|
|
323
|
+
if (options.flip) image = image.flip();
|
|
324
|
+
if (options.flop) image = image.flop();
|
|
325
|
+
image = image.resize(width, height, {
|
|
326
|
+
fit,
|
|
327
|
+
filter,
|
|
328
|
+
withoutEnlargement: Boolean(options.withoutEnlargement),
|
|
329
|
+
});
|
|
330
|
+
if (brightness !== 1 || saturation !== 1)
|
|
331
|
+
image = image.modulate({ brightness, saturation });
|
|
332
|
+
|
|
333
|
+
const originalName = basename(inputPath, extname(inputPath));
|
|
334
|
+
const outputPath = join(dirname(inputPath), `${originalName}.resized.${format === 'jpeg' ? 'jpg' : 'png'}`);
|
|
335
|
+
|
|
336
|
+
if (format === 'jpeg') {
|
|
337
|
+
image = image.jpeg({
|
|
338
|
+
quality: integer(options.jpegQuality, 'JPEG 品質', { min: 1, max: 100 }),
|
|
339
|
+
progressive: Boolean(options.progressive),
|
|
340
|
+
});
|
|
341
|
+
} else {
|
|
342
|
+
const palette = Boolean(options.pngPalette);
|
|
343
|
+
const pngOptions = {
|
|
344
|
+
compressionLevel: integer(options.pngCompression, 'PNG 壓縮等級', { min: 0, max: 9 }),
|
|
345
|
+
palette,
|
|
346
|
+
};
|
|
347
|
+
if (palette) {
|
|
348
|
+
pngOptions.colors = integer(options.pngColors, 'PNG 調色盤色數', { min: 2, max: 256 });
|
|
349
|
+
pngOptions.dither = Boolean(options.pngDither);
|
|
350
|
+
}
|
|
351
|
+
image = image.png(pngOptions);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const bytes = await image.write(outputPath);
|
|
355
|
+
const metadata = await new Bun.Image(outputPath).metadata();
|
|
356
|
+
return { ok: true, outputPath, bytes, width: metadata.width, height: metadata.height };
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return { ok: false, error: describeBackendError(error) };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
package/demos/select.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env jsmdcui
|
|
2
|
+
|
|
3
|
+
# Select and Task List Demo
|
|
4
|
+
|
|
5
|
+
Use `Enter` or `Space` to toggle a task item in the TUI, then activate these
|
|
6
|
+
links to read the values under headings at different levels.
|
|
7
|
+
|
|
8
|
+
- [Read primary color (h2 single value)](javascript:showHeadingValue('select-primary-color'))
|
|
9
|
+
- [Read enabled features (h3 array)](javascript:showHeadingValue('enabled-features'))
|
|
10
|
+
- [Read log level (h4 single value)](javascript:showHeadingValue('select-log-level'))
|
|
11
|
+
- [Read permissions (h2 array)](javascript:showHeadingValue('permissions'))
|
|
12
|
+
- [Read every group](javascript:showAllHeadingValues())
|
|
13
|
+
|
|
14
|
+
## Select Primary Color
|
|
15
|
+
|
|
16
|
+
- [ ] Red
|
|
17
|
+
- [x] Green
|
|
18
|
+
- [x] Nested green detail (ignored)
|
|
19
|
+
- [ ] Another nested detail (ignored)
|
|
20
|
+
- [x] Blue (ignored because select returns the first checked value)
|
|
21
|
+
|
|
22
|
+
### Enabled Features
|
|
23
|
+
|
|
24
|
+
- [x] Search
|
|
25
|
+
- [ ] Notifications
|
|
26
|
+
- [x] Nested notification option (ignored)
|
|
27
|
+
- [x] Offline mode
|
|
28
|
+
- [x] Sync
|
|
29
|
+
|
|
30
|
+
#### Select Log Level
|
|
31
|
+
|
|
32
|
+
- [ ] Debug
|
|
33
|
+
- [x] Info
|
|
34
|
+
- [ ] Warning
|
|
35
|
+
- [ ] Error
|
|
36
|
+
|
|
37
|
+
## Permissions
|
|
38
|
+
|
|
39
|
+
- [x] Read
|
|
40
|
+
- [x] Write
|
|
41
|
+
- [x] Write temporary files (ignored)
|
|
42
|
+
- [ ] Write system files (ignored)
|
|
43
|
+
- [ ] Execute
|
|
44
|
+
- [x] Share
|
|
45
|
+
|
|
46
|
+
```js front
|
|
47
|
+
export function showHeadingValue(id)
|
|
48
|
+
{
|
|
49
|
+
const value = $('#'+id).val();
|
|
50
|
+
alert(id+': '+JSON.stringify(value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function showAllHeadingValues()
|
|
54
|
+
{
|
|
55
|
+
const values = {
|
|
56
|
+
primaryColor: $('#select-primary-color').val(),
|
|
57
|
+
enabledFeatures: $('#enabled-features').val(),
|
|
58
|
+
logLevel: $('#select-log-level').val(),
|
|
59
|
+
permissions: $('#permissions').val(),
|
|
60
|
+
};
|
|
61
|
+
alert(JSON.stringify(values, null, 2));
|
|
62
|
+
}
|
|
63
|
+
```
|
package/demos/todo-zh.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env jsmdcui
|
|
2
|
+
|
|
3
|
+
# Todo List
|
|
4
|
+
|
|
5
|
+
## Todos
|
|
6
|
+
|
|
7
|
+
- [ ] canvas support
|
|
8
|
+
- [x] push/pop/splice/slice list items
|
|
9
|
+
- [x] image by --kitty
|
|
10
|
+
- [ ] clean up codebase
|
|
11
|
+
|
|
12
|
+
## Actions
|
|
13
|
+
|
|
14
|
+
```text#todo-input
|
|
15
|
+
Fly to the moon
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
在上方輸入 Todo 文字,再選擇新增或移除。移除會刪除第一個文字完全相同的項目。
|
|
19
|
+
|
|
20
|
+
- [新增 Todo](javascript:addTodo())
|
|
21
|
+
- [移除 Todo](javascript:removeTodo())
|
|
22
|
+
- [顯示已完成](javascript:showCompleted())
|
|
23
|
+
- [顯示未完成](javascript:showPending())
|
|
24
|
+
|
|
25
|
+
## 操作結果:
|
|
26
|
+
|
|
27
|
+
```text#todo-status
|
|
28
|
+
尚未操作
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Filtered Result
|
|
32
|
+
|
|
33
|
+
```text#todo-result
|
|
34
|
+
點按「顯示已完成」或「顯示未完成」查看結果
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```js front
|
|
38
|
+
function todoText() {
|
|
39
|
+
return $('#todo-input').val().trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function showItems(title, checked) {
|
|
43
|
+
const items = $('#todos')
|
|
44
|
+
.slice()
|
|
45
|
+
.filter(item => item.checked === checked);
|
|
46
|
+
|
|
47
|
+
const output = items.length
|
|
48
|
+
? items.map(item => `${item.checked ? '✓' : '○'} ${item.value}`).join('\n')
|
|
49
|
+
: '(沒有項目)';
|
|
50
|
+
$('#todo-result').val(`${title}\n${output}`);
|
|
51
|
+
$('#todo-status').val(`找到 ${items.length} 個${title}項目`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function addTodo() {
|
|
55
|
+
const value = todoText();
|
|
56
|
+
if (!value) {
|
|
57
|
+
$('#todo-status').val('新增失敗:請先輸入 Todo 文字');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const length = $('#todos').push(value);
|
|
62
|
+
$('#todo-input').val('');
|
|
63
|
+
$('#todo-status').val(`已新增「${value}」,目前共有 ${length} 個 Todo`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function removeTodo() {
|
|
67
|
+
const value = todoText();
|
|
68
|
+
if (!value) {
|
|
69
|
+
$('#todo-status').val('移除失敗:請輸入要移除的 Todo 文字');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const items = $('#todos').slice();
|
|
74
|
+
const index = items.findIndex(item => item.value === value);
|
|
75
|
+
if (index < 0) {
|
|
76
|
+
$('#todo-status').val(`移除失敗:找不到「${value}」`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const removed = $('#todos').splice(index, 1);
|
|
81
|
+
$('#todo-input').val('');
|
|
82
|
+
$('#todo-status').val(`已移除「${removed[0]}」`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function showCompleted() {
|
|
86
|
+
showItems('已完成', true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function showPending() {
|
|
90
|
+
showItems('未完成', false);
|
|
91
|
+
}
|
|
92
|
+
```
|
package/demos/todo.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env jsmdcui
|
|
2
|
+
|
|
3
|
+
# Todo List
|
|
4
|
+
|
|
5
|
+
## Todos
|
|
6
|
+
|
|
7
|
+
- [ ] canvas support
|
|
8
|
+
- [x] push/pop/splice/slice list items
|
|
9
|
+
- [x] image by --kitty
|
|
10
|
+
- [ ] clean up codebase
|
|
11
|
+
|
|
12
|
+
## Actions
|
|
13
|
+
|
|
14
|
+
```text#todo-input
|
|
15
|
+
Fly to the moon
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Enter Todo text above, then choose Add or Remove. Remove deletes the first item whose text matches exactly.
|
|
19
|
+
|
|
20
|
+
- [Add Todo](javascript:addTodo())
|
|
21
|
+
- [Remove Todo](javascript:removeTodo())
|
|
22
|
+
- [Show Completed](javascript:showCompleted())
|
|
23
|
+
- [Show Pending](javascript:showPending())
|
|
24
|
+
|
|
25
|
+
## Operation Result
|
|
26
|
+
|
|
27
|
+
```text#todo-status
|
|
28
|
+
No action yet
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Filtered Result
|
|
32
|
+
|
|
33
|
+
```text#todo-result
|
|
34
|
+
Select “Show Completed” or “Show Pending” to view matching items
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```js front
|
|
38
|
+
function todoText() {
|
|
39
|
+
return $('#todo-input').val().trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function showItems(title, checked) {
|
|
43
|
+
const items = $('#todos')
|
|
44
|
+
.slice()
|
|
45
|
+
.filter(item => item.checked === checked);
|
|
46
|
+
|
|
47
|
+
const output = items.length
|
|
48
|
+
? items.map(item => `${item.checked ? '✓' : '○'} ${item.value}`).join('\n')
|
|
49
|
+
: '(No items)';
|
|
50
|
+
const noun = items.length === 1 ? 'item' : 'items';
|
|
51
|
+
$('#todo-result').val(`${title}\n${output}`);
|
|
52
|
+
$('#todo-status').val(`Found ${items.length} ${title.toLowerCase()} ${noun}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function addTodo() {
|
|
56
|
+
const value = todoText();
|
|
57
|
+
if (!value) {
|
|
58
|
+
$('#todo-status').val('Add failed: enter Todo text first');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const length = $('#todos').push(value);
|
|
63
|
+
$('#todo-input').val('');
|
|
64
|
+
$('#todo-status').val(`Added “${value}”; ${length} Todos total`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function removeTodo() {
|
|
68
|
+
const value = todoText();
|
|
69
|
+
if (!value) {
|
|
70
|
+
$('#todo-status').val('Remove failed: enter the Todo text to remove');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const items = $('#todos').slice();
|
|
75
|
+
const index = items.findIndex(item => item.value === value);
|
|
76
|
+
if (index < 0) {
|
|
77
|
+
$('#todo-status').val(`Remove failed: could not find “${value}”`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const removed = $('#todos').splice(index, 1);
|
|
82
|
+
$('#todo-input').val('');
|
|
83
|
+
$('#todo-status').val(`Removed “${removed[0]}”`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function showCompleted() {
|
|
87
|
+
showItems('Completed', true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function showPending() {
|
|
91
|
+
showItems('Pending', false);
|
|
92
|
+
}
|
|
93
|
+
```
|