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.
- package/.vercel/project.json +1 -0
- package/README.md +144 -54
- package/api/cli.ts +44 -5
- package/deploy/mjpic-start.desktop +20 -0
- package/deploy/node_install.sh +111 -0
- package/deploy//344/275/277/347/224/250/350/257/264/346/230/216.txt +17 -0
- package/dist/cli/cli.js +38 -4
- package/dist/client/index.html +2 -1
- package/index.html +2 -1
- package/package.json +5 -3
- package/scripts/get-random-port.js +40 -0
- package/src/components/dialogs/SaveDialog.tsx +1 -1
- package/src/components/layout/Header.tsx +43 -18
- package/src/components/layout/RightPanel.tsx +0 -21
- package/vite.config.ts +3 -1
- package/tmp/guangxi.jpg +0 -0
- 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
- 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
- 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
- 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
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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.
|
|
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'
|
|
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
|
-
|
|
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.
|
|
80
|
+
.description('敏捷图片(mjpic)是一个轻量级网页版图片处理工具,设计灵感来源于光影魔术手。')
|
|
81
|
+
.version('1.0.6');
|
|
48
82
|
const attachServeOptions = (cmd) => {
|
|
49
83
|
cmd
|
|
50
|
-
.option('-p, --port <number>', 'server port'
|
|
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) => {
|
package/dist/client/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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
"
|
|
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
|
|
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
|
-
//
|
|
43
|
-
const
|
|
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 (!
|
|
48
|
-
console.error('No image
|
|
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
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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
|