mjpic 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trae/documents/mjpic-prd.md +111 -0
- package/.trae/documents/mjpic-technical-architecture.md +234 -0
- package/README.md +57 -0
- package/api/app.ts +60 -0
- package/api/cli.ts +61 -0
- package/api/index.ts +19 -0
- package/api/routes/auth.ts +33 -0
- package/api/routes/image.ts +27 -0
- package/api/server.ts +45 -0
- package/dist/cli/app.js +43 -0
- package/dist/cli/cli.js +49 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/routes/auth.js +28 -0
- package/dist/cli/routes/image.js +21 -0
- package/dist/cli/server.js +38 -0
- package/dist/client/assets/index-BUIYLOn-.js +197 -0
- package/dist/client/assets/index-BoiS81Ei.css +1 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/index.html +354 -0
- package/eslint.config.js +28 -0
- package/index.html +24 -0
- package/nodemon.json +10 -0
- package/package.json +68 -0
- package/postcss.config.js +10 -0
- package/public/favicon.svg +4 -0
- package/src/App.tsx +13 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/dialogs/AspectRatioDialog.tsx +218 -0
- package/src/components/dialogs/SaveDialog.tsx +150 -0
- package/src/components/layout/CanvasArea.tsx +874 -0
- package/src/components/layout/Header.tsx +156 -0
- package/src/components/layout/RightPanel.tsx +886 -0
- package/src/components/layout/Sidebar.tsx +36 -0
- package/src/components/layout/StatusBar.tsx +44 -0
- package/src/hooks/useDebounce.ts +17 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/i18n/index.ts +26 -0
- package/src/i18n/locales/en.json +56 -0
- package/src/i18n/locales/zh.json +59 -0
- package/src/index.css +14 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +11 -0
- package/src/pages/Home.tsx +72 -0
- package/src/store/useImageStore.ts +316 -0
- package/src/store/usePresetStore.ts +65 -0
- package/src/store/useUIStore.ts +17 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +13 -0
- package/tmp/guangxi.jpg +0 -0
- package/tsconfig.json +40 -0
- package/tsconfig.server.json +15 -0
- package/vercel.json +12 -0
- package/vite.config.ts +50 -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,156 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useImageStore } from '@/store/useImageStore';
|
|
3
|
+
import { FolderOpen, Save, Undo, Redo, Languages } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import Konva from 'konva';
|
|
6
|
+
import { SaveDialog } from '@/components/dialogs/SaveDialog';
|
|
7
|
+
|
|
8
|
+
interface HeaderProps {
|
|
9
|
+
stageRef: React.MutableRefObject<Konva.Stage | null>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Header = ({ stageRef }: HeaderProps) => {
|
|
13
|
+
const { undo, redo, loadImage, fileName } = useImageStore();
|
|
14
|
+
const { t, i18n } = useTranslation();
|
|
15
|
+
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
|
|
16
|
+
const [currentPath, setCurrentPath] = useState<string | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
const handleSaveClick = async () => {
|
|
19
|
+
if (!stageRef.current) return;
|
|
20
|
+
|
|
21
|
+
// Try to get current image path for CLI mode
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch('/api/current-image');
|
|
24
|
+
const imageData = response.ok ? await response.json() : null;
|
|
25
|
+
if (imageData && imageData.path) {
|
|
26
|
+
setCurrentPath(imageData.path);
|
|
27
|
+
} else {
|
|
28
|
+
setCurrentPath(undefined);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
setCurrentPath(undefined);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setIsSaveDialogOpen(true);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleConfirmSave = async (format: string, quality: number, savePath?: string, saveFileName?: string) => {
|
|
38
|
+
setIsSaveDialogOpen(false);
|
|
39
|
+
if (!stageRef.current) return;
|
|
40
|
+
|
|
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;
|
|
46
|
+
|
|
47
|
+
if (!imageNode || !contentGroup) {
|
|
48
|
+
console.error('No image or content group found to save');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Store current scale to restore later
|
|
53
|
+
const currentScaleX = imageNode.scaleX();
|
|
54
|
+
|
|
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
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Check if we are in CLI mode with an opened file
|
|
67
|
+
// If savePath is provided (from dialog), use it
|
|
68
|
+
if (savePath) {
|
|
69
|
+
// CLI Mode: Save to specific path
|
|
70
|
+
const saveRes = await fetch('/api/save', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ data: dataUrl, path: savePath })
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (saveRes.ok) {
|
|
77
|
+
alert(t('common.saveSuccess'));
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error('Save API failed');
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Browser Mode or API fail: Download
|
|
83
|
+
const link = document.createElement('a');
|
|
84
|
+
link.download = saveFileName || 'mjpic-edit.png'; // Use provided filename
|
|
85
|
+
link.href = dataUrl;
|
|
86
|
+
document.body.appendChild(link);
|
|
87
|
+
link.click();
|
|
88
|
+
document.body.removeChild(link);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error('Save failed:', e);
|
|
92
|
+
alert('Failed to save image.');
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleOpen = async () => {
|
|
97
|
+
// Open file dialog
|
|
98
|
+
const input = document.createElement('input');
|
|
99
|
+
input.type = 'file';
|
|
100
|
+
input.accept = 'image/*';
|
|
101
|
+
input.onchange = (e) => {
|
|
102
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
103
|
+
if (file) {
|
|
104
|
+
const url = URL.createObjectURL(file);
|
|
105
|
+
loadImage(url, file.name, file.name);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
input.click();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const toggleLanguage = () => {
|
|
112
|
+
const newLang = i18n.language === 'en' ? 'zh' : 'en';
|
|
113
|
+
i18n.changeLanguage(newLang);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="h-12 bg-zinc-900 border-b border-zinc-800 flex items-center px-4 justify-between z-20 shrink-0">
|
|
118
|
+
<div className="flex items-center gap-4">
|
|
119
|
+
<h1 className="text-zinc-100 font-bold text-lg mr-4">{t('common.appName')}</h1>
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<button onClick={handleOpen} className="px-3 py-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100 text-sm flex items-center gap-1 transition-colors">
|
|
122
|
+
<FolderOpen size={16} /> {t('common.open')}
|
|
123
|
+
</button>
|
|
124
|
+
<button onClick={handleSaveClick} className="px-3 py-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100 text-sm flex items-center gap-1 transition-colors">
|
|
125
|
+
<Save size={16} /> {t('common.save')}
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="flex items-center gap-1">
|
|
131
|
+
<button
|
|
132
|
+
onClick={toggleLanguage}
|
|
133
|
+
className="px-3 py-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100 text-sm flex items-center gap-1 transition-colors mr-2"
|
|
134
|
+
title={i18n.language === 'en' ? 'Switch to Chinese' : 'Switch to English'}
|
|
135
|
+
>
|
|
136
|
+
<Languages size={16} />
|
|
137
|
+
{i18n.language === 'en' ? '中文' : 'English'}
|
|
138
|
+
</button>
|
|
139
|
+
<button onClick={undo} className="p-2 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100 transition-colors" title={t('common.undo')}>
|
|
140
|
+
<Undo size={18} />
|
|
141
|
+
</button>
|
|
142
|
+
<button onClick={redo} className="p-2 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100 transition-colors" title={t('common.redo')}>
|
|
143
|
+
<Redo size={18} />
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<SaveDialog
|
|
148
|
+
isOpen={isSaveDialogOpen}
|
|
149
|
+
onClose={() => setIsSaveDialogOpen(false)}
|
|
150
|
+
onConfirm={handleConfirmSave}
|
|
151
|
+
defaultPath={currentPath}
|
|
152
|
+
defaultFileName={fileName || undefined}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|