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.
Files changed (58) hide show
  1. package/.trae/documents/mjpic-prd.md +111 -0
  2. package/.trae/documents/mjpic-technical-architecture.md +234 -0
  3. package/README.md +57 -0
  4. package/api/app.ts +60 -0
  5. package/api/cli.ts +61 -0
  6. package/api/index.ts +19 -0
  7. package/api/routes/auth.ts +33 -0
  8. package/api/routes/image.ts +27 -0
  9. package/api/server.ts +45 -0
  10. package/dist/cli/app.js +43 -0
  11. package/dist/cli/cli.js +49 -0
  12. package/dist/cli/index.js +13 -0
  13. package/dist/cli/routes/auth.js +28 -0
  14. package/dist/cli/routes/image.js +21 -0
  15. package/dist/cli/server.js +38 -0
  16. package/dist/client/assets/index-BUIYLOn-.js +197 -0
  17. package/dist/client/assets/index-BoiS81Ei.css +1 -0
  18. package/dist/client/favicon.svg +4 -0
  19. package/dist/client/index.html +354 -0
  20. package/eslint.config.js +28 -0
  21. package/index.html +24 -0
  22. package/nodemon.json +10 -0
  23. package/package.json +68 -0
  24. package/postcss.config.js +10 -0
  25. package/public/favicon.svg +4 -0
  26. package/src/App.tsx +13 -0
  27. package/src/assets/react.svg +1 -0
  28. package/src/components/Empty.tsx +8 -0
  29. package/src/components/dialogs/AspectRatioDialog.tsx +218 -0
  30. package/src/components/dialogs/SaveDialog.tsx +150 -0
  31. package/src/components/layout/CanvasArea.tsx +874 -0
  32. package/src/components/layout/Header.tsx +156 -0
  33. package/src/components/layout/RightPanel.tsx +886 -0
  34. package/src/components/layout/Sidebar.tsx +36 -0
  35. package/src/components/layout/StatusBar.tsx +44 -0
  36. package/src/hooks/useDebounce.ts +17 -0
  37. package/src/hooks/useTheme.ts +29 -0
  38. package/src/i18n/index.ts +26 -0
  39. package/src/i18n/locales/en.json +56 -0
  40. package/src/i18n/locales/zh.json +59 -0
  41. package/src/index.css +14 -0
  42. package/src/lib/utils.ts +73 -0
  43. package/src/main.tsx +11 -0
  44. package/src/pages/Home.tsx +72 -0
  45. package/src/store/useImageStore.ts +316 -0
  46. package/src/store/usePresetStore.ts +65 -0
  47. package/src/store/useUIStore.ts +17 -0
  48. package/src/vite-env.d.ts +1 -0
  49. package/tailwind.config.js +13 -0
  50. package/tmp/guangxi.jpg +0 -0
  51. package/tsconfig.json +40 -0
  52. package/tsconfig.server.json +15 -0
  53. package/vercel.json +12 -0
  54. package/vite.config.ts +50 -0
  55. 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
  56. 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
  57. 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
  58. 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
+ };