xw-devtool-cli 1.0.42 → 1.0.44
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 +56 -2
- package/README_EN.md +28 -1
- package/package.json +10 -1
- package/src/commands/clock.js +135 -0
- package/src/commands/countdown.js +159 -0
- package/src/commands/qrcodeDecode.js +76 -0
- package/src/commands/stopwatch.js +125 -0
- package/src/commands/url.js +4 -1
- package/src/index.js +20 -0
- package/src/locales/en.js +42 -0
- package/src/locales/zh.js +42 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
一个基于 Node.js 的开发者命令行工具箱,旨在提供开箱即用的常用开发工具,帮助开发者快速处理日常任务。
|
|
6
6
|
|
|
7
|
-
主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock
|
|
7
|
+
主要功能包括:Base64 编解码、图片格式转换、图片与 Base64 互转、Mock 数据生成、时间戳/日期格式化、时间计算、控制台时钟、倒计时工具、计时工具、URL 编解码、UUID 生成、汉字转拼音、颜色转换、变量格式转换、哈希计算、二维码生成、二维码识别、特殊符号大全、Markdown 语法工具、VS Code 代码段生成、当前目录树生成等。大部分结果可一键复制到剪贴板,极大提升开发效率。
|
|
8
8
|
|
|
9
9
|
## ✨ 功能特性
|
|
10
10
|
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
- **时间格式化**:时间戳/日期字符串 -> `YYYY-MM-DD HH:mm:ss`。
|
|
36
36
|
- **获取时间戳**:快速获取当前毫秒级时间戳。
|
|
37
37
|
- **时间计算**:计算日期差值或日期偏移 (Add/Subtract)。
|
|
38
|
+
- **控制台时钟**:在终端实时显示当前时间,按 `Enter` 或 `Q` 返回上一步。
|
|
39
|
+
- **倒计时工具**:输入倒计时分钟数后启动终端倒计时;倒计时结束后继续计时并显示负号(如 `-00:00:05`)。
|
|
40
|
+
- **计时工具**:启动后从 `00:00:00` 开始正向计时,终端每秒刷新显示。
|
|
38
41
|
- **开发辅助工具**:
|
|
39
42
|
- **URL 编解码**:Encode/Decode URL。
|
|
40
43
|
- **Unicode 编解码**:文本与 Unicode 转义序列 (\uXXXX) 互转。
|
|
@@ -46,6 +49,7 @@
|
|
|
46
49
|
- **变量格式转换**:支持 CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase 互转。
|
|
47
50
|
- **哈希计算**:支持 MD5, SHA1, SHA256, SHA512, SM3 算法。
|
|
48
51
|
- **二维码生成**:终端直接显示二维码,支持保存为 PNG 图片(带时间戳文件名)。
|
|
52
|
+
- **二维码识别**:支持选择二维码图片进行识别,识别结果自动复制到剪贴板。
|
|
49
53
|
- **特殊符号大全**:包含常用符号、箭头、数学符号、货币、希腊字母等 170+ 个符号,支持一键复制。
|
|
50
54
|
- **Emoji 输入**:支持分类查看和选择常用 Emoji,一键复制到剪贴板。
|
|
51
55
|
- **HTML 实体工具**:支持 HTML 实体编码与解码 (如 `&` <-> `&`)。
|
|
@@ -209,6 +213,13 @@ x. 设置 / 语言 (Settings)
|
|
|
209
213
|
- 终端直接显示二维码预览。
|
|
210
214
|
- 可选保存为 PNG 图片,默认文件名包含时间戳。
|
|
211
215
|
|
|
216
|
+
### 二维码识别
|
|
217
|
+
- 选择 `qrcodeDecode`(菜单对应数字/字母)进入。
|
|
218
|
+
- 支持两种图片输入方式:
|
|
219
|
+
- **选择文件(对话框)**
|
|
220
|
+
- **手动输入文件路径**
|
|
221
|
+
- 自动识别二维码内容并复制到剪贴板。
|
|
222
|
+
|
|
212
223
|
### 5. URL 编解码
|
|
213
224
|
- 选择 `5` 进入。
|
|
214
225
|
- 选择 `Encode` (编码) 或 `Decode` (解码)。
|
|
@@ -283,6 +294,22 @@ x. 设置 / 语言 (Settings)
|
|
|
283
294
|
5. 查看计算后的新日期。
|
|
284
295
|
- 示例:计算 "当前时间 3 天后" 或 "2025-01-01 5 小时前"。
|
|
285
296
|
|
|
297
|
+
### 控制台时钟
|
|
298
|
+
- 在菜单中选择 `控制台时钟` 进入。
|
|
299
|
+
- 终端会每秒刷新显示当前时间。
|
|
300
|
+
- 按 `Enter` 或 `Q` 返回上一步。
|
|
301
|
+
|
|
302
|
+
### 倒计时工具
|
|
303
|
+
- 在菜单中选择 `倒计时工具` 进入。
|
|
304
|
+
- 输入倒计时分钟数后,终端会每秒刷新倒计时。
|
|
305
|
+
- 倒计时结束后会继续计时并显示负号(如 `-00:00:01`、`-00:00:02`)。
|
|
306
|
+
- 按 `Enter` 或 `Q` 返回上一步。
|
|
307
|
+
|
|
308
|
+
### 计时工具
|
|
309
|
+
- 在菜单中选择 `计时工具` 进入。
|
|
310
|
+
- 启动后从 `00:00:00` 开始,终端每秒刷新已计时时间。
|
|
311
|
+
- 按 `Enter` 或 `Q` 返回上一步。
|
|
312
|
+
|
|
286
313
|
### 14. 颜色转换 (Hex <-> RGB)
|
|
287
314
|
- 选择 `e` 进入。
|
|
288
315
|
- 输入 Hex 颜色值 (如 #333) 或 RGB 值 (如 rgb(51,51,51))。
|
|
@@ -346,7 +373,7 @@ x. 设置 / 语言 (Settings)
|
|
|
346
373
|
|
|
347
374
|
A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
|
|
348
375
|
|
|
349
|
-
Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, special symbols, Markdown snippets, VS Code snippet generation, etc.
|
|
376
|
+
Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, console clock, countdown timer, stopwatch, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
|
|
350
377
|
|
|
351
378
|
### ✨ Features
|
|
352
379
|
|
|
@@ -372,6 +399,9 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
372
399
|
- **Formatting**: Timestamp/Date string -> `YYYY-MM-DD HH:mm:ss`.
|
|
373
400
|
- **Timestamp**: Quickly get current millisecond timestamp.
|
|
374
401
|
- **Calculation**: Calculate date differences or offsets (Add/Subtract).
|
|
402
|
+
- **Console Clock**: Show real-time current time in terminal, press `Enter` or `Q` to go back.
|
|
403
|
+
- **Countdown Timer**: Enter countdown minutes to start timer; once it reaches zero, it keeps counting with a negative sign (such as `-00:00:05`).
|
|
404
|
+
- **Stopwatch**: Start from `00:00:00` and count up every second in terminal.
|
|
375
405
|
- **Dev Tools**:
|
|
376
406
|
- **URL Encode/Decode**
|
|
377
407
|
- **Unicode Encode/Decode**: Text <-> Unicode escape sequences (\uXXXX).
|
|
@@ -381,6 +411,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
381
411
|
- **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
|
|
382
412
|
- **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
|
|
383
413
|
- **QR Code**: Display QR codes in terminal, save as PNG.
|
|
414
|
+
- **QR Code Reader**: Select an image with QR code and decode content, then copy the result to clipboard automatically.
|
|
384
415
|
- **Special Symbols**: 170+ symbols, arrows, math symbols, etc.
|
|
385
416
|
- **Emoji Picker**: Browse and copy emojis.
|
|
386
417
|
- **HTML Entity**: Encode/Decode HTML entities.
|
|
@@ -495,6 +526,13 @@ You can enter a feature key directly, or type a keyword (e.g. `json`, `image`, `
|
|
|
495
526
|
- Preview in terminal.
|
|
496
527
|
- Option to save as PNG.
|
|
497
528
|
|
|
529
|
+
#### QR Code Reader
|
|
530
|
+
- Select `qrcodeDecode` (menu key).
|
|
531
|
+
- Image input methods:
|
|
532
|
+
- **Select file (dialog)**
|
|
533
|
+
- **Enter file path manually**
|
|
534
|
+
- Decode content and copy it to clipboard automatically.
|
|
535
|
+
|
|
498
536
|
#### 5. URL Encode/Decode
|
|
499
537
|
- Select `5`.
|
|
500
538
|
- Select `Encode` or `Decode`.
|
|
@@ -547,6 +585,22 @@ You can enter a feature key directly, or type a keyword (e.g. `json`, `image`, `
|
|
|
547
585
|
- **Diff**: Calculate difference between two dates.
|
|
548
586
|
- **Offset**: Calculate date after add/subtract time units.
|
|
549
587
|
|
|
588
|
+
#### Console Clock
|
|
589
|
+
- Select `Console Clock` in the menu.
|
|
590
|
+
- Current time refreshes every second in terminal.
|
|
591
|
+
- Press `Enter` or `Q` to go back.
|
|
592
|
+
|
|
593
|
+
#### Countdown Timer
|
|
594
|
+
- Select `Countdown Timer` in the menu.
|
|
595
|
+
- Enter minutes to start the countdown.
|
|
596
|
+
- After reaching zero, the timer continues with a negative sign (for example `-00:00:01`).
|
|
597
|
+
- Press `Enter` or `Q` to go back.
|
|
598
|
+
|
|
599
|
+
#### Stopwatch
|
|
600
|
+
- Select `Stopwatch` in the menu.
|
|
601
|
+
- It starts from `00:00:00` and updates every second.
|
|
602
|
+
- Press `Enter` or `Q` to go back.
|
|
603
|
+
|
|
550
604
|
#### 14. Color Converter
|
|
551
605
|
- Select `e`.
|
|
552
606
|
- Hex <-> RGB.
|
package/README_EN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A Node.js-based developer command-line toolbox designed to provide out-of-the-box common development tools to help developers handle daily tasks quickly.
|
|
6
6
|
|
|
7
|
-
Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
|
|
7
|
+
Key features include: Base64 encoding/decoding, image format conversion, image <-> Base64, Mock data generation, timestamp/date formatting, time calculation, console clock, countdown timer, stopwatch, URL encoding/decoding, UUID generation, Chinese pinyin conversion, color conversion, variable format conversion, hash calculation, QR code generation, QR code recognition, special symbols, Markdown snippets, VS Code snippet generation, current directory tree generation, etc. Most results can be copied to the clipboard in one step, greatly improving development efficiency.
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
@@ -29,6 +29,9 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
29
29
|
- **Formatting**: Timestamp/Date string -> `YYYY-MM-DD HH:mm:ss`.
|
|
30
30
|
- **Timestamp**: Quickly get current millisecond timestamp.
|
|
31
31
|
- **Calculation**: Calculate date differences or offsets (Add/Subtract).
|
|
32
|
+
- **Console Clock**: Show real-time current time in terminal, press `Enter` or `Q` to go back.
|
|
33
|
+
- **Countdown Timer**: Enter countdown minutes to start timer; once it reaches zero, it keeps counting with a negative sign (such as `-00:00:05`).
|
|
34
|
+
- **Stopwatch**: Start from `00:00:00` and count up every second in terminal.
|
|
32
35
|
- **Dev Tools**:
|
|
33
36
|
- **URL Encode/Decode**
|
|
34
37
|
- **Unicode Encode/Decode**: Text <-> Unicode escape sequences (\uXXXX).
|
|
@@ -40,6 +43,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
|
|
|
40
43
|
- **Variable Format**: CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase.
|
|
41
44
|
- **Hash Calculator**: MD5, SHA1, SHA256, SHA512, SM3.
|
|
42
45
|
- **QR Code**: Display QR codes in terminal, save as PNG.
|
|
46
|
+
- **QR Code Reader**: Select a QR code image and recognize the content, then copy the result to clipboard.
|
|
43
47
|
- **Special Symbols**: 170+ symbols, arrows, math symbols, etc.
|
|
44
48
|
- **Emoji Picker**: Browse and copy emojis.
|
|
45
49
|
- **HTML Entity**: Encode/Decode HTML entities.
|
|
@@ -160,6 +164,13 @@ s. Settings (Language)
|
|
|
160
164
|
- Preview in terminal.
|
|
161
165
|
- Option to save as PNG.
|
|
162
166
|
|
|
167
|
+
### QR Code Reader
|
|
168
|
+
- Select `qrcodeDecode` (menu key).
|
|
169
|
+
- Supports two input methods:
|
|
170
|
+
- **Select file (dialog)**
|
|
171
|
+
- **Enter file path manually**
|
|
172
|
+
- Recognized content is copied to clipboard automatically.
|
|
173
|
+
|
|
163
174
|
### 5. URL Encode/Decode
|
|
164
175
|
- Select `5`.
|
|
165
176
|
- Select `Encode` or `Decode`.
|
|
@@ -216,6 +227,22 @@ s. Settings (Language)
|
|
|
216
227
|
- **Diff**: Calculate difference between two dates.
|
|
217
228
|
- **Offset**: Calculate date after add/subtract time units.
|
|
218
229
|
|
|
230
|
+
### Console Clock
|
|
231
|
+
- Select `Console Clock` in the menu.
|
|
232
|
+
- Current time refreshes every second in terminal.
|
|
233
|
+
- Press `Enter` or `Q` to go back.
|
|
234
|
+
|
|
235
|
+
### Countdown Timer
|
|
236
|
+
- Select `Countdown Timer` in the menu.
|
|
237
|
+
- Enter minutes to start the countdown.
|
|
238
|
+
- After reaching zero, the timer continues with a negative sign (for example `-00:00:01`).
|
|
239
|
+
- Press `Enter` or `Q` to go back.
|
|
240
|
+
|
|
241
|
+
### Stopwatch
|
|
242
|
+
- Select `Stopwatch` in the menu.
|
|
243
|
+
- It starts from `00:00:00` and updates every second.
|
|
244
|
+
- Press `Enter` or `Q` to go back.
|
|
245
|
+
|
|
219
246
|
### 14. Color Converter
|
|
220
247
|
- Select `e`.
|
|
221
248
|
- Hex <-> RGB.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xw-devtool-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "基于node的开发者助手cli",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
"mock-data",
|
|
35
35
|
"timestamp",
|
|
36
36
|
"time",
|
|
37
|
+
"clock",
|
|
38
|
+
"console-clock",
|
|
39
|
+
"countdown",
|
|
40
|
+
"countdown-timer",
|
|
41
|
+
"stopwatch",
|
|
42
|
+
"stopwatch-timer",
|
|
37
43
|
"date-calculation",
|
|
38
44
|
"image",
|
|
39
45
|
"image-conversion",
|
|
@@ -42,6 +48,8 @@
|
|
|
42
48
|
"png",
|
|
43
49
|
"placeholder-image",
|
|
44
50
|
"qrcode",
|
|
51
|
+
"qr-code-reader",
|
|
52
|
+
"qr-decoder",
|
|
45
53
|
"json-format",
|
|
46
54
|
"json-formatter",
|
|
47
55
|
"variable-case",
|
|
@@ -87,6 +95,7 @@
|
|
|
87
95
|
"he": "^1.2.0",
|
|
88
96
|
"i18next": "^25.7.3",
|
|
89
97
|
"inquirer": "^13.1.0",
|
|
98
|
+
"jsqr": "^1.4.0",
|
|
90
99
|
"lorem-ipsum": "^2.0.8",
|
|
91
100
|
"pinyin": "^4.0.0",
|
|
92
101
|
"qrcode": "^1.5.4",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import i18next from '../i18n.js';
|
|
3
|
+
|
|
4
|
+
function getLocale() {
|
|
5
|
+
return i18next.language && i18next.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getNowDisplay(locale) {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const timeText = new Intl.DateTimeFormat(locale, {
|
|
11
|
+
hour12: false,
|
|
12
|
+
hour: '2-digit',
|
|
13
|
+
minute: '2-digit',
|
|
14
|
+
second: '2-digit'
|
|
15
|
+
}).format(now);
|
|
16
|
+
const dateText = new Intl.DateTimeFormat(locale, {
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: '2-digit',
|
|
19
|
+
day: '2-digit',
|
|
20
|
+
weekday: 'long'
|
|
21
|
+
}).format(now);
|
|
22
|
+
return { timeText, dateText };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getCharDisplayWidth(char) {
|
|
26
|
+
if (/[\u0000-\u001f\u007f]/.test(char)) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
/[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
|
|
31
|
+
) {
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDisplayWidth(text) {
|
|
38
|
+
let width = 0;
|
|
39
|
+
for (const char of text) {
|
|
40
|
+
width += getCharDisplayWidth(char);
|
|
41
|
+
}
|
|
42
|
+
return width;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fitText(text, width) {
|
|
46
|
+
let result = '';
|
|
47
|
+
let currentWidth = 0;
|
|
48
|
+
for (const char of text) {
|
|
49
|
+
const charWidth = getCharDisplayWidth(char);
|
|
50
|
+
if (currentWidth + charWidth > width) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
result += char;
|
|
54
|
+
currentWidth += charWidth;
|
|
55
|
+
}
|
|
56
|
+
const remaining = width - getDisplayWidth(result);
|
|
57
|
+
if (remaining > 0) {
|
|
58
|
+
return `${result}${' '.repeat(remaining)}`;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderClockBlock() {
|
|
64
|
+
const { timeText, dateText } = getNowDisplay(getLocale());
|
|
65
|
+
const innerWidth = 34;
|
|
66
|
+
const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
|
|
67
|
+
const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
|
|
68
|
+
const line = (text) => `│ ${fitText(text, innerWidth)} │`;
|
|
69
|
+
return [
|
|
70
|
+
top,
|
|
71
|
+
line(i18next.t('clock.title')),
|
|
72
|
+
line(''),
|
|
73
|
+
line(`${i18next.t('clock.now')}${timeText}`),
|
|
74
|
+
line(dateText),
|
|
75
|
+
line(i18next.t('clock.exitTip')),
|
|
76
|
+
bottom
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function clockHandler() {
|
|
81
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
82
|
+
const { timeText, dateText } = getNowDisplay(getLocale());
|
|
83
|
+
console.log(`${i18next.t('clock.now')}${timeText} (${dateText})`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
let renderedLineCount = 0;
|
|
89
|
+
let shouldExitApp = false;
|
|
90
|
+
|
|
91
|
+
const render = () => {
|
|
92
|
+
const lines = renderClockBlock();
|
|
93
|
+
if (renderedLineCount > 0) {
|
|
94
|
+
readline.moveCursor(process.stdout, 0, -renderedLineCount);
|
|
95
|
+
readline.cursorTo(process.stdout, 0);
|
|
96
|
+
readline.clearScreenDown(process.stdout);
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
99
|
+
renderedLineCount = lines.length;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
clearInterval(timer);
|
|
104
|
+
process.stdin.off('data', onData);
|
|
105
|
+
process.stdin.setRawMode(false);
|
|
106
|
+
process.stdin.pause();
|
|
107
|
+
readline.cursorTo(process.stdout, 0);
|
|
108
|
+
readline.clearScreenDown(process.stdout);
|
|
109
|
+
process.stdout.write('\n');
|
|
110
|
+
if (shouldExitApp) {
|
|
111
|
+
console.log(i18next.t('menu.bye'));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
resolve();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onData = (chunk) => {
|
|
118
|
+
const key = chunk.toString().toLowerCase();
|
|
119
|
+
if (key === '0') {
|
|
120
|
+
shouldExitApp = true;
|
|
121
|
+
cleanup();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (key === '\r' || key === 'q' || key === '\u0003') {
|
|
125
|
+
cleanup();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const timer = setInterval(render, 1000);
|
|
130
|
+
process.stdin.setRawMode(true);
|
|
131
|
+
process.stdin.resume();
|
|
132
|
+
process.stdin.on('data', onData);
|
|
133
|
+
render();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import i18next from '../i18n.js';
|
|
4
|
+
|
|
5
|
+
function getCharDisplayWidth(char) {
|
|
6
|
+
if (/[\u0000-\u001f\u007f]/.test(char)) {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
if (
|
|
10
|
+
/[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
|
|
11
|
+
) {
|
|
12
|
+
return 2;
|
|
13
|
+
}
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getDisplayWidth(text) {
|
|
18
|
+
let width = 0;
|
|
19
|
+
for (const char of text) {
|
|
20
|
+
width += getCharDisplayWidth(char);
|
|
21
|
+
}
|
|
22
|
+
return width;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fitText(text, width) {
|
|
26
|
+
let result = '';
|
|
27
|
+
let currentWidth = 0;
|
|
28
|
+
for (const char of text) {
|
|
29
|
+
const charWidth = getCharDisplayWidth(char);
|
|
30
|
+
if (currentWidth + charWidth > width) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
result += char;
|
|
34
|
+
currentWidth += charWidth;
|
|
35
|
+
}
|
|
36
|
+
const remaining = width - getDisplayWidth(result);
|
|
37
|
+
if (remaining > 0) {
|
|
38
|
+
return `${result}${' '.repeat(remaining)}`;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatSeconds(seconds) {
|
|
44
|
+
const isNegative = seconds < 0;
|
|
45
|
+
const absSeconds = Math.abs(seconds);
|
|
46
|
+
const hours = Math.floor(absSeconds / 3600);
|
|
47
|
+
const minutes = Math.floor((absSeconds % 3600) / 60);
|
|
48
|
+
const secs = absSeconds % 60;
|
|
49
|
+
const sign = isNegative ? '-' : '';
|
|
50
|
+
return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseMinutes(input) {
|
|
54
|
+
const value = Number(input);
|
|
55
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderCountdownBlock(totalSeconds, elapsedSeconds) {
|
|
62
|
+
const remainSeconds = totalSeconds - elapsedSeconds;
|
|
63
|
+
const innerWidth = 34;
|
|
64
|
+
const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
|
|
65
|
+
const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
|
|
66
|
+
const line = (text) => `│ ${fitText(text, innerWidth)} │`;
|
|
67
|
+
const status = remainSeconds < 0 ? i18next.t('countdown.statusOvertime') : i18next.t('countdown.statusCounting');
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
top,
|
|
71
|
+
line(i18next.t('countdown.title')),
|
|
72
|
+
line(''),
|
|
73
|
+
line(`${i18next.t('countdown.remaining')}${formatSeconds(remainSeconds)}`),
|
|
74
|
+
line(`${i18next.t('countdown.total')}${formatSeconds(totalSeconds)}`),
|
|
75
|
+
line(`${i18next.t('countdown.status')}${status}`),
|
|
76
|
+
line(i18next.t('countdown.exitTip')),
|
|
77
|
+
bottom
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function countdownHandler() {
|
|
82
|
+
const { minutesInput } = await inquirer.prompt([
|
|
83
|
+
{
|
|
84
|
+
type: 'input',
|
|
85
|
+
name: 'minutesInput',
|
|
86
|
+
message: i18next.t('countdown.inputPrompt'),
|
|
87
|
+
default: '1',
|
|
88
|
+
validate: (input) => {
|
|
89
|
+
const value = parseMinutes(input);
|
|
90
|
+
if (value === null) {
|
|
91
|
+
return i18next.t('countdown.invalidMinutes');
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const minutes = parseMinutes(minutesInput);
|
|
99
|
+
const totalSeconds = Math.round(minutes * 60);
|
|
100
|
+
|
|
101
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
102
|
+
console.log(i18next.t('countdown.nonTtyTip', { value: formatSeconds(totalSeconds) }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await new Promise((resolve) => {
|
|
107
|
+
let renderedLineCount = 0;
|
|
108
|
+
let elapsedSeconds = 0;
|
|
109
|
+
let shouldExitApp = false;
|
|
110
|
+
|
|
111
|
+
const render = () => {
|
|
112
|
+
const lines = renderCountdownBlock(totalSeconds, elapsedSeconds);
|
|
113
|
+
if (renderedLineCount > 0) {
|
|
114
|
+
readline.moveCursor(process.stdout, 0, -renderedLineCount);
|
|
115
|
+
readline.cursorTo(process.stdout, 0);
|
|
116
|
+
readline.clearScreenDown(process.stdout);
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
119
|
+
renderedLineCount = lines.length;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const cleanup = () => {
|
|
123
|
+
clearInterval(timer);
|
|
124
|
+
process.stdin.off('data', onData);
|
|
125
|
+
process.stdin.setRawMode(false);
|
|
126
|
+
process.stdin.pause();
|
|
127
|
+
readline.cursorTo(process.stdout, 0);
|
|
128
|
+
readline.clearScreenDown(process.stdout);
|
|
129
|
+
process.stdout.write('\n');
|
|
130
|
+
if (shouldExitApp) {
|
|
131
|
+
console.log(i18next.t('menu.bye'));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const onData = (chunk) => {
|
|
138
|
+
const key = chunk.toString().toLowerCase();
|
|
139
|
+
if (key === '0') {
|
|
140
|
+
shouldExitApp = true;
|
|
141
|
+
cleanup();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (key === '\r' || key === 'q' || key === '\u0003') {
|
|
145
|
+
cleanup();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const timer = setInterval(() => {
|
|
150
|
+
elapsedSeconds += 1;
|
|
151
|
+
render();
|
|
152
|
+
}, 1000);
|
|
153
|
+
|
|
154
|
+
process.stdin.setRawMode(true);
|
|
155
|
+
process.stdin.resume();
|
|
156
|
+
process.stdin.on('data', onData);
|
|
157
|
+
render();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import jsQR from 'jsqr';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import i18next from '../i18n.js';
|
|
6
|
+
import { selectFile } from '../utils/fileDialog.js';
|
|
7
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
8
|
+
import { outputText } from '../utils/output.js';
|
|
9
|
+
|
|
10
|
+
async function getImagePath() {
|
|
11
|
+
const method = await selectFromMenu(
|
|
12
|
+
i18next.t('qrcodeDecode.selectInputMethod'),
|
|
13
|
+
[
|
|
14
|
+
{ name: i18next.t('qrcodeDecode.inputDialog'), value: 'dialog' },
|
|
15
|
+
{ name: i18next.t('qrcodeDecode.inputManual'), value: 'manual' }
|
|
16
|
+
],
|
|
17
|
+
true,
|
|
18
|
+
i18next.t('common.back')
|
|
19
|
+
);
|
|
20
|
+
if (method === '__BACK__') {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let imagePath = '';
|
|
25
|
+
if (method === 'dialog') {
|
|
26
|
+
imagePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.gif;*.bmp;*.webp|All Files|*.*') || '';
|
|
27
|
+
if (!imagePath) {
|
|
28
|
+
console.log(i18next.t('qrcodeDecode.dialogUnavailable'));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!imagePath) {
|
|
33
|
+
const answer = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'input',
|
|
36
|
+
name: 'path',
|
|
37
|
+
message: i18next.t('qrcodeDecode.enterPath'),
|
|
38
|
+
validate: (value) => (fs.existsSync(value) ? true : i18next.t('qrcodeDecode.notExist'))
|
|
39
|
+
}
|
|
40
|
+
]);
|
|
41
|
+
imagePath = answer.path;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return imagePath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function decodeQRCodeFromImage(imagePath) {
|
|
48
|
+
const { data, info } = await sharp(imagePath)
|
|
49
|
+
.rotate()
|
|
50
|
+
.ensureAlpha()
|
|
51
|
+
.raw()
|
|
52
|
+
.toBuffer({ resolveWithObject: true });
|
|
53
|
+
const pixelData = new Uint8ClampedArray(data);
|
|
54
|
+
const result = jsQR(pixelData, info.width, info.height, { inversionAttempts: 'attemptBoth' });
|
|
55
|
+
return result ? result.data : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function qrcodeDecodeHandler() {
|
|
59
|
+
const imagePath = await getImagePath();
|
|
60
|
+
if (!imagePath) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const decoded = await decodeQRCodeFromImage(imagePath);
|
|
66
|
+
if (!decoded) {
|
|
67
|
+
console.log(i18next.t('qrcodeDecode.notFound'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await outputText(decoded, { showPreview: false, copyToClipboard: true });
|
|
71
|
+
console.log(i18next.t('qrcodeDecode.success'));
|
|
72
|
+
console.log(`${i18next.t('qrcodeDecode.result')}\n${decoded}\n`);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(i18next.t('qrcodeDecode.decodeFail', { message: error.message }));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import i18next from '../i18n.js';
|
|
3
|
+
|
|
4
|
+
function getCharDisplayWidth(char) {
|
|
5
|
+
if (/[\u0000-\u001f\u007f]/.test(char)) {
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
if (
|
|
9
|
+
/[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/.test(char)
|
|
10
|
+
) {
|
|
11
|
+
return 2;
|
|
12
|
+
}
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDisplayWidth(text) {
|
|
17
|
+
let width = 0;
|
|
18
|
+
for (const char of text) {
|
|
19
|
+
width += getCharDisplayWidth(char);
|
|
20
|
+
}
|
|
21
|
+
return width;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fitText(text, width) {
|
|
25
|
+
let result = '';
|
|
26
|
+
let currentWidth = 0;
|
|
27
|
+
for (const char of text) {
|
|
28
|
+
const charWidth = getCharDisplayWidth(char);
|
|
29
|
+
if (currentWidth + charWidth > width) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
result += char;
|
|
33
|
+
currentWidth += charWidth;
|
|
34
|
+
}
|
|
35
|
+
const remaining = width - getDisplayWidth(result);
|
|
36
|
+
if (remaining > 0) {
|
|
37
|
+
return `${result}${' '.repeat(remaining)}`;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatSeconds(seconds) {
|
|
43
|
+
const hours = Math.floor(seconds / 3600);
|
|
44
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
45
|
+
const secs = seconds % 60;
|
|
46
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderStopwatchBlock(elapsedSeconds) {
|
|
50
|
+
const innerWidth = 34;
|
|
51
|
+
const top = `┌${'─'.repeat(innerWidth + 2)}┐`;
|
|
52
|
+
const bottom = `└${'─'.repeat(innerWidth + 2)}┘`;
|
|
53
|
+
const line = (text) => `│ ${fitText(text, innerWidth)} │`;
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
top,
|
|
57
|
+
line(i18next.t('stopwatch.title')),
|
|
58
|
+
line(''),
|
|
59
|
+
line(`${i18next.t('stopwatch.elapsed')}${formatSeconds(elapsedSeconds)}`),
|
|
60
|
+
line(`${i18next.t('stopwatch.status')}${i18next.t('stopwatch.statusRunning')}`),
|
|
61
|
+
line(i18next.t('stopwatch.exitTip')),
|
|
62
|
+
bottom
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function stopwatchHandler() {
|
|
67
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
68
|
+
console.log(i18next.t('stopwatch.nonTtyTip'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await new Promise((resolve) => {
|
|
73
|
+
let renderedLineCount = 0;
|
|
74
|
+
let elapsedSeconds = 0;
|
|
75
|
+
let shouldExitApp = false;
|
|
76
|
+
|
|
77
|
+
const render = () => {
|
|
78
|
+
const lines = renderStopwatchBlock(elapsedSeconds);
|
|
79
|
+
if (renderedLineCount > 0) {
|
|
80
|
+
readline.moveCursor(process.stdout, 0, -renderedLineCount);
|
|
81
|
+
readline.cursorTo(process.stdout, 0);
|
|
82
|
+
readline.clearScreenDown(process.stdout);
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
85
|
+
renderedLineCount = lines.length;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
clearInterval(timer);
|
|
90
|
+
process.stdin.off('data', onData);
|
|
91
|
+
process.stdin.setRawMode(false);
|
|
92
|
+
process.stdin.pause();
|
|
93
|
+
readline.cursorTo(process.stdout, 0);
|
|
94
|
+
readline.clearScreenDown(process.stdout);
|
|
95
|
+
process.stdout.write('\n');
|
|
96
|
+
if (shouldExitApp) {
|
|
97
|
+
console.log(i18next.t('menu.bye'));
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onData = (chunk) => {
|
|
104
|
+
const key = chunk.toString().toLowerCase();
|
|
105
|
+
if (key === '0') {
|
|
106
|
+
shouldExitApp = true;
|
|
107
|
+
cleanup();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key === '\r' || key === 'q' || key === '\u0003') {
|
|
111
|
+
cleanup();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const timer = setInterval(() => {
|
|
116
|
+
elapsedSeconds += 1;
|
|
117
|
+
render();
|
|
118
|
+
}, 1000);
|
|
119
|
+
|
|
120
|
+
process.stdin.setRawMode(true);
|
|
121
|
+
process.stdin.resume();
|
|
122
|
+
process.stdin.on('data', onData);
|
|
123
|
+
render();
|
|
124
|
+
});
|
|
125
|
+
}
|
package/src/commands/url.js
CHANGED
|
@@ -7,7 +7,10 @@ export async function urlHandler() {
|
|
|
7
7
|
const mode = await selectFromMenu(i18next.t('url.title'), [
|
|
8
8
|
{ name: i18next.t('url.encode'), value: 'encode' },
|
|
9
9
|
{ name: i18next.t('url.decode'), value: 'decode' }
|
|
10
|
-
]);
|
|
10
|
+
], true, i18next.t('common.back'));
|
|
11
|
+
if (mode === '__BACK__') {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
11
14
|
|
|
12
15
|
const { input } = await inquirer.prompt([
|
|
13
16
|
{
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,9 @@ import { imgSplitHandler } from './commands/imgSplit.js';
|
|
|
14
14
|
import { imgMergeHandler } from './commands/imgMerge.js';
|
|
15
15
|
import { timeFormatHandler } from './commands/timeFormat.js';
|
|
16
16
|
import { timeCalcHandler } from './commands/timeCalc.js';
|
|
17
|
+
import { clockHandler } from './commands/clock.js';
|
|
18
|
+
import { countdownHandler } from './commands/countdown.js';
|
|
19
|
+
import { stopwatchHandler } from './commands/stopwatch.js';
|
|
17
20
|
import { mockHandler } from './commands/mock.js';
|
|
18
21
|
import { uuidHandler } from './commands/uuid.js';
|
|
19
22
|
import { pinyinHandler } from './commands/pinyin.js';
|
|
@@ -23,6 +26,7 @@ import { variableFormatHandler } from './commands/variableFormat.js';
|
|
|
23
26
|
import { jsonFormatHandler } from './commands/jsonFormat.js';
|
|
24
27
|
import { hashingHandler } from './commands/hashing.js';
|
|
25
28
|
import { qrcodeHandler } from './commands/qrcode.js';
|
|
29
|
+
import { qrcodeDecodeHandler } from './commands/qrcodeDecode.js';
|
|
26
30
|
import { specialCharsHandler } from './commands/specialChars.js';
|
|
27
31
|
import { emojiHandler } from './commands/emoji.js';
|
|
28
32
|
import { htmlEntitiesHandler } from './commands/htmlEntities.js';
|
|
@@ -68,6 +72,7 @@ function getFeatures() {
|
|
|
68
72
|
{ name: i18next.t('menu.features.imgCompress'), value: 'imgCompress' },
|
|
69
73
|
{ name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
|
|
70
74
|
{ name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
|
|
75
|
+
{ name: i18next.t('menu.features.qrcodeDecode'), value: 'qrcodeDecode' },
|
|
71
76
|
|
|
72
77
|
// Encode/Decode & Formatting
|
|
73
78
|
{ name: i18next.t('menu.features.url'), value: 'url' },
|
|
@@ -82,6 +87,9 @@ function getFeatures() {
|
|
|
82
87
|
// Utils (Time, Color, UUID, Hash)
|
|
83
88
|
{ name: i18next.t('menu.features.timeFormat'), value: 'timeFormat' },
|
|
84
89
|
{ name: i18next.t('menu.features.timeCalc'), value: 'timeCalc' },
|
|
90
|
+
{ name: i18next.t('menu.features.clock'), value: 'clock' },
|
|
91
|
+
{ name: i18next.t('menu.features.countdown'), value: 'countdown' },
|
|
92
|
+
{ name: i18next.t('menu.features.stopwatch'), value: 'stopwatch' },
|
|
85
93
|
{ name: i18next.t('menu.features.color'), value: 'color' },
|
|
86
94
|
{ name: i18next.t('menu.features.colorPreview'), value: 'colorPreview' },
|
|
87
95
|
{ name: i18next.t('menu.features.uuid'), value: 'uuid' },
|
|
@@ -308,6 +316,15 @@ async function handleAction(action) {
|
|
|
308
316
|
case 'timeCalc':
|
|
309
317
|
await timeCalcHandler();
|
|
310
318
|
break;
|
|
319
|
+
case 'clock':
|
|
320
|
+
await clockHandler();
|
|
321
|
+
break;
|
|
322
|
+
case 'countdown':
|
|
323
|
+
await countdownHandler();
|
|
324
|
+
break;
|
|
325
|
+
case 'stopwatch':
|
|
326
|
+
await stopwatchHandler();
|
|
327
|
+
break;
|
|
311
328
|
case 'mock':
|
|
312
329
|
await mockHandler();
|
|
313
330
|
break;
|
|
@@ -336,6 +353,9 @@ async function handleAction(action) {
|
|
|
336
353
|
case 'qrcode':
|
|
337
354
|
await qrcodeHandler();
|
|
338
355
|
break;
|
|
356
|
+
case 'qrcodeDecode':
|
|
357
|
+
await qrcodeDecodeHandler();
|
|
358
|
+
break;
|
|
339
359
|
case 'specialChars':
|
|
340
360
|
await specialCharsHandler();
|
|
341
361
|
break;
|
package/src/locales/en.js
CHANGED
|
@@ -50,6 +50,7 @@ export default {
|
|
|
50
50
|
imgCompress: 'Image Compression',
|
|
51
51
|
placeholderImg: 'Placeholder Image Generator',
|
|
52
52
|
qrcode: 'QR Code Generator',
|
|
53
|
+
qrcodeDecode: 'QR Code Reader',
|
|
53
54
|
url: 'URL Encode/Decode',
|
|
54
55
|
baseConvert: 'Number Base Converter',
|
|
55
56
|
base64: 'String Encode/Decode (Base64)',
|
|
@@ -60,6 +61,9 @@ export default {
|
|
|
60
61
|
pinyin: 'Chinese to Pinyin',
|
|
61
62
|
timeFormat: 'Time Format / Timestamp',
|
|
62
63
|
timeCalc: 'Time Calculation (Diff/Offset)',
|
|
64
|
+
clock: 'Console Clock',
|
|
65
|
+
countdown: 'Countdown Timer',
|
|
66
|
+
stopwatch: 'Stopwatch',
|
|
63
67
|
color: 'Color Converter (Hex <-> RGB)',
|
|
64
68
|
colorPreview: 'Color Preview',
|
|
65
69
|
uuid: 'Get UUID',
|
|
@@ -110,6 +114,18 @@ export default {
|
|
|
110
114
|
y2Prompt: 'Enter Y2 (px or %):',
|
|
111
115
|
result: 'Picked Color'
|
|
112
116
|
},
|
|
117
|
+
qrcodeDecode: {
|
|
118
|
+
selectInputMethod: 'Select QR image input method',
|
|
119
|
+
inputDialog: 'Select file (dialog)',
|
|
120
|
+
inputManual: 'Enter file path manually',
|
|
121
|
+
dialogUnavailable: 'File dialog is unavailable or canceled. Please enter the file path manually.',
|
|
122
|
+
enterPath: 'Enter QR code image path:',
|
|
123
|
+
notExist: 'File does not exist.',
|
|
124
|
+
notFound: 'No QR code detected. Please make sure the image is clear and contains a QR code.',
|
|
125
|
+
success: 'Recognition completed. Content copied to clipboard.',
|
|
126
|
+
result: 'Recognized content:',
|
|
127
|
+
decodeFail: 'Failed to decode QR code: {{message}}'
|
|
128
|
+
},
|
|
113
129
|
url: {
|
|
114
130
|
title: 'URL Encode/Decode',
|
|
115
131
|
encode: 'Encode',
|
|
@@ -118,6 +134,32 @@ export default {
|
|
|
118
134
|
result: 'Result:',
|
|
119
135
|
error: 'Error processing URL:'
|
|
120
136
|
},
|
|
137
|
+
clock: {
|
|
138
|
+
title: 'Console Clock',
|
|
139
|
+
start: 'Console clock started. Press Enter / Q to go back.',
|
|
140
|
+
now: 'Current time: ',
|
|
141
|
+
exitTip: 'Press Enter / Q to go back, 0 to exit app'
|
|
142
|
+
},
|
|
143
|
+
countdown: {
|
|
144
|
+
title: 'Countdown Timer',
|
|
145
|
+
inputPrompt: 'Enter countdown minutes:',
|
|
146
|
+
invalidMinutes: 'Please enter a number greater than or equal to 0',
|
|
147
|
+
remaining: 'Remaining: ',
|
|
148
|
+
total: 'Total: ',
|
|
149
|
+
status: 'Status: ',
|
|
150
|
+
statusCounting: 'Counting down',
|
|
151
|
+
statusOvertime: 'Overtime',
|
|
152
|
+
exitTip: 'Press Enter / Q to go back, 0 to exit app',
|
|
153
|
+
nonTtyTip: 'Current terminal does not support dynamic countdown. Target duration: {{value}}'
|
|
154
|
+
},
|
|
155
|
+
stopwatch: {
|
|
156
|
+
title: 'Stopwatch',
|
|
157
|
+
elapsed: 'Elapsed: ',
|
|
158
|
+
status: 'Status: ',
|
|
159
|
+
statusRunning: 'Running',
|
|
160
|
+
exitTip: 'Press Enter / Q to go back, 0 to exit app',
|
|
161
|
+
nonTtyTip: 'Current terminal does not support dynamic stopwatch.'
|
|
162
|
+
},
|
|
121
163
|
baseConvert: {
|
|
122
164
|
inputPrompt: 'Enter number:',
|
|
123
165
|
invalidInput: 'Please enter a valid number',
|
package/src/locales/zh.js
CHANGED
|
@@ -50,6 +50,7 @@ export default {
|
|
|
50
50
|
imgCompress: '图片压缩',
|
|
51
51
|
placeholderImg: '占位图生成器',
|
|
52
52
|
qrcode: '二维码生成器',
|
|
53
|
+
qrcodeDecode: '二维码识别',
|
|
53
54
|
url: 'URL 编码/解码',
|
|
54
55
|
baseConvert: '进制转换工具',
|
|
55
56
|
base64: '字符串 编码/解码 (Base64)',
|
|
@@ -60,6 +61,9 @@ export default {
|
|
|
60
61
|
pinyin: '汉字转拼音',
|
|
61
62
|
timeFormat: '时间格式化 / 时间戳',
|
|
62
63
|
timeCalc: '时间计算 (差值/偏移)',
|
|
64
|
+
clock: '控制台时钟',
|
|
65
|
+
countdown: '倒计时工具',
|
|
66
|
+
stopwatch: '计时工具',
|
|
63
67
|
color: '颜色转换 (Hex <-> RGB)',
|
|
64
68
|
colorPreview: '颜色预览',
|
|
65
69
|
uuid: '生成 UUID',
|
|
@@ -110,6 +114,18 @@ export default {
|
|
|
110
114
|
y2Prompt: '输入 Y2(px 或 %):',
|
|
111
115
|
result: '吸取结果'
|
|
112
116
|
},
|
|
117
|
+
qrcodeDecode: {
|
|
118
|
+
selectInputMethod: '选择二维码图片输入方式',
|
|
119
|
+
inputDialog: '选择文件(对话框)',
|
|
120
|
+
inputManual: '手动输入文件路径',
|
|
121
|
+
dialogUnavailable: '文件对话框不可用或已取消,请手动输入文件路径。',
|
|
122
|
+
enterPath: '请输入二维码图片路径:',
|
|
123
|
+
notExist: '文件不存在。',
|
|
124
|
+
notFound: '未识别到二维码,请确认图片清晰且包含二维码。',
|
|
125
|
+
success: '识别完成,内容已复制到剪贴板。',
|
|
126
|
+
result: '识别结果:',
|
|
127
|
+
decodeFail: '二维码识别失败: {{message}}'
|
|
128
|
+
},
|
|
113
129
|
url: {
|
|
114
130
|
title: 'URL 编码/解码',
|
|
115
131
|
encode: '编码',
|
|
@@ -118,6 +134,32 @@ export default {
|
|
|
118
134
|
result: '结果:',
|
|
119
135
|
error: '处理 URL 时出错:'
|
|
120
136
|
},
|
|
137
|
+
clock: {
|
|
138
|
+
title: '控制台时钟',
|
|
139
|
+
start: '控制台时钟已启动,按 Enter / Q 返回上一步。',
|
|
140
|
+
now: '当前时间: ',
|
|
141
|
+
exitTip: '按 Enter / Q 返回上一步,按 0 直接退出'
|
|
142
|
+
},
|
|
143
|
+
countdown: {
|
|
144
|
+
title: '倒计时工具',
|
|
145
|
+
inputPrompt: '请输入倒计时分钟数:',
|
|
146
|
+
invalidMinutes: '请输入大于等于 0 的数字分钟数',
|
|
147
|
+
remaining: '剩余时间: ',
|
|
148
|
+
total: '倒计总时: ',
|
|
149
|
+
status: '当前状态: ',
|
|
150
|
+
statusCounting: '倒计时中',
|
|
151
|
+
statusOvertime: '已超时',
|
|
152
|
+
exitTip: '按 Enter / Q 返回上一步,按 0 直接退出',
|
|
153
|
+
nonTtyTip: '当前终端不支持动态倒计时。目标时长: {{value}}'
|
|
154
|
+
},
|
|
155
|
+
stopwatch: {
|
|
156
|
+
title: '计时工具',
|
|
157
|
+
elapsed: '已计时: ',
|
|
158
|
+
status: '当前状态: ',
|
|
159
|
+
statusRunning: '计时中',
|
|
160
|
+
exitTip: '按 Enter / Q 返回上一步,按 0 直接退出',
|
|
161
|
+
nonTtyTip: '当前终端不支持动态计时。'
|
|
162
|
+
},
|
|
121
163
|
baseConvert: {
|
|
122
164
|
inputPrompt: '请输入数字:',
|
|
123
165
|
invalidInput: '请输入有效的数字',
|