mjpic 1.0.5 → 1.0.7

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.
Files changed (20) hide show
  1. package/.vercel/project.json +1 -0
  2. package/README.md +144 -54
  3. package/api/cli.ts +44 -5
  4. package/deploy/mjpic-start.desktop +20 -0
  5. package/deploy/node_install.sh +111 -0
  6. package/deploy//344/275/277/347/224/250/350/257/264/346/230/216.txt +17 -0
  7. package/dist/cli/cli.js +38 -4
  8. package/dist/client/index.html +2 -1
  9. package/index.html +2 -1
  10. package/package.json +5 -3
  11. package/scripts/get-random-port.js +40 -0
  12. package/src/components/dialogs/SaveDialog.tsx +1 -1
  13. package/src/components/layout/Header.tsx +43 -18
  14. package/src/components/layout/RightPanel.tsx +0 -21
  15. package/vite.config.ts +3 -1
  16. package/tmp/guangxi.jpg +0 -0
  17. package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.47.45_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/351/242/204/350/256/276/345/260/272/345/257/270.jpg +0 -0
  18. package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.47.51_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/346/211/213/345/267/245/350/276/223/345/205/245/345/260/272/345/257/270.jpg +0 -0
  19. package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.54.56_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/346/267/273/345/212/240/345/270/270/347/224/250/345/260/272/345/257/270.jpg +0 -0
  20. package//345/217/202/350/200/203/345/233/276/347/211/207//346/210/252/345/261/2172026-02-18 16.55.11_/345/233/276/347/211/207/345/260/272/345/257/270/350/260/203/350/212/202_/345/210/240/351/231/244/345/270/270/347/224/250/345/260/272/345/257/270.jpg +0 -0
@@ -0,0 +1 @@
1
+ {"neverMindDeployCard":true}
package/README.md CHANGED
@@ -1,57 +1,147 @@
1
- # React + TypeScript + Vite
2
-
3
- This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
-
5
- Currently, two official plugins are available:
6
-
7
- - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
- - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
-
10
- ## Expanding the ESLint configuration
11
-
12
- If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13
-
14
- ```js
15
- export default tseslint.config({
16
- extends: [
17
- // Remove ...tseslint.configs.recommended and replace with this
18
- ...tseslint.configs.recommendedTypeChecked,
19
- // Alternatively, use this for stricter rules
20
- ...tseslint.configs.strictTypeChecked,
21
- // Optionally, add this for stylistic rules
22
- ...tseslint.configs.stylisticTypeChecked,
23
- ],
24
- languageOptions: {
25
- // other options...
26
- parserOptions: {
27
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
28
- tsconfigRootDir: import.meta.dirname,
29
- },
30
- },
31
- })
1
+ # 敏捷图片 (mjpic)
2
+
3
+ 敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。
4
+
5
+ ## 特性
6
+
7
+ - **轻量级**:无需安装庞大的软件,即开即用。
8
+ - **本地处理**:图片不上传服务器,保护隐私。
9
+ - **功能丰富**:支持裁剪、旋转、尺寸调整、边框添加等常用功能。
10
+ - **CLI 支持**:提供命令行工具,方便快速打开和处理图片。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install -g mjpic
32
16
  ```
33
17
 
