xw-devtool-cli 1.0.29 → 1.0.33

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 CHANGED
@@ -24,6 +24,8 @@
24
24
  - **全屏十字辅助线**:显示跟随鼠标的全屏红色十字线,用于屏幕对齐和定位。
25
25
  - **屏幕测距**:点击屏幕上任意两点,测量其像素距离。
26
26
  - **屏幕文字标注**:在屏幕上点击并输入文字,进行临时标注。右键撤销,Delete 清空。
27
+ - **图片压缩**:支持文件或文件夹;文件夹可批量压缩其下所有图片(可选包含子文件夹)。支持 PNG/JPG/WebP,质量 1-100;输出文件名带时间戳并复制输出目录到剪贴板。
28
+ - 压缩完成后可选择自动打开输出目录(带返回上一步)。
27
29
  - **Mock 数据生成**:
28
30
  - 支持生成:英文段落 (Lorem Ipsum)、中文字符、中国居民身份证号、电子邮箱、URL、订单号、手机号、座机号。
29
31
  - 支持批量生成。
@@ -47,16 +49,25 @@
47
49
  - **HTML 实体工具**:支持 HTML 实体编码与解码 (如 `&` <-> `&amp;`)。
48
50
  - **Markdown 语法工具**:提供常用 Markdown 语法模板 (Headers, Lists, Tables, Code 等),一键复制。
49
51
  - **VS Code 代码段生成器**:将代码转换为 VS Code Snippet JSON,自动生成语法速查表注释。
52
+ - **Git 助手**: 标准化生成 Git 分支名 (feature/bugfix/custom) 和 Conventional Commits 提交日志。
50
53
  - **占位图生成**:自定义尺寸、背景色、文字颜色和格式生成占位图。
51
54
  - **便捷操作**:
52
55
  - 支持文件选择对话框 (Windows)。
53
56
  - 结果自动复制到剪贴板。
54
57
  - 支持从剪贴板自动读取输入(部分工具支持直接回车读取)。
55
58
  - 支持将大文本结果保存为文件。
56
- - **多语言支持 (i18n)**:
59
+ - **多语言支持 (i18n)**:
57
60
  - 支持 **中文** 和 **English**。
58
61
  - 可通过菜单中的 `Settings` 切换,或使用命令行参数:`xw-devtool --zh` / `xw-devtool --en`。
59
62
 
63
+ ## 🖥️ 系统支持说明(Windows 专属功能)
64
+
65
+ - 屏幕取色(来源:屏幕)仅支持 Windows;来源为图片的取色跨平台可用
66
+ - 全屏十字辅助线 仅支持 Windows
67
+ - 屏幕测距(像素)仅支持 Windows
68
+ - 屏幕文字标注 仅支持 Windows
69
+ - 文件选择/保存对话框 仅支持 Windows;在非 Windows 平台请手动输入文件路径或使用命令行默认路径
70
+
60
71
  ## 📦 安装
61
72
 
62
73
  ### 全局安装 (推荐)
package/README_EN.md CHANGED
@@ -19,6 +19,8 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
19
19
  - **Screen Crosshair**: Display a full-screen red crosshair following the mouse for alignment.
20
20
  - **Pixel Distance**: Measure pixel distance between two points on the screen.
21
21
  - **Screen Mark**: Click on the screen to add text annotations. Right-click to undo, Delete to clear.
22
+ - **Image Compression**: Compress a single file or an entire folder (optionally include subfolders). Supports PNG/JPG/WebP with quality 1–100. Output filenames include timestamps; the output directory is copied to clipboard.
23
+ - After compression, optionally open the output directory (includes a Back option).
22
24
  - **Mock Data**:
23
25
  - Generate: Lorem Ipsum, Chinese characters, ID cards, Emails, URLs, Order IDs, Phone numbers.
24
26
  - Supports batch generation.
@@ -42,6 +44,7 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
42
44
  - **HTML Entity**: Encode/Decode HTML entities.
43
45
  - **Markdown Snippets**: Common Markdown templates.
44
46
  - **VS Code Snippets**: Generate VS Code snippet JSON from code.
47
+ - **Git Helper**: Generate standardized Git branch names (feature/bugfix) and Conventional Commits messages.
45
48
  - **Convenience**:
46
49
  - File selection dialog (Windows).
47
50
  - Auto-copy results to clipboard.
