suisuyy
Rename project from xpaintai to paintai; update canvas dimensions and AI API endpoint; enhance AI features with dimensions mode
0817dc1
| import React from 'react'; | |
| import { CloseIcon, FullscreenEnterIcon, FullscreenExitIcon } from './icons'; // Import fullscreen icons | |
| import { AiImageQuality, AiDimensionsMode } from '../hooks/useAiFeatures'; | |
| interface SettingsPanelProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| showZoomSlider: boolean; | |
| onShowZoomSliderChange: (show: boolean) => void; | |
| aiImageQuality: AiImageQuality; | |
| onAiImageQualityChange: (quality: AiImageQuality) => void; | |
| aiApiEndpoint: string; | |
| onAiApiEndpointChange: (endpoint: string) => void; | |
| aiDimensionsMode: AiDimensionsMode; | |
| onAiDimensionsModeChange: (mode: AiDimensionsMode) => void; | |
| isFullscreenActive: boolean; | |
| onToggleFullscreen: () => void; | |
| currentCanvasWidth: number; | |
| currentCanvasHeight: number; | |
| onCanvasSizeChange: (width: number, height: number) => void; | |
| } | |
| const CANVAS_SIZE_OPTIONS = [ | |
| { label: '1024 x 1536 px', width: 1024, height: 1536 }, | |
| { label: '1024 x 1024 px', width: 1024, height: 1024 }, | |
| { label: '1200 x 1200 px', width: 1200, height: 1200 }, | |
| { label: '1600 x 1600 px', width: 1600, height: 1600 }, | |
| { label: '2000 x 2000 px', width: 2000, height: 2000 }, | |
| { label: '4000 x 4000 px', width: 4000, height: 4000 }, | |
| ]; | |
| const AI_API_OPTIONS = [ | |
| { | |
| label: 'Pollinations AI', | |
| value: 'https://rpfor.deno.dev/proxy?url=https://image.pollinations.ai/prompt/{prompt}?nologo=true&model=gptimage&image={imgurl.url}&quality={quality}{size_params}' | |
| }, | |
| { | |
| label: 'Gemini Simple Get', | |
| value: 'https://geminisimpleget.deno.dev/prompt/{prompt}?image={imgurl.url}' | |
| } | |
| ]; | |
| const SettingsPanel: React.FC<SettingsPanelProps> = ({ | |
| isOpen, | |
| onClose, | |
| showZoomSlider, | |
| onShowZoomSliderChange, | |
| aiImageQuality, | |
| onAiImageQualityChange, | |
| aiApiEndpoint, | |
| onAiApiEndpointChange, | |
| aiDimensionsMode, | |
| onAiDimensionsModeChange, | |
| isFullscreenActive, | |
| onToggleFullscreen, | |
| currentCanvasWidth, | |
| currentCanvasHeight, | |
| onCanvasSizeChange, | |
| }) => { | |
| if (!isOpen) return null; | |
| const qualityOptions: { label: string; value: AiImageQuality }[] = [ | |
| { label: 'Low', value: 'low' }, | |
| { label: 'Medium', value: 'medium' }, | |
| { label: 'High (HD)', value: 'hd' }, | |
| ]; | |
| const handleSizeSelection = (event: React.ChangeEvent<HTMLSelectElement>) => { | |
| const selectedValue = event.target.value; | |
| const [newWidth, newHeight] = selectedValue.split('x').map(Number); | |
| if (!isNaN(newWidth) && !isNaN(newHeight)) { | |
| onCanvasSizeChange(newWidth, newHeight); | |
| } | |
| }; | |
| const currentSizeValue = `${currentCanvasWidth}x${currentCanvasHeight}`; | |
| return ( | |
| <div | |
| className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex justify-center items-center z-[1000] p-4" | |
| aria-modal="true" | |
| role="dialog" | |
| aria-labelledby="settings-panel-title" | |
| > | |
| <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-md transform transition-all overflow-y-auto max-h-[90vh]"> | |
| <div className="flex justify-between items-center mb-6"> | |
| <h2 id="settings-panel-title" className="text-xl font-semibold text-slate-800"> | |
| Application Settings | |
| </h2> | |
| <button | |
| onClick={onClose} | |
| className="text-slate-400 hover:text-slate-600 transition-colors" | |
| aria-label="Close settings panel" | |
| > | |
| <CloseIcon className="w-6 h-6" /> | |
| </button> | |
| </div> | |
| {/* Interface Settings */} | |
| <div className="mb-6"> | |
| <h3 className="text-md font-medium text-slate-700 mb-2">Interface</h3> | |
| <div className="space-y-3"> | |
| {/* Zoom Slider Toggle */} | |
| <div className="flex items-center justify-between bg-slate-50 p-3 rounded-md border border-slate-200"> | |
| <label htmlFor="showZoomSliderToggle" className="text-sm text-slate-600"> | |
| Show Zoom Slider | |
| </label> | |
| <button | |
| id="showZoomSliderToggle" | |
| role="switch" | |
| aria-checked={showZoomSlider} | |
| onClick={() => onShowZoomSliderChange(!showZoomSlider)} | |
| className={`relative inline-flex items-center h-6 rounded-full w-11 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${ | |
| showZoomSlider ? 'bg-blue-600' : 'bg-slate-300' | |
| }`} | |
| > | |
| <span className="sr-only">Toggle Zoom Slider Visibility</span> | |
| <span | |
| className={`inline-block w-4 h-4 transform bg-white rounded-full transition-transform duration-200 ease-in-out ${ | |
| showZoomSlider ? 'translate-x-6' : 'translate-x-1' | |
| }`} | |
| /> | |
| </button> | |
| </div> | |
| {/* Fullscreen Toggle */} | |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200"> | |
| <button | |
| onClick={onToggleFullscreen} | |
| className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors" | |
| aria-label={isFullscreenActive ? 'Exit Fullscreen' : 'Enter Fullscreen'} | |
| > | |
| {isFullscreenActive ? ( | |
| <FullscreenExitIcon className="w-5 h-5 mr-2" /> | |
| ) : ( | |
| <FullscreenEnterIcon className="w-5 h-5 mr-2" /> | |
| )} | |
| {isFullscreenActive ? 'Exit Fullscreen' : 'Enter Fullscreen'} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Canvas Dimensions Settings */} | |
| <div className="mb-6"> | |
| <h3 className="text-md font-medium text-slate-700 mb-2">Canvas Dimensions</h3> | |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200"> | |
| <label htmlFor="canvasSizeSelect" className="block text-sm text-slate-600 mb-1"> | |
| Canvas Size | |
| </label> | |
| <select | |
| id="canvasSizeSelect" | |
| value={currentSizeValue} | |
| onChange={handleSizeSelection} | |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" | |
| aria-describedby="canvas-size-description" | |
| > | |
| {CANVAS_SIZE_OPTIONS.map((option) => ( | |
| <option key={`${option.width}x${option.height}`} value={`${option.width}x${option.height}`}> | |
| {option.label} | |
| </option> | |
| ))} | |
| </select> | |
| <p id="canvas-size-description" className="text-xs text-slate-500 mt-1"> | |
| Changing size will clear the current canvas and history. | |
| </p> | |
| </div> | |
| </div> | |
| {/* AI Image Generation Settings */} | |
| <div className="mb-6"> | |
| <h3 className="text-md font-medium text-slate-700 mb-2">AI Image Generation</h3> | |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4"> | |
| <label htmlFor="aiApiEndpointSelect" className="block text-sm text-slate-600 mb-1"> | |
| API Endpoint | |
| </label> | |
| <select | |
| id="aiApiEndpointSelect" | |
| value={aiApiEndpoint} | |
| onChange={(e) => onAiApiEndpointChange(e.target.value)} | |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" | |
| aria-describedby="ai-api-description" | |
| > | |
| {AI_API_OPTIONS.map((option) => ( | |
| <option key={option.value} value={option.value}> | |
| {option.label} | |
| </option> | |
| ))} | |
| </select> | |
| <p id="ai-api-description" className="text-xs text-slate-500 mt-1"> | |
| Select an API for image generation. Placeholders are filled automatically. | |
| </p> | |
| </div> | |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4"> | |
| <label htmlFor="aiDimensionsModeSelect" className="block text-sm text-slate-600 mb-1"> | |
| AI Image Dimensions | |
| </label> | |
| <select | |
| id="aiDimensionsModeSelect" | |
| value={aiDimensionsMode} | |
| onChange={(e) => onAiDimensionsModeChange(e.target.value as AiDimensionsMode)} | |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" | |
| aria-describedby="ai-dimensions-description" | |
| > | |
| <option value="api_default">API Default (no size)</option> | |
| <option value="match_canvas">Match Canvas Size</option> | |
| <option value="fixed_1024">Fixed 1024x1024</option> | |
| </select> | |
| <p id="ai-dimensions-description" className="text-xs text-slate-500 mt-1"> | |
| Controls `width` & `height` params for APIs that support them (e.g., Pollinations AI). | |
| </p> | |
| </div> | |
| <div className="bg-slate-50 p-3 rounded-md border border-slate-200"> | |
| <label htmlFor="aiImageQualitySelect" className="block text-sm text-slate-600 mb-1"> | |
| Image Quality | |
| </label> | |
| <select | |
| id="aiImageQualitySelect" | |
| value={aiImageQuality} | |
| onChange={(e) => onAiImageQualityChange(e.target.value as AiImageQuality)} | |
| className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm" | |
| aria-describedby="ai-quality-description" | |
| > | |
| {qualityOptions.map((option) => ( | |
| <option key={option.value} value={option.value}> | |
| {option.label} | |
| </option> | |
| ))} | |
| </select> | |
| <p id="ai-quality-description" className="text-xs text-slate-500 mt-1"> | |
| Used for APIs that support a 'quality' parameter (e.g., Pollinations AI). | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex justify-end gap-3 mt-8"> | |
| <button | |
| onClick={onClose} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-sm font-medium" | |
| > | |
| Done | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default SettingsPanel; |