34
- You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
35
-
36
- ```js
37
- // eslint.config.js
38
- import reactX from 'eslint-plugin-react-x'
39
- import reactDom from 'eslint-plugin-react-dom'
40
-
41
- export default tseslint.config({
42
- extends: [
43
- // other configs...
44
- // Enable lint rules for React
45
- reactX.configs['recommended-typescript'],
46
- // Enable lint rules for React DOM
47
- reactDom.configs.recommended,
48
- ],
49
- languageOptions: {
50
- // other options...
51
- parserOptions: {
52
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
53
- tsconfigRootDir: import.meta.dirname,
54
- },
55
- },
56
- })
18
+ ## 使用
19
+
20
+ ### 命令行模式
21
+
22
+ 打开指定图片:
23
+ ```bash
24
+ mjpic path/to/image.jpg
57
25
  ```
26
+
27
+ 启动服务:
28
+ ```bash
29
+ mjpic start
30
+ # 或
31
+ mjpic dev
32
+ ```
33
+
34
+ ### 网页模式
35
+
36
+ 访问部署后的网页即可使用。
37
+
38
+ ## 开发
39
+
40
+ 1. 克隆仓库
41
+ 2. 安装依赖:`npm install`
42
+ 3. 启动开发服务器:`npm run dev`
43
+
44
+
45
+ ## 安装使用
46
+
47
+ ### 安装
48
+
49
+ Q:Arm架构的麒麟操作系统V10如何安装Node.js?
50
+
51
+ ■ 豆包:
52
+
53
+ 方法一:直接手工安装
54
+
55
+ 访问Node.js官网 https://nodejs.org/zh-cn/download
56
+
57
+ uname -m # aarch64表示系统架构是ARM64
58
+
59
+ 下载Linux ARM64版本
60
+
61
+ cd ~/下载
62
+ wget https://nodejs.org/dist/v24.13.1/node-v24.13.1-linux-arm64.tar.xz
63
+ tar -xvf node-xxx-arm64.tar.xz
64
+ sudo mv node-xxx-arm64 /usr/local/nodejs
65
+
66
+ 配置环境变量
67
+
68
+ sudo vim /etc/profile
69
+ export NODE_HOME=/usr/local/nodejs
70
+ export PATH=$NODE_HOME/bin:$PATH
71
+ source /etc/profile
72
+
73
+ 测试安装是否成功
74
+
75
+ node -v
76
+
77
+ 方法二:配合`/deploy/node_install.sh`脚本安装
78
+
79
+ 1. 保存`node_install.sh 到`下载 目录。
80
+
81
+ 2. 打开终端(terminal)。
82
+
83
+ 3. 进入下载目录:
84
+
85
+ cd ~/下载
86
+
87
+ 4. 赋予脚本可执行权限:
88
+
89
+ chmod +x node_install.sh
90
+
91
+ 5. 以root权限运行脚本:
92
+
93
+ sudo ./node_install.sh
94
+
95
+ 6. 使环境变量生效
96
+
97
+ source ~/.bashrc
98
+
99
+ 7. 下载 敏捷图片(mjpic):
100
+
101
+ npm install mjpic@latest
102
+
103
+ 8. 运行 敏捷图片(mjpic):
104
+
105
+ npx mjpic build --port 3030
106
+
107
+ ### 创建桌面快捷方式
108
+
109
+ Q:如何在麒麟操作系统V10的桌面创建一个快捷方式,单击后直接在终端运行 npx mjpic build --port 3030 命令?
110
+
111
+ ■ 豆包:
112
+
113
+ 方法一:
114
+
115
+ cd ~/桌面
116
+
117
+ vim mjpic-start.desktop
118
+
119
+ [Desktop Entry]
120
+ 快捷方式类型(应用程序)
121
+ Type=Application
122
+ 显示名称(桌面显示的文件名)
123
+ Name=敏捷图片
124
+ 注释(鼠标悬停显示)
125
+ Comment=启动mjpic服务,端口3030
126
+ 图标(可选,可替换为自己的图标路径,也可留空)
127
+ Icon=image
128
+ 关键:执行的命令(xterm/gnome-terminal 打开终端并执行命令)
129
+ 麒麟V10优先用 gnome-terminal,若不行换 xterm
130
+ Exec=mate-terminal -- bash --login -c "source ~/.bashrc; npx mjpic build --port 3030; exec bash"
131
+ 执行终端窗口是否保持打开(yes=执行完不关闭,no=执行完关闭)
132
+ Terminal=yes
133
+ 分类(桌面显示的归类)
134
+ Categories=Graphics
135
+ 编码
136
+ Encoding=UTF-8
137
+ 是否允许执行
138
+ Executable=true
139
+
140
+ chmod +x ~/桌面/mjpic-start.desktop
141
+
142
+ 注:如有相关提示,选择始终允许执行。
143
+
144
+
145
+ 方法二:
146
+
147
+ 9. 关闭终端,保存 mjpic-start.desktop 到 桌面,右键属性勾选可执行权限。双击新建图标运行。
package/api/cli.ts CHANGED
@@ -5,6 +5,7 @@ import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import open from 'open';
8
+ import net from 'net';
8
9
  import app from './app.js';
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
@@ -12,8 +13,46 @@ const __dirname = path.dirname(__filename);
12
13
 
13
14
  const program = new Command();
14
15
 
15
- const startServer = async (options: { port: string; host: string }, file?: string) => {
16
- const port = parseInt(options.port, 10);
16
+ // Check if a port is available
17
+ const isPortAvailable = (port: number): Promise<boolean> => {
18
+ return new Promise((resolve) => {
19
+ const server = net.createServer();
20
+ server.once('error', () => resolve(false));
21
+ server.once('listening', () => {
22
+ server.close();
23
+ resolve(true);
24
+ });
25
+ server.listen(port);
26
+ });
27
+ };
28
+
29
+ // Generate a random port between min and max, excluding common ports
30
+ const getRandomPort = async (): Promise<number> => {
31
+ const commonPorts = [3000, 5000, 5173, 8000, 8080, 8888, 4200];
32
+ const min = 10000;
33
+ const max = 65535;
34
+
35
+ // Try up to 50 times to find a port
36
+ for (let i = 0; i < 50; i++) {
37
+ const port = Math.floor(Math.random() * (max - min + 1) + min);
38
+ if (!commonPorts.includes(port) && await isPortAvailable(port)) {
39
+ return port;
40
+ }
41
+ }
42
+
43
+ // Fallback to 0 (let OS choose) if we can't find one, though OS might pick a common one (unlikely)
44
+ return 0;
45
+ };
46
+
47
+ const startServer = async (options: { port?: string; host: string }, file?: string) => {
48
+ let port: number;
49
+
50
+ if (options.port) {
51
+ port = parseInt(options.port, 10);
52
+ } else {
53
+ port = await getRandomPort();
54
+ }
55
+
17
56
  const host = options.host;
18
57
 
19
58
  if (file) {
@@ -53,12 +92,12 @@ const startServer = async (options: { port: string; host: string }, file?: strin
53
92
 
54
93
  program
55
94
  .name('mjpic')
56
- .description('敏捷图片 - 轻量级图片处理工具')
57
- .version('1.0.5');
95
+ .description('敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。')
96
+ .version('1.0.6');
58
97
 
59
98
  const attachServeOptions = (cmd: Command) => {
60
99
  cmd
61
- .option('-p, --port <number>', 'server port', '3000')
100
+ .option('-p, --port <number>', 'server port')
62
101
  .option('--host <string>', 'server host', 'localhost')
63
102
  .argument('[file]', 'image file to open')
64
103
  .action(async (file, options) => {
@@ -0,0 +1,20 @@
1
+ [Desktop Entry]
2
+ # 快捷方式类型(应用程序)
3
+ Type=Application
4
+ # 显示名称(桌面显示的文件名)
5
+ Name=敏捷图片
6
+ # 注释(鼠标悬停显示)
7
+ Comment=启动mjpic服务,端口3030
8
+ # 图标(可选,可替换为自己的图标路径,也可留空)
9
+ Icon=image
10
+ # 关键:执行的命令(xterm/gnome-terminal 打开终端并执行命令)
11
+ # 麒麟V10优先用 gnome-terminal,若不行换 xterm
12
+ Exec=mate-terminal -- bash --login -c "source ~/.bashrc; npx mjpic build --port 3030; exec bash"
13
+ # 执行终端窗口是否保持打开(yes=执行完不关闭,no=执行完关闭)
14
+ Terminal=yes
15
+ # 分类(桌面显示的归类)
16
+ Categories=Graphics
17
+ # 编码
18
+ Encoding=UTF-8
19
+ # 是否允许执行
20
+ Executable=true
@@ -0,0 +1,111 @@
1
+ #!/bin/bash
2
+ # Node.js ARM64架构 麒麟V10 自动安装脚本
3
+ # 版本:v24.13.1
4
+ # 修复:sudo执行时使用原始用户目录 + 环境变量全局生效
5
+
6
+ # 定义Node.js版本和下载地址
7
+ NODE_VERSION="v24.13.1"
8
+ NODE_ARCH="linux-arm64"
9
+ NODE_PACKAGE="node-${NODE_VERSION}-${NODE_ARCH}.tar.xz"
10
+ DOWNLOAD_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_PACKAGE}"
11
+ INSTALL_DIR="/usr/local/nodejs"
12
+
13
+ # 关键修复1:获取执行sudo前的原始用户(而非root)
14
+ ORIGINAL_USER=${SUDO_USER:-$USER}
15
+ ORIGINAL_USER_HOME="/home/${ORIGINAL_USER}"
16
+ DOWNLOAD_DIR="${ORIGINAL_USER_HOME}/下载"
17
+
18
+ # 检查是否为root权限(移动文件需要sudo)
19
+ if [ $EUID -ne 0 ]; then
20
+ echo "错误:请使用root权限运行此脚本(sudo ./node_install.sh)"
21
+ exit 1
22
+ fi
23
+
24
+ # 步骤1:进入原始用户的下载目录
25
+ echo "===== 进入原始用户(${ORIGINAL_USER})的下载目录:${DOWNLOAD_DIR} ====="
26
+ cd "${DOWNLOAD_DIR}" || {
27
+ echo "错误:${DOWNLOAD_DIR} 目录不存在,创建该目录..."
28
+ mkdir -p "${DOWNLOAD_DIR}"
29
+ chown "${ORIGINAL_USER}:${ORIGINAL_USER}" "${DOWNLOAD_DIR}"
30
+ cd "${DOWNLOAD_DIR}"
31
+ }
32
+
33
+ # 步骤2:下载Node.js安装包
34
+ echo -e "\n===== 开始下载Node.js ${NODE_VERSION} ====="
35
+ if [ -f "${NODE_PACKAGE}" ]; then
36
+ echo "检测到已存在同名安装包,跳过下载..."
37
+ else
38
+ wget "${DOWNLOAD_URL}" -O "${NODE_PACKAGE}" || {
39
+ echo "下载失败!请更换淘宝镜像源:"
40
+ echo "DOWNLOAD_URL=\"https://npm.taobao.org/mirrors/node/${NODE_VERSION}/${NODE_PACKAGE}\""
41
+ exit 1
42
+ }
43
+ chown "${ORIGINAL_USER}:${ORIGINAL_USER}" "${NODE_PACKAGE}"
44
+ fi
45
+
46
+ # 步骤3:解压安装包
47
+ echo -e "\n===== 解压安装包 ====="
48
+ tar -xvf "${NODE_PACKAGE}" || {
49
+ echo "解压失败!安装包可能损坏,请删除后重新下载"
50
+ exit 1
51
+ }
52
+ chown -R "${ORIGINAL_USER}:${ORIGINAL_USER}" "node-${NODE_VERSION}-${NODE_ARCH}"
53
+
54
+ # 步骤4:移动到系统目录
55
+ echo -e "\n===== 移动安装目录到 ${INSTALL_DIR} ====="
56
+ if [ -d "${INSTALL_DIR}" ]; then
57
+ echo "检测到已存在${INSTALL_DIR},先备份并删除..."
58
+ mv "${INSTALL_DIR}" "${INSTALL_DIR}_bak_$(date +%Y%m%d%H%M%S)"
59
+ fi
60
+ mv "node-${NODE_VERSION}-${NODE_ARCH}" "${INSTALL_DIR}" || {
61
+ echo "移动目录失败!请检查权限"
62
+ exit 1
63
+ }
64
+
65
+ # 步骤5:配置环境变量(关键修复2:确保全局生效)
66
+ echo -e "\n===== 配置系统环境变量 ====="
67
+ # 方案1:写入/etc/profile(系统级,所有用户生效)
68
+ if ! grep -q "NODE_HOME=${INSTALL_DIR}" /etc/profile; then
69
+ echo "" >> /etc/profile
70
+ echo "# Node.js environment variables" >> /etc/profile
71
+ echo "export NODE_HOME=${INSTALL_DIR}" >> /etc/profile
72
+ echo "export PATH=\$NODE_HOME/bin:\$PATH" >> /etc/profile
73
+ echo "系统级环境变量已添加到/etc/profile"
74
+ else
75
+ echo "系统级环境变量已存在,无需重复添加"
76
+ fi
77
+
78
+ # 方案2:写入原始用户的~/.bashrc(用户级,避免source失效)
79
+ USER_BASHRC="${ORIGINAL_USER_HOME}/.bashrc"
80
+ if ! grep -q "NODE_HOME=${INSTALL_DIR}" "${USER_BASHRC}"; then
81
+ echo "" >> "${USER_BASHRC}"
82
+ echo "# Node.js environment variables" >> "${USER_BASHRC}"
83
+ echo "export NODE_HOME=${INSTALL_DIR}" >> "${USER_BASHRC}"
84
+ echo "export PATH=\$NODE_HOME/bin:\$PATH" >> "${USER_BASHRC}"
85
+ chown "${ORIGINAL_USER}:${ORIGINAL_USER}" "${USER_BASHRC}"
86
+ echo "用户级环境变量已添加到${USER_BASHRC}"
87
+ else
88
+ echo "用户级环境变量已存在,无需重复添加"
89
+ fi
90
+
91
+ # 步骤6:验证安装(直接用绝对路径验证,避免环境变量未生效的误判)
92
+ echo -e "\n===== 验证安装结果 ====="
93
+ # 直接使用Node.js的绝对路径验证,不受当前shell环境影响
94
+ NODE_ABS_PATH="${INSTALL_DIR}/bin/node"
95
+ NPM_ABS_PATH="${INSTALL_DIR}/bin/npm"
96
+
97
+ if [ -f "${NODE_ABS_PATH}" ] && [ -f "${NPM_ABS_PATH}" ]; then
98
+ echo "Node.js 绝对路径验证:$(${NODE_ABS_PATH} -v)"
99
+ echo "npm 绝对路径验证:$(${NPM_ABS_PATH} -v)"
100
+ echo -e "\n===== 安装成功!====="
101
+ echo "注意:环境变量需以下方式生效:"
102
+ echo "1. 普通用户执行:source ~/.bashrc (立即生效)"
103
+ echo "2. 或重启终端/重新登录(永久生效)"
104
+ echo "3. 验证命令:node -v && npm -v"
105
+ else
106
+ echo -e "\n===== 安装失败!====="
107
+ echo "未找到Node.js/npm的可执行文件,请检查安装目录:${INSTALL_DIR}/bin"
108
+ exit 1
109
+ fi
110
+
111
+ exit 0
@@ -0,0 +1,17 @@
1
+ # 敏捷图片(mjpic)安装使用说明
2
+
3
+ 1. 保存 node_install.sh 到 下载 目录。
4
+ 2. 打开终端(terminal)。
5
+ 3. 进入下载目录:
6
+ cd ~/下载
7
+ 4. 赋予脚本可执行权限:
8
+ chmod +x node_install.sh
9
+ 5. 以root权限运行脚本:
10
+ sudo ./node_install.sh
11
+ 6. 使环境变量生效
12
+ source ~/.bashrc
13
+ 7. 下载 敏捷图片(mjpic):
14
+ npm install mjpic@latest
15
+ 8. 运行 敏捷图片(mjpic):
16
+ npx mjpic build --port 3030
17
+ 9. 关闭终端,保存 mjpic-start.desktop 到 桌面,右键属性勾选可执行权限。双击新建图标运行。
package/dist/cli/cli.js CHANGED
@@ -5,12 +5,46 @@ import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import open from 'open';
8
+ import net from 'net';
8
9
  import app from './app.js';
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  const program = new Command();
13
+ // Check if a port is available
14
+ const isPortAvailable = (port) => {
15
+ return new Promise((resolve) => {
16
+ const server = net.createServer();
17
+ server.once('error', () => resolve(false));
18
+ server.once('listening', () => {
19
+ server.close();
20
+ resolve(true);
21
+ });
22
+ server.listen(port);
23
+ });
24
+ };
25
+ // Generate a random port between min and max, excluding common ports
26
+ const getRandomPort = async () => {
27
+ const commonPorts = [3000, 5000, 5173, 8000, 8080, 8888, 4200];
28
+ const min = 10000;
29
+ const max = 65535;
30
+ // Try up to 50 times to find a port
31
+ for (let i = 0; i < 50; i++) {
32
+ const port = Math.floor(Math.random() * (max - min + 1) + min);
33
+ if (!commonPorts.includes(port) && await isPortAvailable(port)) {
34
+ return port;
35
+ }
36
+ }
37
+ // Fallback to 0 (let OS choose) if we can't find one, though OS might pick a common one (unlikely)
38
+ return 0;
39
+ };
12
40
  const startServer = async (options, file) => {
13
- const port = parseInt(options.port, 10);
41
+ let port;
42
+ if (options.port) {
43
+ port = parseInt(options.port, 10);
44
+ }
45
+ else {
46
+ port = await getRandomPort();
47
+ }
14
48
  const host = options.host;
15
49
  if (file) {
16
50
  const absPath = path.resolve(file);
@@ -43,11 +77,11 @@ const startServer = async (options, file) => {
43
77
  };
44
78
  program
45
79
  .name('mjpic')
46
- .description('敏捷图片 - 轻量级图片处理工具')
47
- .version('1.0.5');
80
+ .description('敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。')
81
+ .version('1.0.6');
48
82
  const attachServeOptions = (cmd) => {
49
83
  cmd
50
- .option('-p, --port <number>', 'server port', '3000')
84
+ .option('-p, --port <number>', 'server port')
51
85
  .option('--host <string>', 'server host', 'localhost')
52
86
  .argument('[file]', 'image file to open')
53
87
  .action(async (file, options) => {
@@ -4,7 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>敏捷图片 - mjpic</title>
7
+ <meta name="description" content="敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。" />
8
+ <title>敏捷图片 (mjpic) - 轻量级网页版图片处理工具</title>
8
9
  <script type="module" crossorigin src="/assets/index-BQfYCBRX.js"></script>
9
10
  <link rel="stylesheet" crossorigin href="/assets/index-BoiS81Ei.css">
10
11
  </head>
package/index.html CHANGED
@@ -4,7 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>敏捷图片 - mjpic</title>
7
+ <meta name="description" content="敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。" />
8
+ <title>敏捷图片 (mjpic) - 轻量级网页版图片处理工具</title>
8
9
  <script type="module">
9
10
  if (import.meta.hot?.on) {
10
11
  import.meta.hot.on('vite:error', (error) => {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "mjpic",
3
- "version": "1.0.5",
3
+ "description": "敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。",
4
+ "version": "1.0.7",
4
5
  "type": "module",
5
6
  "bin": {
6
7
  "mjpic": "./dist/cli/cli.js"
@@ -13,8 +14,8 @@
13
14
  "lint": "eslint .",
14
15
  "preview": "vite preview",
15
16
  "check": "tsc --noEmit",
16
- "server:dev": "nodemon",
17
- "dev": "concurrently \"npm run client:dev\" \"npm run server:dev\""
17
+ "server:dev": "PORT=${PORT:-3002} nodemon",
18
+ "dev": "export PORT=$(node scripts/get-random-port.js) && export CLIENT_PORT=$(node scripts/get-random-port.js) && echo \"Starting Backend on $PORT, Frontend on $CLIENT_PORT\" && concurrently \"npm run client:dev\" \"npm run server:dev\""
18
19
  },
19
20
  "dependencies": {
20
21
  "@dnd-kit/core": "^6.3.1",
@@ -29,6 +30,7 @@
29
30
  "i18next-browser-languagedetector": "^8.2.1",
30
31
  "konva": "^9.3.16",
31
32
  "lucide-react": "^0.511.0",
33
+ "mjpic": "^1.0.6",
32
34
  "open": "^11.0.0",
33
35
  "react": "^18.3.1",
34
36
  "react-dom": "^18.3.1",
@@ -0,0 +1,40 @@
1
+ import net from 'net';
2
+
3
+ // Check if a port is available
4
+ const isPortAvailable = (port) => {
5
+ return new Promise((resolve) => {
6
+ const server = net.createServer();
7
+ server.once('error', () => resolve(false));
8
+ server.once('listening', () => {
9
+ server.close();
10
+ resolve(true);
11
+ });
12
+ server.listen(port);
13
+ });
14
+ };
15
+
16
+ // Generate a random port between min and max, excluding common ports
17
+ const getRandomPort = async () => {
18
+ const commonPorts = [3000, 5000, 5173, 8000, 8080, 8888, 4200];
19
+ const min = 10000;
20
+ const max = 65535;
21
+
22
+ // Try up to 50 times to find a port
23
+ for (let i = 0; i < 50; i++) {
24
+ const port = Math.floor(Math.random() * (max - min + 1) + min);
25
+ if (!commonPorts.includes(port) && await isPortAvailable(port)) {
26
+ return port;
27
+ }
28
+ }
29
+
30
+ // Fallback to 0 (let OS choose)
31
+ return 0;
32
+ };
33
+
34
+ // Run if called directly
35
+ const run = async () => {
36
+ const port = await getRandomPort();
37
+ console.log(port);
38
+ };
39
+
40
+ run();
@@ -137,7 +137,7 @@ export const SaveDialog = ({ isOpen, onClose, onConfirm, defaultPath, defaultFil
137
137
  {t('common.cancel')}
138
138
  </button>
139
139
  <button
140
- onClick={() => onConfirm(format, quality / 100, savePath, fileName)}
140
+ onClick={() => onConfirm(format, quality, savePath, fileName)}
141
141
  className="flex-1 px-3 py-2 rounded bg-blue-600 hover:bg-blue-500 text-white text-sm transition-colors"
142
142
  >
143
143
  {t('common.confirm')}
@@ -39,30 +39,55 @@ export const Header = ({ stageRef }: HeaderProps) => {
39
39
  if (!stageRef.current) return;
40
40
 
41
41
  try {
42
- // Find the image node to get current scale
43
- const imageNode = stageRef.current.findOne('Image') as Konva.Image;
44
- // Find the content group (which contains image + border)
45
- const contentGroup = stageRef.current.findOne('#content-group') as Konva.Group;
42
+ // Get the current state from the image store
43
+ const { previewImage, config, originalWidth, originalHeight } = useImageStore.getState();
46
44
 
47
- if (!imageNode || !contentGroup) {
48
- console.error('No image or content group found to save');
45
+ if (!previewImage) {
46
+ console.error('No image found to save');
49
47
  return;
50
48
  }
51
-
52
- // Store current scale to restore later
53
- const currentScaleX = imageNode.scaleX();
54
49
 
55
- // Calculate pixelRatio needed to get original resolution
56
- // The content is scaled down by currentScaleX to fit screen.
57
- // We need to scale it back up by 1/currentScaleX.
58
- const pixelRatio = 1 / currentScaleX;
59
-
60
- const dataUrl = contentGroup.toDataURL({
61
- pixelRatio: pixelRatio,
62
- mimeType: format,
63
- quality: quality
50
+ // Determine the final dimensions based on resize config
51
+ let finalWidth = originalWidth || 800;
52
+ let finalHeight = originalHeight || 600;
53
+
54
+ if (config.resize && config.resize.width > 0 && config.resize.height > 0) {
55
+ finalWidth = config.resize.width;
56
+ finalHeight = config.resize.height;
57
+ }
58
+
59
+ // Create a temporary canvas with the correct dimensions
60
+ const canvas = document.createElement('canvas');
61
+ canvas.width = finalWidth;
62
+ canvas.height = finalHeight;
63
+ const ctx = canvas.getContext('2d');
64
+
65
+ if (!ctx) {
66
+ console.error('Failed to create canvas context');
67
+ return;
68
+ }
69
+
70
+ // Set high quality rendering
71
+ ctx.imageSmoothingEnabled = true;
72
+ ctx.imageSmoothingQuality = 'high';
73
+
74
+ // Load the preview image as the source
75
+ const img = new Image();
76
+ img.crossOrigin = 'anonymous';
77
+
78
+ await new Promise<void>((resolve, reject) => {
79
+ img.onload = () => {
80
+ // Draw the image with the correct dimensions
81
+ ctx.drawImage(img, 0, 0, finalWidth, finalHeight);
82
+ resolve();
83
+ };
84
+ img.onerror = reject;
85
+ img.src = previewImage;
64
86
  });
65
87
 
88
+ // Convert canvas to data URL with correct quality
89
+ const dataUrl = canvas.toDataURL(format, quality / 100);
90
+
66
91
  // Check if we are in CLI mode with an opened file
67
92
  // If savePath is provided (from dialog), use it
68
93
  if (savePath) {
@@ -428,27 +428,6 @@ export const RightPanel = () => {
428
428
  <label htmlFor="maintainAspectRatio" className="text-xs text-zinc-400 cursor-pointer">{t('common.lockAspectRatio')}</label>
429
429
  </div>
430
430
  </div>
431
-
432
- <div className="flex gap-3 mt-2">
433
- <button
434
- onClick={() => {
435
- // 确定按钮的逻辑
436
- // 这里可以添加额外的验证或处理
437
- }}
438
- className="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2 rounded text-sm font-medium"
439
- >
440
- {t('common.confirm')}
441
- </button>
442
- <button
443
- onClick={() => {
444
- // 取消按钮的逻辑
445
- // 可以重置为之前的尺寸或保持不变
446
- }}
447
- className="flex-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-200 py-2 rounded text-sm"
448
- >
449
- {t('common.cancel')}
450
- </button>
451
- </div>
452
431
  </div>
453
432
  </div>
454
433
  )}
package/vite.config.ts CHANGED
@@ -15,9 +15,11 @@ export default defineConfig({
15
15
  tsconfigPaths(),
16
16
  ],
17
17
  server: {
18
+ port: parseInt(process.env.CLIENT_PORT || '5173'),
19
+ strictPort: true, // Fail if port is busy
18
20
  proxy: {
19
21
  '/api': {
20
- target: 'http://localhost:3002',
22
+ target: `http://localhost:${process.env.PORT || 3002}`,
21
23
  changeOrigin: true,
22
24
  secure: false,
23
25
  configure: (proxy, _options) => {
package/tmp/guangxi.jpg DELETED
Binary file