@@ -51,6 +54,18 @@ Key features include: Base64 encoding/decoding, image format conversion, image <
51
54
  - Support **English** and **Chinese**.
52
55
  - Switch via **Settings** in the menu or use CLI flags: `xw-devtool --en` / `xw-devtool --zh`.
53
56
 
57
+ ## 💻 System Requirements
58
+
59
+ This tool is based on Node.js and supports multiple platforms, but some screen-interaction features are currently **Windows only** (due to reliance on PowerShell/WinForms):
60
+
61
+ - **Screen Color Pick** (Screen source)
62
+ - **Screen Crosshair**
63
+ - **Pixel Distance Tool**
64
+ - **Screen Mark Tool**
65
+ - **File Dialogs** (Select/Save file) - *On non-Windows systems, please input file paths manually.*
66
+
67
+ All other features (Image processing, Encoders, Mock data, Time, etc.) are fully cross-platform (Windows/macOS/Linux).
68
+
54
69
  ## 📦 Installation
55
70
 
56
71
  ### Global Installation (Recommended)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xw-devtool-cli",
3
- "version": "1.0.29",
3
+ "version": "1.0.33",
4
4
  "type": "module",
5
5
  "description": "基于node的开发者助手cli",
6
6
  "main": "index.js",
@@ -51,7 +51,15 @@
51
51
  "emoji",
52
52
  "markdown",
53
53
  "screen-mark",
54
- "pixel-distance"
54
+ "pixel-distance",
55
+ "image-compress",
56
+ "png-compress",
57
+ "jpg-compress",
58
+ "webp-compress",
59
+ "git",
60
+ "git-branch",
61
+ "commit-message",
62
+ "conventional-commits"
55
63
  ],
56
64
  "author": "npmxw",
57
65
  "license": "ISC",
