add setting
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -22,17 +22,19 @@ const Editor: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false)
|
const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false)
|
||||||
const { loadPages, pages } = useEditorStore()
|
const { loadPages, pages, documentSettings } = useEditorStore()
|
||||||
const { data } = useGetCatalogById(id!)
|
const { data } = useGetCatalogById(id!)
|
||||||
const { scheduleUpdate, isSaving } = useUpdateCatalog()
|
const { scheduleUpdate, isSaving } = useUpdateCatalog()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.data && data?.data?.content) {
|
if (data?.data && data?.data?.content) {
|
||||||
const json = JSON.parse(data?.data?.content)
|
const json = JSON.parse(data?.data?.content)
|
||||||
console.log(json);
|
if (!json) return
|
||||||
|
|
||||||
if (json) {
|
if (Array.isArray(json)) {
|
||||||
loadPages(json)
|
loadPages(json)
|
||||||
|
} else if (json.pages) {
|
||||||
|
loadPages(json.pages, json.documentSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +47,11 @@ const Editor: FC = () => {
|
|||||||
|
|
||||||
scheduleUpdate({
|
scheduleUpdate({
|
||||||
id,
|
id,
|
||||||
content: JSON.stringify(pages),
|
content: JSON.stringify({ pages, documentSettings }),
|
||||||
})
|
})
|
||||||
// عمداً scheduleUpdate را در وابستگیها نیاوردیم تا از لوپ ذخیرهسازی جلوگیری شود
|
// عمداً scheduleUpdate را در وابستگیها نیاوردیم تا از لوپ ذخیرهسازی جلوگیری شود
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [id, pages])
|
}, [id, pages, documentSettings])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Stage, Layer, Rect } from "react-konva";
|
import { Stage, Layer, Rect, Image as KonvaImage } from "react-konva";
|
||||||
|
import useImage from "use-image";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import { useEditorStore } from "../store/editorStore";
|
import { useEditorStore } from "../store/editorStore";
|
||||||
import { useDrawingHandlers } from "./canvas/useDrawingHandlers";
|
import { useDrawingHandlers } from "./canvas/useDrawingHandlers";
|
||||||
@@ -39,10 +40,38 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
setLayerRef,
|
setLayerRef,
|
||||||
setGuides,
|
setGuides,
|
||||||
zoom,
|
zoom,
|
||||||
|
documentSettings,
|
||||||
} = useEditorStore();
|
} = useEditorStore();
|
||||||
|
|
||||||
const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers();
|
const bgColor =
|
||||||
|
documentSettings.backgroundType === 'color'
|
||||||
|
? documentSettings.backgroundColor
|
||||||
|
: '#ffffff';
|
||||||
|
|
||||||
|
const [bgImage] = useImage(
|
||||||
|
documentSettings.backgroundType === 'image'
|
||||||
|
? documentSettings.backgroundImageUrl
|
||||||
|
: '',
|
||||||
|
);
|
||||||
const stageSize = useStageSize(catalogSize);
|
const stageSize = useStageSize(catalogSize);
|
||||||
|
const bgImageCrop = (() => {
|
||||||
|
if (!bgImage) return null;
|
||||||
|
|
||||||
|
const containerRatio = stageSize.width / stageSize.height;
|
||||||
|
const imageRatio = bgImage.width / bgImage.height;
|
||||||
|
|
||||||
|
if (imageRatio > containerRatio) {
|
||||||
|
const cropWidth = bgImage.height * containerRatio;
|
||||||
|
const cropX = (bgImage.width - cropWidth) / 2;
|
||||||
|
return { x: cropX, y: 0, width: cropWidth, height: bgImage.height };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cropHeight = bgImage.width / containerRatio;
|
||||||
|
const cropY = (bgImage.height - cropHeight) / 2;
|
||||||
|
return { x: 0, y: cropY, width: bgImage.width, height: cropHeight };
|
||||||
|
})();
|
||||||
|
|
||||||
|
const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers();
|
||||||
useKeyboardMovement();
|
useKeyboardMovement();
|
||||||
|
|
||||||
const { handleSelect, handleCellClick, handleCellDblClick } = useSelectionHandlers();
|
const { handleSelect, handleCellClick, handleCellDblClick } = useSelectionHandlers();
|
||||||
@@ -362,9 +391,20 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
y={0}
|
y={0}
|
||||||
width={stageSize.width}
|
width={stageSize.width}
|
||||||
height={stageSize.height}
|
height={stageSize.height}
|
||||||
fill="#ffffff"
|
fill={bgColor}
|
||||||
listening={false}
|
listening={false}
|
||||||
/>
|
/>
|
||||||
|
{documentSettings.backgroundType === 'image' && bgImage && (
|
||||||
|
<KonvaImage
|
||||||
|
image={bgImage}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={stageSize.width}
|
||||||
|
height={stageSize.height}
|
||||||
|
crop={bgImageCrop ?? undefined}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Layer>
|
</Layer>
|
||||||
|
|
||||||
<ObjectsLayer
|
<ObjectsLayer
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import { Layer, Book1, ArrowLeft2 } from 'iconsax-react'
|
import { Layer, Book1, ArrowLeft2, Setting2 } from 'iconsax-react'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import PagesPanel from './PagesPanel'
|
import PagesPanel from './PagesPanel'
|
||||||
import LayersList from './LayersList'
|
import LayersList from './LayersList'
|
||||||
|
import SettingsPanel from './SettingsPanel'
|
||||||
|
|
||||||
type TabType = 'pages' | 'layers'
|
type TabType = 'pages' | 'layers' | 'settings'
|
||||||
|
|
||||||
type LayersPanelProps = {
|
type LayersPanelProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
setIsOpen: (isOpen: boolean) => void
|
setIsOpen: (isOpen: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TAB_LABELS: Record<TabType, string> = {
|
||||||
|
pages: 'صفحات',
|
||||||
|
layers: 'لایه ها',
|
||||||
|
settings: 'تنظیمات',
|
||||||
|
}
|
||||||
|
|
||||||
const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
|
const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('pages')
|
const [activeTab, setActiveTab] = useState<TabType>('pages')
|
||||||
|
|
||||||
@@ -19,23 +26,23 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
|
|||||||
<div className="fixed left-4 top-[100px] bottom-4 z-30 flex flex-col">
|
<div className="fixed left-4 top-[100px] bottom-4 z-30 flex flex-col">
|
||||||
<div className="bg-white rounded-t-[32px] rounded-b-[32px] w-16 h-full flex flex-col items-center gap-4 p-4">
|
<div className="bg-white rounded-t-[32px] rounded-b-[32px] w-16 h-full flex flex-col items-center gap-4 p-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => { setActiveTab('pages'); setIsOpen(true) }}
|
||||||
setActiveTab('pages')
|
|
||||||
setIsOpen(true)
|
|
||||||
}}
|
|
||||||
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
|
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
>
|
>
|
||||||
<Book1 size={24} color="black" />
|
<Book1 size={24} color="black" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => { setActiveTab('layers'); setIsOpen(true) }}
|
||||||
setActiveTab('layers')
|
|
||||||
setIsOpen(true)
|
|
||||||
}}
|
|
||||||
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
|
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
>
|
>
|
||||||
<Layer size={24} color="black" />
|
<Layer size={24} color="black" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setActiveTab('settings'); setIsOpen(true) }}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<Setting2 size={24} color="black" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -45,28 +52,34 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
|
|||||||
<div className="fixed left-4 top-[100px] bottom-4 z-30 flex flex-col">
|
<div className="fixed left-4 top-[100px] bottom-4 z-30 flex flex-col">
|
||||||
<div className="bg-white rounded-l-4xl w-16 h-full flex flex-col items-center gap-4 p-4">
|
<div className="bg-white rounded-l-4xl w-16 h-full flex flex-col items-center gap-4 p-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => setActiveTab('pages')}
|
||||||
setActiveTab('pages')
|
|
||||||
}}
|
|
||||||
className={clx(
|
className={clx(
|
||||||
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
||||||
activeTab === 'pages' ? 'bg-white' : 'hover:bg-gray-200'
|
activeTab === 'pages' ? 'bg-gray-100' : 'hover:bg-gray-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Book1 variant={activeTab === 'pages' ? 'Bold' : 'Outline'} size={24} color="black" />
|
<Book1 variant={activeTab === 'pages' ? 'Bold' : 'Outline'} size={24} color="black" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => setActiveTab('layers')}
|
||||||
setActiveTab('layers')
|
|
||||||
}}
|
|
||||||
className={clx(
|
className={clx(
|
||||||
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
||||||
activeTab === 'layers' ? 'bg-white' : 'hover:bg-gray-200'
|
activeTab === 'layers' ? 'bg-gray-100' : 'hover:bg-gray-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Layer variant={activeTab === 'layers' ? 'Bold' : 'Outline'} size={24} color="black" />
|
<Layer variant={activeTab === 'layers' ? 'Bold' : 'Outline'} size={24} color="black" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('settings')}
|
||||||
|
className={clx(
|
||||||
|
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
||||||
|
activeTab === 'settings' ? 'bg-gray-100' : 'hover:bg-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Setting2 variant={activeTab === 'settings' ? 'Bold' : 'Outline'} size={24} color="black" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="fixed border-l border-border left-20 top-[100px] bottom-4 z-30 flex flex-col w-[232px]">
|
<div className="fixed border-l border-border left-20 top-[100px] bottom-4 z-30 flex flex-col w-[232px]">
|
||||||
@@ -80,12 +93,14 @@ const LayersPanel = ({ isOpen, setIsOpen }: LayersPanelProps) => {
|
|||||||
<ArrowLeft2 size={20} color="black" />
|
<ArrowLeft2 size={20} color="black" />
|
||||||
</button>
|
</button>
|
||||||
<h2 className="text-lg font-medium">
|
<h2 className="text-lg font-medium">
|
||||||
{activeTab === 'pages' ? 'صفحات' : 'لایه ها'}
|
{TAB_LABELS[activeTab]}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'pages' ? <PagesPanel /> : <LayersList />}
|
{activeTab === 'pages' && <PagesPanel />}
|
||||||
|
{activeTab === 'layers' && <LayersList />}
|
||||||
|
{activeTab === 'settings' && <SettingsPanel />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { clx } from '@/helpers/utils'
|
||||||
|
import { useEditorStore } from '../store/editorStore'
|
||||||
|
import type { DisplayStyle, BackgroundType } from '../store/editorStore'
|
||||||
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||||
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
||||||
|
import ColorsImage from '@/assets/images/colors.png'
|
||||||
|
|
||||||
|
const PRESET_COLORS = [
|
||||||
|
'#a8edcf',
|
||||||
|
'#e91e8c',
|
||||||
|
'#ff9800',
|
||||||
|
'#6d8b8b',
|
||||||
|
'#111111',
|
||||||
|
'#e53935',
|
||||||
|
'#e0e0e0',
|
||||||
|
]
|
||||||
|
|
||||||
|
const DISPLAY_STYLE_OPTIONS: { value: DisplayStyle; label: string }[] = [
|
||||||
|
{ value: 'single', label: 'تک صفحه ای' },
|
||||||
|
{ value: 'double', label: 'دو صفحه ای' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const BACKGROUND_TYPE_OPTIONS: { value: BackgroundType; label: string }[] = [
|
||||||
|
{ value: 'color', label: 'رنگ' },
|
||||||
|
{ value: 'image', label: 'تصویر' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const SettingsPanel = () => {
|
||||||
|
const { documentSettings, updateDocumentSettings } = useEditorStore()
|
||||||
|
const colorInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { mutateAsync: uploadFile, isPending: imageLoading } = useSingleUpload()
|
||||||
|
const [uploadedPreviews, setUploadedPreviews] = useState<string[]>(
|
||||||
|
documentSettings.backgroundImageUrl ? [documentSettings.backgroundImageUrl] : []
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayStyle,
|
||||||
|
autoPlay,
|
||||||
|
pageFlipSound,
|
||||||
|
leftToRightFlip,
|
||||||
|
backgroundType,
|
||||||
|
backgroundColor,
|
||||||
|
} = documentSettings
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUploadedPreviews(
|
||||||
|
documentSettings.backgroundImageUrl ? [documentSettings.backgroundImageUrl] : []
|
||||||
|
)
|
||||||
|
}, [documentSettings.backgroundImageUrl])
|
||||||
|
|
||||||
|
const handleImageFiles = async (files: File[]) => {
|
||||||
|
if (files.length === 0) return
|
||||||
|
const file = files[files.length - 1]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await uploadFile(file)
|
||||||
|
const imageUrl = result?.data?.url
|
||||||
|
if (!imageUrl) return
|
||||||
|
|
||||||
|
updateDocumentSettings({ backgroundImageUrl: imageUrl })
|
||||||
|
setUploadedPreviews([imageUrl])
|
||||||
|
} catch {
|
||||||
|
// TODO: show error toast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviewChange = (previews: string[]) => {
|
||||||
|
setUploadedPreviews(previews)
|
||||||
|
if (previews.length === 0) {
|
||||||
|
updateDocumentSettings({ backgroundImageUrl: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-0 overflow-y-auto h-full text-right" dir="rtl">
|
||||||
|
|
||||||
|
{/* ─── تنظیمات نمایش ─── */}
|
||||||
|
<section className="p-4 border-b border-border">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">تنظیمات نمایش</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* استایل نمایش */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-xs text-gray-500 mb-2">استایل نمایش</label>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={displayStyle}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateDocumentSettings({ displayStyle: e.target.value as DisplayStyle })
|
||||||
|
}
|
||||||
|
className="w-full appearance-none border border-border rounded-xl px-3 py-2.5 text-sm bg-white text-right pr-3 pl-8 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-200"
|
||||||
|
>
|
||||||
|
{DISPLAY_STYLE_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||||
|
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* چکباکسها */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<CheckboxRow
|
||||||
|
label="پخش خودکار"
|
||||||
|
checked={autoPlay}
|
||||||
|
onChange={(v) => updateDocumentSettings({ autoPlay: v })}
|
||||||
|
/>
|
||||||
|
<CheckboxRow
|
||||||
|
label="صدای برگ خوردن"
|
||||||
|
checked={pageFlipSound}
|
||||||
|
onChange={(v) => updateDocumentSettings({ pageFlipSound: v })}
|
||||||
|
/>
|
||||||
|
<CheckboxRow
|
||||||
|
label="چپ به راست ورق خوردن"
|
||||||
|
checked={leftToRightFlip}
|
||||||
|
onChange={(v) => updateDocumentSettings({ leftToRightFlip: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ─── پس زمینه ─── */}
|
||||||
|
<section className="p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800 mb-4">پس زمینه</h3>
|
||||||
|
|
||||||
|
{/* نوع پسزمینه */}
|
||||||
|
<div className="flex items-center gap-5 mb-4">
|
||||||
|
{BACKGROUND_TYPE_OPTIONS.map((opt) => (
|
||||||
|
<label key={opt.value} className="flex items-center gap-1.5 cursor-pointer select-none">
|
||||||
|
<span className="text-xs text-gray-600">{opt.label}</span>
|
||||||
|
<div
|
||||||
|
onClick={() => updateDocumentSettings({ backgroundType: opt.value })}
|
||||||
|
className={clx(
|
||||||
|
'w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors cursor-pointer',
|
||||||
|
backgroundType === opt.value
|
||||||
|
? 'border-gray-800 bg-gray-800'
|
||||||
|
: 'border-gray-300 bg-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{backgroundType === opt.value && (
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── انتخاب رنگ ─── */}
|
||||||
|
{backgroundType === 'color' && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 mb-3">انتخاب رنگ</p>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{PRESET_COLORS.map((color) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
onClick={() => updateDocumentSettings({ backgroundColor: color })}
|
||||||
|
className={clx(
|
||||||
|
'w-9 h-9 rounded-xl border-2 transition-all',
|
||||||
|
backgroundColor === color
|
||||||
|
? 'border-gray-800 scale-110'
|
||||||
|
: 'border-transparent hover:scale-105'
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
title={color}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Color Wheel */}
|
||||||
|
<button
|
||||||
|
onClick={() => colorInputRef.current?.click()}
|
||||||
|
className="w-9 h-9 rounded-xl border-2 border-transparent hover:scale-105 transition-all overflow-hidden relative"
|
||||||
|
title="رنگ دلخواه"
|
||||||
|
>
|
||||||
|
<img src={ColorsImage} alt="انتخاب رنگ" className="w-full h-full object-cover" />
|
||||||
|
<input
|
||||||
|
ref={colorInputRef}
|
||||||
|
type="color"
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={(e) => updateDocumentSettings({ backgroundColor: e.target.value })}
|
||||||
|
className="absolute inset-0 opacity-0 w-full h-full cursor-pointer"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* نمایش رنگ انتخابشده */}
|
||||||
|
<div className="mt-3 flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-6 h-6 rounded-lg border border-border"
|
||||||
|
style={{ backgroundColor }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 font-mono">{backgroundColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ─── آپلود تصویر ─── */}
|
||||||
|
{backgroundType === 'image' && (
|
||||||
|
<div>
|
||||||
|
{imageLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-8">
|
||||||
|
<span className="h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-gray-800" />
|
||||||
|
<span className="text-xs text-gray-500">در حال آپلود...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label="تصویر پسزمینه را آپلود کنید"
|
||||||
|
onChange={handleImageFiles}
|
||||||
|
isMultiple={false}
|
||||||
|
preview={uploadedPreviews}
|
||||||
|
onChangePreview={handlePreviewChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckboxRowProps = {
|
||||||
|
label: string
|
||||||
|
checked: boolean
|
||||||
|
onChange: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckboxRow = ({ label, checked, onChange }: CheckboxRowProps) => (
|
||||||
|
<label className="flex items-center justify-between cursor-pointer select-none group">
|
||||||
|
<span className="text-sm text-gray-700">{label}</span>
|
||||||
|
<div
|
||||||
|
onClick={() => onChange(!checked)}
|
||||||
|
className={clx(
|
||||||
|
'w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all',
|
||||||
|
checked
|
||||||
|
? 'border-gray-800 bg-gray-800'
|
||||||
|
: 'border-gray-300 bg-white group-hover:border-gray-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{checked && (
|
||||||
|
<svg width="10" height="8" viewBox="0 0 10 8" fill="none">
|
||||||
|
<path
|
||||||
|
d="M1 4l3 3 5-6"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SettingsPanel
|
||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableData,
|
TableData,
|
||||||
ToolType,
|
ToolType,
|
||||||
|
DocumentSettings,
|
||||||
} from "./editorStore.types";
|
} from "./editorStore.types";
|
||||||
|
|
||||||
const MAX_OBJECT_HISTORY = 50;
|
const MAX_OBJECT_HISTORY = 50;
|
||||||
@@ -38,6 +39,9 @@ export type {
|
|||||||
TableCell,
|
TableCell,
|
||||||
TableData,
|
TableData,
|
||||||
ToolType,
|
ToolType,
|
||||||
|
DocumentSettings,
|
||||||
|
DisplayStyle,
|
||||||
|
BackgroundType,
|
||||||
} from "./editorStore.types";
|
} from "./editorStore.types";
|
||||||
|
|
||||||
type EditorStoreType = {
|
type EditorStoreType = {
|
||||||
@@ -117,7 +121,7 @@ type EditorStoreType = {
|
|||||||
toggleObjectVisibility: (id: string) => void;
|
toggleObjectVisibility: (id: string) => void;
|
||||||
groupObjects: (objectIds: string[]) => void;
|
groupObjects: (objectIds: string[]) => void;
|
||||||
ungroupObjects: (groupId: string) => void;
|
ungroupObjects: (groupId: string) => void;
|
||||||
loadPages: (pages: Page[]) => void;
|
loadPages: (pages: Page[], documentSettings?: Partial<DocumentSettings>) => void;
|
||||||
clipboardObjects: EditorObject[];
|
clipboardObjects: EditorObject[];
|
||||||
objectHistoryPast: EditorObject[][];
|
objectHistoryPast: EditorObject[][];
|
||||||
objectHistoryFuture: EditorObject[][];
|
objectHistoryFuture: EditorObject[][];
|
||||||
@@ -128,6 +132,8 @@ type EditorStoreType = {
|
|||||||
redoObjects: () => void;
|
redoObjects: () => void;
|
||||||
bringObjectForward: (id: string) => void;
|
bringObjectForward: (id: string) => void;
|
||||||
sendObjectBackward: (id: string) => void;
|
sendObjectBackward: (id: string) => void;
|
||||||
|
documentSettings: DocumentSettings;
|
||||||
|
updateDocumentSettings: (updates: Partial<DocumentSettings>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useEditorStore = create<EditorStoreType>((set, get) => {
|
export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||||
@@ -172,6 +178,19 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
documentSettings: {
|
||||||
|
displayStyle: 'double',
|
||||||
|
autoPlay: false,
|
||||||
|
pageFlipSound: true,
|
||||||
|
leftToRightFlip: false,
|
||||||
|
backgroundType: 'color',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
backgroundImageUrl: '',
|
||||||
|
},
|
||||||
|
updateDocumentSettings: (updates) =>
|
||||||
|
set((state) => ({
|
||||||
|
documentSettings: { ...state.documentSettings, ...updates },
|
||||||
|
})),
|
||||||
tool: "select",
|
tool: "select",
|
||||||
setTool: (tool) =>
|
setTool: (tool) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
@@ -957,7 +976,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
const state = get();
|
const state = get();
|
||||||
return state.objects.filter((obj) => obj.groupId === groupId);
|
return state.objects.filter((obj) => obj.groupId === groupId);
|
||||||
},
|
},
|
||||||
loadPages: (pages) => {
|
loadPages: (pages, documentSettings) => {
|
||||||
if (pages.length === 0) return;
|
if (pages.length === 0) return;
|
||||||
const normalizedPages = pages.map((page) => ({
|
const normalizedPages = pages.map((page) => ({
|
||||||
...page,
|
...page,
|
||||||
@@ -966,6 +985,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
objects: ensureMissingGroupObjects(page.objects || []),
|
objects: ensureMissingGroupObjects(page.objects || []),
|
||||||
}));
|
}));
|
||||||
const firstPage = normalizedPages[0];
|
const firstPage = normalizedPages[0];
|
||||||
|
const currentSettings = get().documentSettings;
|
||||||
set({
|
set({
|
||||||
pages: normalizedPages,
|
pages: normalizedPages,
|
||||||
currentPageId: firstPage.id,
|
currentPageId: firstPage.id,
|
||||||
@@ -975,6 +995,9 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
selectedObjectIds: [],
|
selectedObjectIds: [],
|
||||||
objectHistoryPast: [],
|
objectHistoryPast: [],
|
||||||
objectHistoryFuture: [],
|
objectHistoryFuture: [],
|
||||||
|
...(documentSettings && {
|
||||||
|
documentSettings: { ...currentSettings, ...documentSettings },
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,3 +81,16 @@ export type Page = {
|
|||||||
objects: EditorObject[];
|
objects: EditorObject[];
|
||||||
guides: PageGuide[];
|
guides: PageGuide[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DisplayStyle = 'single' | 'double';
|
||||||
|
export type BackgroundType = 'color' | 'image';
|
||||||
|
|
||||||
|
export type DocumentSettings = {
|
||||||
|
displayStyle: DisplayStyle;
|
||||||
|
autoPlay: boolean;
|
||||||
|
pageFlipSound: boolean;
|
||||||
|
leftToRightFlip: boolean;
|
||||||
|
backgroundType: BackgroundType;
|
||||||
|
backgroundColor: string;
|
||||||
|
backgroundImageUrl: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user