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,36 @@
|
|
|
1
|
+
import { Wand2, Sliders, Ruler, Crop, RotateCw, Square } from 'lucide-react';
|
|
2
|
+
import { useUIStore, ToolType } from '@/store/useUIStore';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
export const Sidebar = () => {
|
|
7
|
+
const { activeTool, setActiveTool } = useUIStore();
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
|
|
10
|
+
const tools: { id: ToolType; icon: any; label: string }[] = [
|
|
11
|
+
{ id: 'enhance', icon: Wand2, label: t('sidebar.enhance') },
|
|
12
|
+
{ id: 'adjust', icon: Sliders, label: t('sidebar.adjust') },
|
|
13
|
+
{ id: 'resize', icon: Ruler, label: t('sidebar.resize') },
|
|
14
|
+
{ id: 'crop', icon: Crop, label: t('sidebar.crop') },
|
|
15
|
+
{ id: 'rotate', icon: RotateCw, label: t('sidebar.rotate') },
|
|
16
|
+
{ id: 'border', icon: Square, label: t('sidebar.border') },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="w-[80px] bg-zinc-900 border-r border-zinc-800 flex flex-col items-center py-4 gap-2 h-full z-10">
|
|
21
|
+
{tools.map((tool) => (
|
|
22
|
+
<button
|
|
23
|
+
key={tool.id}
|
|
24
|
+
onClick={() => setActiveTool(tool.id)}
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex flex-col items-center justify-center w-16 h-16 rounded-lg transition-colors text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
|
|
27
|
+
activeTool === tool.id && "bg-zinc-800 text-blue-500 hover:text-blue-400"
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<tool.icon className="w-6 h-6 mb-1" />
|
|
31
|
+
<span className="text-[10px]">{tool.label}</span>
|
|
32
|
+
</button>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useImageStore } from '@/store/useImageStore';
|
|
2
|
+
import { getAspectRatioLabel } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
export const StatusBar = () => {
|
|
5
|
+
const { fileName, originalWidth, originalHeight, config } = useImageStore();
|
|
6
|
+
|
|
7
|
+
// 计算当前显示的分辨率
|
|
8
|
+
const currentWidth = config.resize?.width || originalWidth;
|
|
9
|
+
const currentHeight = config.resize?.height || originalHeight;
|
|
10
|
+
|
|
11
|
+
// 计算比例标签
|
|
12
|
+
const aspectRatio = getAspectRatioLabel(currentWidth, currentHeight);
|
|
13
|
+
|
|
14
|
+
// 如果没有图片,不显示状态栏
|
|
15
|
+
if (!fileName) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="h-6 bg-zinc-900 border-t border-zinc-800 flex items-center px-4 text-xs text-zinc-500 gap-4 shrink-0">
|
|
21
|
+
{fileName && (
|
|
22
|
+
<span className="text-zinc-400">
|
|
23
|
+
{fileName}
|
|
24
|
+
</span>
|
|
25
|
+
)}
|
|
26
|
+
{currentWidth > 0 && currentHeight > 0 && (
|
|
27
|
+
<>
|
|
28
|
+
<span className="text-zinc-600">|</span>
|
|
29
|
+
<span>
|
|
30
|
+
{currentWidth} × {currentHeight}
|
|
31
|
+
</span>
|
|
32
|
+
{aspectRatio && (
|
|
33
|
+
<>
|
|
34
|
+
<span className="text-zinc-600">|</span>
|
|
35
|
+
<span>
|
|
36
|
+
{aspectRatio.label}{aspectRatio.isApproximate && '(近似)'}
|
|
37
|
+
</span>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = setTimeout(() => {
|
|
8
|
+
setDebouncedValue(value);
|
|
9
|
+
}, delay);
|
|
10
|
+
|
|
11
|
+
return () => {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
};
|
|
14
|
+
}, [value, delay]);
|
|
15
|
+
|
|
16
|
+
return debouncedValue;
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
type Theme = 'light' | 'dark';
|
|
4
|
+
|
|
5
|
+
export function useTheme() {
|
|
6
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
7
|
+
const savedTheme = localStorage.getItem('theme') as Theme;
|
|
8
|
+
if (savedTheme) {
|
|
9
|
+
return savedTheme;
|
|
10
|
+
}
|
|
11
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
document.documentElement.classList.remove('light', 'dark');
|
|
16
|
+
document.documentElement.classList.add(theme);
|
|
17
|
+
localStorage.setItem('theme', theme);
|
|
18
|
+
}, [theme]);
|
|
19
|
+
|
|
20
|
+
const toggleTheme = () => {
|
|
21
|
+
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
theme,
|
|
26
|
+
toggleTheme,
|
|
27
|
+
isDark: theme === 'dark'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
import { initReactI18next } from 'react-i18next';
|
|
3
|
+
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
4
|
+
import en from './locales/en.json';
|
|
5
|
+
import zh from './locales/zh.json';
|
|
6
|
+
|
|
7
|
+
i18n
|
|
8
|
+
.use(LanguageDetector)
|
|
9
|
+
.use(initReactI18next)
|
|
10
|
+
.init({
|
|
11
|
+
resources: {
|
|
12
|
+
en: {
|
|
13
|
+
translation: en
|
|
14
|
+
},
|
|
15
|
+
zh: {
|
|
16
|
+
translation: zh
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
lng: 'zh', // Default language
|
|
20
|
+
fallbackLng: 'en',
|
|
21
|
+
interpolation: {
|
|
22
|
+
escapeValue: false // React already safes from xss
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default i18n;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"appName": "MjPic",
|
|
4
|
+
"open": "Open",
|
|
5
|
+
"save": "Save",
|
|
6
|
+
"undo": "Undo",
|
|
7
|
+
"redo": "Redo",
|
|
8
|
+
"reset": "Reset",
|
|
9
|
+
"dragDrop": "Drag and drop image here or use open button",
|
|
10
|
+
"width": "Width",
|
|
11
|
+
"height": "Height",
|
|
12
|
+
"saveSettings": "Save Settings",
|
|
13
|
+
"format": "Format",
|
|
14
|
+
"quality": "Quality",
|
|
15
|
+
"cancel": "Cancel",
|
|
16
|
+
"confirm": "Confirm",
|
|
17
|
+
"saveSuccess": "Image saved successfully",
|
|
18
|
+
"savePath": "Save Path",
|
|
19
|
+
"fileName": "File Name",
|
|
20
|
+
"copySuffix": "_copy",
|
|
21
|
+
"manageRatios": "Manage Aspect Ratios",
|
|
22
|
+
"add": "Add",
|
|
23
|
+
"labelOptional": "Label (Optional)",
|
|
24
|
+
"default": "Default",
|
|
25
|
+
"presetSizes": "Preset Sizes",
|
|
26
|
+
"lockAspectRatio": "Lock Aspect Ratio"
|
|
27
|
+
},
|
|
28
|
+
"sidebar": {
|
|
29
|
+
"enhance": "Enhance",
|
|
30
|
+
"adjust": "Adjust",
|
|
31
|
+
"resize": "Resize",
|
|
32
|
+
"crop": "Crop",
|
|
33
|
+
"rotate": "Rotate",
|
|
34
|
+
"border": "Border"
|
|
35
|
+
},
|
|
36
|
+
"panels": {
|
|
37
|
+
"autoEnhance": "Auto Enhance",
|
|
38
|
+
"fillLight": "Fill Light",
|
|
39
|
+
"autoWhiteBalance": "Auto White Balance",
|
|
40
|
+
"brightness": "Brightness",
|
|
41
|
+
"contrast": "Contrast",
|
|
42
|
+
"saturation": "Saturation",
|
|
43
|
+
"sharpness": "Sharpness",
|
|
44
|
+
"dimensions": "Dimensions",
|
|
45
|
+
"borderWidth": "Border Width",
|
|
46
|
+
"borderColor": "Border Color",
|
|
47
|
+
"aspectRatio": "Aspect Ratio",
|
|
48
|
+
"free": "Free",
|
|
49
|
+
"rotateLeft": "Rotate Left 90°",
|
|
50
|
+
"rotateRight": "Rotate Right 90°",
|
|
51
|
+
"straighten": "Straighten Tool",
|
|
52
|
+
"straightenTip": "Draw a line on the image to straighten it",
|
|
53
|
+
"rotationAngle": "Rotation Angle",
|
|
54
|
+
"cropWarning": "Crop interaction not yet implemented on canvas."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"appName": "敏捷图片",
|
|
4
|
+
"open": "打开",
|
|
5
|
+
"save": "保存",
|
|
6
|
+
"undo": "撤销",
|
|
7
|
+
"redo": "重做",
|
|
8
|
+
"reset": "重置",
|
|
9
|
+
"dragDrop": "拖拽图片到此处或使用打开按钮",
|
|
10
|
+
"width": "宽度",
|
|
11
|
+
"height": "高度",
|
|
12
|
+
"saveSettings": "保存设置",
|
|
13
|
+
"format": "格式",
|
|
14
|
+
"quality": "质量",
|
|
15
|
+
"cancel": "取消",
|
|
16
|
+
"confirm": "确定",
|
|
17
|
+
"saveSuccess": "图片保存成功",
|
|
18
|
+
"savePath": "保存路径",
|
|
19
|
+
"fileName": "文件名",
|
|
20
|
+
"copySuffix": "_副本",
|
|
21
|
+
"manageRatios": "管理比例",
|
|
22
|
+
"add": "添加",
|
|
23
|
+
"labelOptional": "标签(可选)",
|
|
24
|
+
"default": "默认",
|
|
25
|
+
"presetSizes": "预设尺寸",
|
|
26
|
+
"lockAspectRatio": "锁定宽高比"
|
|
27
|
+
},
|
|
28
|
+
"sidebar": {
|
|
29
|
+
"enhance": "增强",
|
|
30
|
+
"adjust": "调整",
|
|
31
|
+
"resize": "尺寸",
|
|
32
|
+
"crop": "裁剪",
|
|
33
|
+
"rotate": "旋转",
|
|
34
|
+
"border": "边框"
|
|
35
|
+
},
|
|
36
|
+
"panels": {
|
|
37
|
+
"autoEnhance": "自动美化",
|
|
38
|
+
"fillLight": "一键补光",
|
|
39
|
+
"autoWhiteBalance": "自动白平衡",
|
|
40
|
+
"brightness": "亮度",
|
|
41
|
+
"contrast": "对比度",
|
|
42
|
+
"saturation": "饱和度",
|
|
43
|
+
"sharpness": "清晰度",
|
|
44
|
+
"dimensions": "尺寸",
|
|
45
|
+
"borderWidth": "边框宽度",
|
|
46
|
+
"borderColor": "边框颜色",
|
|
47
|
+
"borderHorizontal": "水平边框",
|
|
48
|
+
"borderVertical": "垂直边框",
|
|
49
|
+
"borderSize": "边框尺寸",
|
|
50
|
+
"aspectRatio": "裁剪比例",
|
|
51
|
+
"free": "自由",
|
|
52
|
+
"rotateLeft": "向左旋转90°",
|
|
53
|
+
"rotateRight": "向右旋转90°",
|
|
54
|
+
"straighten": "拉直工具",
|
|
55
|
+
"straightenTip": "在图片上画一条线来校正水平",
|
|
56
|
+
"rotationAngle": "旋转角度",
|
|
57
|
+
"cropWarning": "画布上的交互式裁剪功能暂未实现。"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
7
|
+
line-height: 1.5;
|
|
8
|
+
font-weight: 400;
|
|
9
|
+
|
|
10
|
+
font-synthesis: none;
|
|
11
|
+
text-rendering: optimizeLegibility;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
-moz-osx-font-smoothing: grayscale;
|
|
14
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 常见比例列表
|
|
9
|
+
const COMMON_RATIOS = [
|
|
10
|
+
{ ratio: 1, label: '1:1' },
|
|
11
|
+
{ ratio: 1.333, label: '4:3' },
|
|
12
|
+
{ ratio: 1.5, label: '3:2' },
|
|
13
|
+
{ ratio: 1.6, label: '16:10' },
|
|
14
|
+
{ ratio: 1.778, label: '16:9' },
|
|
15
|
+
{ ratio: 0.667, label: '2:3' },
|
|
16
|
+
{ ratio: 0.75, label: '3:4' },
|
|
17
|
+
{ ratio: 0.5625, label: '9:16' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// 计算最大公约数
|
|
21
|
+
function gcd(a: number, b: number): number {
|
|
22
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AspectRatioResult {
|
|
26
|
+
label: string;
|
|
27
|
+
isApproximate: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 将宽高比转换为整数比例格式
|
|
31
|
+
export function getAspectRatioLabel(width: number, height: number): AspectRatioResult | null {
|
|
32
|
+
if (width <= 0 || height <= 0) return null;
|
|
33
|
+
|
|
34
|
+
const aspectRatio = width / height;
|
|
35
|
+
|
|
36
|
+
// 先尝试找到常见比例的精确匹配
|
|
37
|
+
for (const common of COMMON_RATIOS) {
|
|
38
|
+
if (Math.abs(aspectRatio - common.ratio) < 0.02) {
|
|
39
|
+
return { label: common.label, isApproximate: false };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 再尝试找到常见比例的近似匹配
|
|
44
|
+
for (const common of COMMON_RATIOS) {
|
|
45
|
+
if (Math.abs(aspectRatio - common.ratio) < 0.05) {
|
|
46
|
+
return { label: common.label, isApproximate: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 如果没有找到常见比例,则计算最大公约数并转换为整数比例
|
|
51
|
+
const divisor = gcd(width, height);
|
|
52
|
+
const ratioWidth = width / divisor;
|
|
53
|
+
const ratioHeight = height / divisor;
|
|
54
|
+
|
|
55
|
+
// 简化比例,如果分子或分母太大则取近似值
|
|
56
|
+
if (ratioWidth > 100 || ratioHeight > 100) {
|
|
57
|
+
// 尝试用常见比例中最接近的
|
|
58
|
+
let closest = COMMON_RATIOS[0];
|
|
59
|
+
let minDiff = Math.abs(aspectRatio - COMMON_RATIOS[0].ratio);
|
|
60
|
+
|
|
61
|
+
for (const common of COMMON_RATIOS) {
|
|
62
|
+
const diff = Math.abs(aspectRatio - common.ratio);
|
|
63
|
+
if (diff < minDiff) {
|
|
64
|
+
minDiff = diff;
|
|
65
|
+
closest = common;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { label: closest.label, isApproximate: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { label: `${ratioWidth}:${ratioHeight}`, isApproximate: false };
|
|
73
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useImageStore } from '@/store/useImageStore';
|
|
3
|
+
import { Header } from '@/components/layout/Header';
|
|
4
|
+
import { Sidebar } from '@/components/layout/Sidebar';
|
|
5
|
+
import { CanvasArea } from '@/components/layout/CanvasArea';
|
|
6
|
+
import { RightPanel } from '@/components/layout/RightPanel';
|
|
7
|
+
import { StatusBar } from '@/components/layout/StatusBar';
|
|
8
|
+
import Konva from 'konva';
|
|
9
|
+
|
|
10
|
+
export default function Home() {
|
|
11
|
+
const { loadImage } = useImageStore();
|
|
12
|
+
const stageRef = useRef<Konva.Stage>(null);
|
|
13
|
+
|
|
14
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
e.stopPropagation();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
e.stopPropagation();
|
|
22
|
+
|
|
23
|
+
const files = e.dataTransfer.files;
|
|
24
|
+
if (files && files.length > 0) {
|
|
25
|
+
const file = files[0];
|
|
26
|
+
if (file.type.startsWith('image/')) {
|
|
27
|
+
const reader = new FileReader();
|
|
28
|
+
reader.onload = (event) => {
|
|
29
|
+
if (event.target?.result) {
|
|
30
|
+
loadImage(event.target.result as string, file.name, file.name);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
reader.readAsDataURL(file);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fetch('/api/current-image')
|
|
40
|
+
.then(res => {
|
|
41
|
+
if (res.ok) return res.json();
|
|
42
|
+
throw new Error('No image');
|
|
43
|
+
})
|
|
44
|
+
.then(data => {
|
|
45
|
+
if (data && data.path) {
|
|
46
|
+
// Extract filename from path
|
|
47
|
+
// Handle both Windows (\) and Unix (/) paths
|
|
48
|
+
const name = data.path.split(/[/\\]/).pop();
|
|
49
|
+
loadImage('/api/image-content', name, data.path);
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.catch(() => {
|
|
53
|
+
// Silently fail if no image provided
|
|
54
|
+
});
|
|
55
|
+
}, [loadImage]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className="flex flex-col h-screen bg-zinc-950 text-zinc-100 overflow-hidden"
|
|
60
|
+
onDragOver={handleDragOver}
|
|
61
|
+
onDrop={handleDrop}
|
|
62
|
+
>
|
|
63
|
+
<Header stageRef={stageRef} />
|
|
64
|
+
<div className="flex flex-1 overflow-hidden">
|
|
65
|
+
<Sidebar />
|
|
66
|
+
<CanvasArea stageRef={stageRef} />
|
|
67
|
+
<RightPanel />
|
|
68
|
+
</div>
|
|
69
|
+
<StatusBar />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|