@@ -0,0 +1,144 @@
1
+ import inquirer from 'inquirer';
2
+ import i18next from '../i18n.js';
3
+ import { copy } from '../utils/clipboard.js';
4
+ import { selectFromMenu } from '../utils/menu.js';
5
+
6
+ export async function gitHelperHandler() {
7
+ while (true) {
8
+ const action = await selectFromMenu(
9
+ i18next.t('gitHelper.selectAction'),
10
+ [
11
+ { name: i18next.t('gitHelper.branchName'), value: 'branch' },
12
+ { name: i18next.t('gitHelper.commitMessage'), value: 'commit' },
13
+ { name: i18next.t('common.back'), value: '__BACK__' }
14
+ ],
15
+ false
16
+ );
17
+ if (action === '__BACK__') return;
18
+ if (action === 'branch') {
19
+ await handleBranchName();
20
+ } else {
21
+ await handleCommitMessage();
22
+ }
23
+ return;
24
+ }
25
+ }
26
+
27
+ async function handleBranchName() {
28
+ const type = await selectFromMenu(
29
+ i18next.t('gitHelper.branchType'),
30
+ [
31
+ { name: 'feature', value: 'feature' },
32
+ { name: 'bugfix', value: 'bugfix' },
33
+ { name: 'hotfix', value: 'hotfix' },
34
+ { name: 'release', value: 'release' },
35
+ { name: 'docs', value: 'docs' },
36
+ { name: 'style', value: 'style' },
37
+ { name: 'refactor', value: 'refactor' },
38
+ { name: 'test', value: 'test' },
39
+ { name: 'chore', value: 'chore' },
40
+ { name: 'custom', value: 'custom' },
41
+ { name: i18next.t('common.back'), value: '__BACK__' }
42
+ ],
43
+ true,
44
+ i18next.t('common.back')
45
+ );
46
+ if (type === '__BACK__') return;
47
+ let customType = '';
48
+ if (type === 'custom') {
49
+ const { customType: ct } = await inquirer.prompt([
50
+ {
51
+ type: 'input',
52
+ name: 'customType',
53
+ message: i18next.t('gitHelper.customTypePrompt'),
54
+ validate: (input) => input.trim() ? true : i18next.t('gitHelper.required')
55
+ }
56
+ ]);
57
+ customType = ct;
58
+ }
59
+ const { ticketId, description } = await inquirer.prompt([
60
+ {
61
+ type: 'input',
62
+ name: 'ticketId',
63
+ message: i18next.t('gitHelper.ticketId'),
64
+ default: ''
65
+ },
66
+ {
67
+ type: 'input',
68
+ name: 'description',
69
+ message: i18next.t('gitHelper.description'),
70
+ validate: (input) => input.trim() ? true : i18next.t('gitHelper.required')
71
+ }
72
+ ]);
73
+
74
+ // Helper to slugify text
75
+ const slugify = (text) => {
76
+ return text.toString().toLowerCase()
77
+ .trim()
78
+ .replace(/\s+/g, '-') // Replace spaces with -
79
+ .replace(/[^\w\-]+/g, '') // Remove all non-word chars
80
+ .replace(/\-\-+/g, '-'); // Replace multiple - with single -
81
+ };
82
+
83
+ const descSlug = slugify(description);
84
+ const ticketPart = ticketId ? `${ticketId.trim()}-` : '';
85
+
86
+ const branchName = `${type === 'custom' ? customType : type}/${ticketPart}${descSlug}`;
87
+
88
+ console.log(`\n${i18next.t('gitHelper.result')}: ${branchName}\n`);
89
+ await copy(branchName);
90
+ }
91
+
92
+ async function handleCommitMessage() {
93
+ const type = await selectFromMenu(
94
+ i18next.t('gitHelper.commitType'),
95
+ [
96
+ { name: 'feat: A new feature', value: 'feat' },
97
+ { name: 'fix: A bug fix', value: 'fix' },
98
+ { name: 'docs: Documentation only changes', value: 'docs' },
99
+ { name: 'style: Changes that do not affect the meaning of the code', value: 'style' },
100
+ { name: 'refactor: A code change that neither fixes a bug nor adds a feature', value: 'refactor' },
101
+ { name: 'perf: A code change that improves performance', value: 'perf' },
102
+ { name: 'test: Adding missing tests or correcting existing tests', value: 'test' },
103
+ { name: 'build: Changes that affect the build system or external dependencies', value: 'build' },
104
+ { name: 'ci: Changes to our CI configuration files and scripts', value: 'ci' },
105
+ { name: 'chore: Other changes that don\'t modify src or test files', value: 'chore' },
106
+ { name: 'revert: Reverts a previous commit', value: 'revert' }
107
+ ]
108
+ );
109
+ const { scope, subject } = await inquirer.prompt([
110
+ {
111
+ type: 'input',
112
+ name: 'scope',
113
+ message: i18next.t('gitHelper.scope'),
114
+ },
115
+ {
116
+ type: 'input',
117
+ name: 'subject',
118
+ message: i18next.t('gitHelper.subject'),
119
+ validate: (input) => input.trim() ? true : i18next.t('gitHelper.required')
120
+ }
121
+ ]);
122
+ const { body, footer } = await inquirer.prompt([
123
+ {
124
+ type: 'editor',
125
+ name: 'body',
126
+ message: i18next.t('gitHelper.body'),
127
+ },
128
+ {
129
+ type: 'input',
130
+ name: 'footer',
131
+ message: i18next.t('gitHelper.footer'),
132
+ }
133
+ ]);
134
+
135
+ const scopePart = scope ? `(${scope})` : '';
136
+ const head = `${type}${scopePart}: ${subject}`;
137
+ const bodyPart = body ? `\n\n${body.trim()}` : '';
138
+ const footerPart = footer ? `\n\n${footer.trim()}` : '';
139
+
140
+ const commitMsg = `${head}${bodyPart}${footerPart}`;
141
+
142
+ console.log(`\n${i18next.t('gitHelper.result')}:\n----------------\n${commitMsg}\n----------------\n`);
143
+ await copy(commitMsg);
144
+ }
@@ -0,0 +1,230 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import sharp from 'sharp';
4
+ import inquirer from 'inquirer';
5
+ import i18next from '../i18n.js';
6
+ import { selectFromMenu } from '../utils/menu.js';
7
+ import { selectFile, selectFolder } from '../utils/fileDialog.js';
8
+ import dayjs from 'dayjs';
9
+ import { spawn, exec } from 'child_process';
10
+ import { copy } from '../utils/clipboard.js';
11
+
12
+ const SUPPORTED_EXT = ['.png', '.jpg', '.jpeg', '.webp'];
13
+
14
+ function isDirPath(p) {
15
+ try {
16
+ const st = fs.statSync(p);
17
+ return st.isDirectory();
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ function listImagesInDir(dir, recursive) {
24
+ const results = [];
25
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const full = path.join(dir, entry.name);
28
+ const isDir = entry.isDirectory() || entry.isSymbolicLink() ? isDirPath(full) : false;
29
+ if (isDir) {
30
+ if (recursive) {
31
+ results.push(...listImagesInDir(full, recursive));
32
+ }
33
+ } else {
34
+ const ext = path.extname(entry.name).toLowerCase();
35
+ if (SUPPORTED_EXT.includes(ext)) {
36
+ results.push(full);
37
+ }
38
+ }
39
+ }
40
+ return results;
41
+ }
42
+
43
+ async function pickInput() {
44
+ const options = [
45
+ { name: i18next.t('imgCompress.sourceFileDialog'), value: 'fileDialog' },
46
+ { name: i18next.t('imgCompress.sourceFolderDialog'), value: 'folderDialog' },
47
+ { name: i18next.t('imgCompress.sourceFileManual'), value: 'fileManual' },
48
+ { name: i18next.t('imgCompress.sourceFolderManual'), value: 'folderManual' }
49
+ ];
50
+ const method = await selectFromMenu(i18next.t('imgCompress.selectSource'), options, true, i18next.t('common.back'));
51
+ if (method === '__BACK__') return { back: true };
52
+ if (method === 'fileDialog') {
53
+ const filePath = selectFile('Image Files|*.png;*.jpg;*.jpeg;*.webp');
54
+ if (!filePath) {
55
+ console.log(i18next.t('imgCompress.dialogFail'));
56
+ return await pickInput();
57
+ }
58
+ return { type: 'file', path: filePath };
59
+ }
60
+ if (method === 'folderDialog') {
61
+ const folderPath = selectFolder();
62
+ if (!folderPath) {
63
+ console.log(i18next.t('imgCompress.dialogFail'));
64
+ return await pickInput();
65
+ }
66
+ return { type: 'folder', path: folderPath };
67
+ }
68
+ if (method === 'fileManual') {
69
+ const { filePath } = await inquirer.prompt([
70
+ { type: 'input', name: 'filePath', message: i18next.t('imgCompress.enterFilePath') }
71
+ ]);
72
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
73
+ console.log(i18next.t('imgCompress.notExist'));
74
+ return await pickInput();
75
+ }
76
+ return { type: 'file', path: filePath };
77
+ }
78
+ if (method === 'folderManual') {
79
+ const { folderPath } = await inquirer.prompt([
80
+ { type: 'input', name: 'folderPath', message: i18next.t('imgCompress.enterFolderPath') }
81
+ ]);
82
+ if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
83
+ console.log(i18next.t('imgCompress.notExist'));
84
+ return await pickInput();
85
+ }
86
+ return { type: 'folder', path: folderPath };
87
+ }
88
+ return { back: true };
89
+ }
90
+
91
+ async function pickQuality() {
92
+ const presets = [
93
+ { name: '90 (High)', value: 90 },
94
+ { name: '80 (Recommended)', value: 80 },
95
+ { name: '70 (Smaller size)', value: 70 },
96
+ { name: i18next.t('imgCompress.customQuality'), value: 'custom' }
97
+ ];
98
+ const qSel = await selectFromMenu(i18next.t('imgCompress.selectQuality'), presets, true, i18next.t('common.back'));
99
+ if (qSel === '__BACK__') return '__BACK__';
100
+ if (qSel === 'custom') {
101
+ const { q } = await inquirer.prompt([
102
+ { type: 'input', name: 'q', message: i18next.t('imgCompress.inputQuality'), default: '80', validate: (v) => (/^\d+$/.test(v) && +v >= 1 && +v <= 100) ? true : i18next.t('imgCompress.qualityInvalid') }
103
+ ]);
104
+ return parseInt(q);
105
+ }
106
+ return qSel;
107
+ }
108
+
109
+ async function pickRecursive() {
110
+ const choice = await selectFromMenu(i18next.t('imgCompress.includeSubfolders'), [
111
+ { name: i18next.t('imgCompress.yes'), value: true },
112
+ { name: i18next.t('imgCompress.no'), value: false }
113
+ ], true, i18next.t('common.back'));
114
+ if (choice === '__BACK__') return '__BACK__';
115
+ return choice;
116
+ }
117
+
118
+ async function pickPreserveStructure() {
119
+ const choice = await selectFromMenu(i18next.t('imgCompress.preservePrompt'), [
120
+ { name: i18next.t('imgCompress.yes'), value: true },
121
+ { name: i18next.t('imgCompress.no'), value: false }
122
+ ], true, i18next.t('common.back'));
123
+ if (choice === '__BACK__') return '__BACK__';
124
+ return choice;
125
+ }
126
+
127
+ async function compressOne(inputPath, quality, outDir, baseDir, preserveStructure) {
128
+ const ext = path.extname(inputPath).toLowerCase();
129
+ const name = path.basename(inputPath, ext);
130
+ const ts = dayjs().format('YYYYMMDD_HHmmss');
131
+ let targetDir = outDir;
132
+ if (preserveStructure && baseDir) {
133
+ const rel = path.relative(baseDir, path.dirname(inputPath));
134
+ targetDir = path.join(outDir, rel);
135
+ fs.mkdirSync(targetDir, { recursive: true });
136
+ }
137
+ const outPath = path.join(targetDir, `${name}_compressed_${ts}${ext}`);
138
+ const image = sharp(inputPath, { failOnError: false });
139
+ if (ext === '.jpg' || ext === '.jpeg') {
140
+ await image.jpeg({ quality, mozjpeg: true }).toFile(outPath);
141
+ } else if (ext === '.png') {
142
+ // Map quality (1-100) to compressionLevel (0-9) roughly
143
+ const level = Math.max(0, Math.min(9, Math.round((100 - quality) / 10)));
144
+ await image.png({ quality, compressionLevel: level }).toFile(outPath);
145
+ } else if (ext === '.webp') {
146
+ await image.webp({ quality }).toFile(outPath);
147
+ } else {
148
+ return null;
149
+ }
150
+ return outPath;
151
+ }
152
+
153
+ export async function imgCompressHandler() {
154
+ const input = await pickInput();
155
+ if (input.back) return;
156
+ const quality = await pickQuality();
157
+ if (quality === '__BACK__') return;
158
+ let files = [];
159
+ let baseDir = '';
160
+ if (input.type === 'file') {
161
+ files = [input.path];
162
+ baseDir = path.dirname(input.path);
163
+ } else {
164
+ const recursive = await pickRecursive();
165
+ if (recursive === '__BACK__') return;
166
+ files = listImagesInDir(input.path, recursive);
167
+ baseDir = input.path;
168
+ console.log(`${i18next.t('imgCompress.scanResult')}: ${files.length}`);
169
+ if (files.length > 0) {
170
+ const preview = files.slice(0, Math.min(files.length, 5));
171
+ preview.forEach((p, i) => console.log(`${i + 1}. ${p}`));
172
+ if (files.length > preview.length) console.log('...');
173
+ }
174
+ }
175
+ if (files.length === 0) {
176
+ console.log(i18next.t('imgCompress.noImages'));
177
+ return;
178
+ }
179
+ const outDir = path.resolve(baseDir, `compressed_${dayjs().format('YYYYMMDD_HHmmss')}`);
180
+ fs.mkdirSync(outDir, { recursive: true });
181
+ let preserveStructure = false;
182
+ if (input.type === 'folder') {
183
+ const preserveChoice = await pickPreserveStructure();
184
+ if (preserveChoice === '__BACK__') return;
185
+ preserveStructure = preserveChoice;
186
+ }
187
+ const results = [];
188
+ for (const f of files) {
189
+ try {
190
+ const out = await compressOne(f, quality, outDir, baseDir, preserveStructure);
191
+ if (out) results.push(out);
192
+ } catch (e) {
193
+ console.error(`${i18next.t('imgCompress.fail')}: ${f} -> ${e.message}`);
194
+ }
195
+ }
196
+ console.log(`\n${i18next.t('imgCompress.done')} ${results.length}/${files.length}`);
197
+ console.log(`${i18next.t('imgCompress.outputDir')}: ${outDir}\n`);
198
+ await copy(`${i18next.t('imgCompress.outputDir')}: ${outDir}`);
199
+
200
+ const openChoice = await selectFromMenu(
201
+ i18next.t('imgCompress.openPrompt'),
202
+ [
203
+ { name: i18next.t('imgCompress.yes'), value: true },
204
+ { name: i18next.t('imgCompress.no'), value: false }
205
+ ],
206
+ true,
207
+ i18next.t('common.back')
208
+ );
209
+ if (openChoice !== '__BACK__' && openChoice === true) {
210
+ try {
211
+ openFolder(outDir);
212
+ console.log(i18next.t('imgCompress.opened'));
213
+ } catch (e) {
214
+ console.error(`${i18next.t('imgCompress.openFail')}: ${e.message}`);
215
+ }
216
+ }
217
+ }
218
+
219
+ function openFolder(dir) {
220
+ const platform = process.platform;
221
+ if (platform === 'win32') {
222
+ exec(`start "" "${dir}"`);
223
+ } else if (platform === 'darwin') {
224
+ const cp = spawn('open', [dir], { detached: true });
225
+ cp.unref();
226
+ } else {
227
+ const cp = spawn('xdg-open', [dir], { detached: true });
228
+ cp.unref();
229
+ }
230
+ }
@@ -123,14 +123,10 @@ public class DistanceForm : Form {
123
123
  double dist = Math.Sqrt(Math.Pow(target.X - p1.X, 2) + Math.Pow(target.Y - p1.Y, 2));
124
124
  string text = String.Format("{0:F2} px", dist);
125
125
 
126
- // Draw text at midpoint
127
- Point mid = new Point((p1.X + target.X) / 2, (p1.Y + target.Y) / 2);
128
126
  SizeF size = g.MeasureString(text, font);
129
-
130
- // Ensure text doesn't go off screen
131
- float txtX = mid.X + 10;
132
- float txtY = mid.Y + 10;
133
-
127
+ float pad = 16;
128
+ float txtX = this.ClientSize.Width - (float)size.Width - pad;
129
+ float txtY = this.ClientSize.Height - (float)size.Height - pad;
134
130
  g.FillRectangle(textBg, txtX - 2, txtY - 2, size.Width + 4, size.Height + 4);
135
131
  g.DrawString(text, font, Brushes.White, txtX, txtY);
136
132
 
package/src/index.js CHANGED
@@ -32,6 +32,8 @@ import { colorPickHandler } from './commands/colorPick.js';
32
32
  import { crosshairHandler } from './commands/crosshair.js';
33
33
  import { pixelDistanceHandler } from './commands/pixelDistance.js';
34
34
  import { screenMarkHandler } from './commands/screenMark.js';
35
+ import { gitHelperHandler } from './commands/gitHelper.js';
36
+ import { imgCompressHandler } from './commands/imgCompress.js';
35
37
 
36
38
  process.on('SIGINT', () => {
37
39
  console.log(`\n${i18next.t('menu.bye')}`);
@@ -59,6 +61,7 @@ function getFeatures() {
59
61
  { name: i18next.t('menu.features.crosshair'), value: 'crosshair' },
60
62
  { name: i18next.t('menu.features.pixelDistance'), value: 'pixelDistance' },
61
63
  { name: i18next.t('menu.features.screenMark'), value: 'screenMark' },
64
+ { name: i18next.t('menu.features.imgCompress'), value: 'imgCompress' },
62
65
  { name: i18next.t('menu.features.placeholderImg'), value: 'placeholderImg' },
63
66
  { name: i18next.t('menu.features.qrcode'), value: 'qrcode' },
64
67
 
@@ -86,6 +89,7 @@ function getFeatures() {
86
89
  { name: i18next.t('menu.features.emoji'), value: 'emoji' },
87
90
  { name: i18next.t('menu.features.markdown'), value: 'markdown' },
88
91
  { name: i18next.t('menu.features.vscodeSnippet'), value: 'vscodeSnippet' },
92
+ { name: i18next.t('menu.features.gitHelper'), value: 'gitHelper' },
89
93
 
90
94
  // Settings
91
95
  { name: i18next.t('menu.features.settings'), value: 'settings' }
@@ -255,12 +259,18 @@ async function handleAction(action) {
255
259
  case 'placeholderImg':
256
260
  await placeholderImgHandler();
257
261
  break;
262
+ case 'imgCompress':
263
+ await imgCompressHandler();
264
+ break;
258
265
  case 'markdown':
259
266
  await markdownHandler();
260
267
  break;
261
268
  case 'vscodeSnippet':
262
269
  await vscodeSnippetHandler();
263
270
  break;
271
+ case 'gitHelper':
272
+ await gitHelperHandler();
273
+ break;
264
274
  case 'settings':
265
275
  await handleSettings();
266
276
  break;
package/src/locales/en.js CHANGED
@@ -2,6 +2,22 @@ export default {
2
2
  common: {
3
3
  back: 'Back to previous step'
4
4
  },
5
+ gitHelper: {
6
+ selectAction: 'Select Action',
7
+ branchName: 'Generate Git Branch Name',
8
+ commitMessage: 'Generate Git Commit Message',
9
+ branchType: 'Select Branch Type',
10
+ customTypePrompt: 'Enter Custom Type:',
11
+ ticketId: 'Ticket ID (Optional):',
12
+ description: 'Description (Short):',
13
+ required: 'Required',
14
+ result: 'Result',
15
+ commitType: 'Select Commit Type',
16
+ scope: 'Scope (Optional):',
17
+ subject: 'Subject (Short summary):',
18
+ body: 'Body (Detailed description, Optional, opens editor):',
19
+ footer: 'Footer (Breaking changes/Issue refs, Optional):'
20
+ },
5
21
  pixelDistance: {
6
22
  notSupported: 'Pixel distance tool not supported on this OS.',
7
23
  startPrompt: 'Pixel distance tool started. Click 2 points to measure. Right-click to reset. ESC to exit.'
@@ -25,6 +41,7 @@ export default {
25
41
  crosshair: 'Screen Crosshair',
26
42
  pixelDistance: 'Pixel Distance Tool',
27
43
  screenMark: 'Screen Mark Tool',
44
+ imgCompress: 'Image Compression',
28
45
  placeholderImg: 'Placeholder Image Generator',
29
46
  qrcode: 'QR Code Generator',
30
47
  url: 'URL Encode/Decode',
@@ -46,6 +63,7 @@ export default {
46
63
  emoji: 'Emoji Picker',
47
64
  markdown: 'Markdown Snippets',
48
65
  vscodeSnippet: 'VS Code Snippet Generator',
66
+ gitHelper: 'Git Helper (Branch/Commit Template)',
49
67
  settings: 'Settings / Language'
50
68
  }
51
69
  },
@@ -111,5 +129,32 @@ export default {
111
129
  hLines: 'Horizontal split lines (e.g. 10 20 30 or 10% 20%):',
112
130
  vLines: 'Vertical split lines (e.g. 10 20 30 or 10% 20%):',
113
131
  success: 'Successfully split image into {{count}} parts. Saved to {{path}}'
132
+ },
133
+ imgCompress: {
134
+ selectSource: 'Select input method',
135
+ sourceFileDialog: 'Select file (dialog)',
136
+ sourceFolderDialog: 'Select folder (dialog)',
137
+ sourceFileManual: 'Enter file path manually',
138
+ sourceFolderManual: 'Enter folder path manually',
139
+ dialogFail: 'Dialog not available or canceled. Please retry or use manual input.',
140
+ enterFilePath: 'Enter image file path:',
141
+ enterFolderPath: 'Enter folder path:',
142
+ notExist: 'Path does not exist or type mismatch.',
143
+ selectQuality: 'Select compression quality (1-100)',
144
+ customQuality: 'Custom quality',
145
+ inputQuality: 'Enter quality (1-100):',
146
+ qualityInvalid: 'Please enter an integer between 1 and 100',
147
+ includeSubfolders: 'Include subfolders?',
148
+ yes: 'Yes',
149
+ no: 'No',
150
+ noImages: 'No images found to compress.',
151
+ fail: 'Compression failed',
152
+ done: 'Compression finished. Success count',
153
+ outputDir: 'Output Directory',
154
+ openPrompt: 'Open output directory?',
155
+ openFail: 'Failed to open directory',
156
+ opened: 'Opened output directory',
157
+ scanResult: 'Images found',
158
+ preservePrompt: 'Preserve original directory structure in output?'
114
159
  }
115
160
  };
package/src/locales/zh.js CHANGED
@@ -6,6 +6,22 @@ export default {
6
6
  notSupported: '当前系统不支持屏幕辅助线。',
7
7
  startPrompt: '屏幕十字辅助线已启动。按任意键退出...'
8
8
  },
9
+ gitHelper: {
10
+ selectAction: '请选择操作',
11
+ branchName: '生成 Git 分支名',
12
+ commitMessage: '生成 Git 提交日志 (Conventional Commits)',
13
+ branchType: '选择分支类型',
14
+ customTypePrompt: '请输入自定义类型:',
15
+ ticketId: 'Ticket ID (可选):',
16
+ description: '描述 (简短):',
17
+ required: '此项必填',
18
+ result: '生成结果',
19
+ commitType: '选择提交类型',
20
+ scope: 'Scope (范围, 可选):',
21
+ subject: 'Subject (简述):',
22
+ body: 'Body (详细描述, 可选, 会打开编辑器):',
23
+ footer: 'Footer (Breaking changes/Issue refs, 可选):'
24
+ },
9
25
  pixelDistance: {
10
26
  notSupported: '当前系统不支持屏幕测距。',
11
27
  startPrompt: '屏幕测距工具已启动。点击两点测量距离。右键重置。ESC 退出。'
@@ -25,6 +41,7 @@ export default {
25
41
  crosshair: '全屏十字辅助线',
26
42
  pixelDistance: '屏幕测距 (像素)',
27
43
  screenMark: '屏幕文字标注',
44
+ imgCompress: '图片压缩',
28
45
  placeholderImg: '占位图生成器',
29
46
  qrcode: '二维码生成器',
30
47
  url: 'URL 编码/解码',
@@ -46,6 +63,7 @@ export default {
46
63
  emoji: 'Emoji 选择器',
47
64
  markdown: 'Markdown 片段',
48
65
  vscodeSnippet: 'VS Code 代码片段生成',
66
+ gitHelper: 'Git 助手 (分支/提交模板)',
49
67
  settings: '设置 / 语言 (Settings)'
50
68
  }
51
69
  },
@@ -111,5 +129,32 @@ export default {
111
129
  hLines: '水平分割线 (例如 10 20 30 或 10% 20%):',
112
130
  vLines: '垂直分割线 (例如 10 20 30 或 10% 20%):',
113
131
  success: '成功将图片分割为 {{count}} 份。保存至 {{path}}'
132
+ },
133
+ imgCompress: {
134
+ selectSource: '选择输入方式',
135
+ sourceFileDialog: '选择文件(对话框)',
136
+ sourceFolderDialog: '选择文件夹(对话框)',
137
+ sourceFileManual: '手动输入文件路径',
138
+ sourceFolderManual: '手动输入文件夹路径',
139
+ dialogFail: '对话框不可用或取消,请重试或改用手动输入。',
140
+ enterFilePath: '请输入图片文件路径:',
141
+ enterFolderPath: '请输入文件夹路径:',
142
+ notExist: '路径不存在或类型不匹配。',
143
+ selectQuality: '选择压缩质量 (1-100)',
144
+ customQuality: '自定义质量',
145
+ inputQuality: '请输入质量 (1-100):',
146
+ qualityInvalid: '请输入 1-100 的整数',
147
+ includeSubfolders: '是否包含子文件夹',
148
+ yes: '是',
149
+ no: '否',
150
+ noImages: '未找到可压缩的图片。',
151
+ fail: '压缩失败',
152
+ done: '压缩完成,成功数量',
153
+ outputDir: '输出目录',
154
+ openPrompt: '是否打开输出目录',
155
+ openFail: '打开目录失败',
156
+ opened: '已打开输出目录',
157
+ scanResult: '扫描到图片数量',
158
+ preservePrompt: '递归时是否按原始目录结构输出'
114
159
  }
115
160
  };
@@ -27,3 +27,15 @@ export function saveFile(defaultName = 'output.txt', filter = 'All Files|*.*') {
27
27
  return null;
28
28
  }
29
29
  }
30
+
31
+ export function selectFolder() {
32
+ if (!isWindows()) return null;
33
+ try {
34
+ const cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $fb = New-Object System.Windows.Forms.FolderBrowserDialog; if($fb.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){ $bytes = [System.Text.Encoding]::UTF8.GetBytes($fb.SelectedPath); Write-Output ([Convert]::ToBase64String($bytes)) }"`;
35
+ const buf = execSync(cmd, { stdio: ['pipe', 'pipe', 'ignore'] });
36
+ const output = Buffer.isBuffer(buf) ? buf.toString('utf8').trim() : String(buf).trim();
37
+ return output ? Buffer.from(output, 'base64').toString('utf8') : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }