quick styles and actions | store

This commit is contained in:
lovebird 2025-09-20 17:39:53 +02:00
parent 3a59b33a91
commit ddcd3d52f9
14 changed files with 201 additions and 26 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -14,6 +14,20 @@
},
"core:default",
"fs:default",
"fs:allow-write-text-file",
"fs:allow-read-text-file",
"fs:allow-create",
"fs:allow-mkdir",
"fs:allow-exists",
"fs:allow-write",
"fs:allow-read",
{
"identifier": "fs:scope-appdata-recursive",
"allow": [
{ "path": "$APPDATA/" },
{ "path": "$APPDATA/**" }
]
},
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",

View File

@ -3,6 +3,7 @@ import { ImageFile, PromptTemplate } from "./types";
import { useTauriListeners } from "./hooks/useTauriListeners";
import { tauriApi } from "./lib/tauriApi";
import { saveStore } from "./lib/init";
import { QUICK_STYLES, QUICK_ACTIONS } from "./constants";
import log from "./lib/log";
import Header from "./components/Header";
import PromptForm from "./components/PromptForm";
@ -44,6 +45,38 @@ function App() {
const [prompts, setPrompts] = useState<PromptTemplate[]>([]);
const appendStyle = (style: string) => {
setPrompt(prev => {
const trimmed = prev.trim();
if (trimmed) {
return `${trimmed}, ${style}`;
} else {
return style;
}
});
log.info(`🎨 Style applied: ${style}`);
};
const executeQuickAction = async (action: { name: string; prompt: string; icon: string }) => {
// Find the last non-generated image to use as input
const inputImages = files.filter(f => !f.isGenerated);
if (inputImages.length === 0) {
log.warn('No input images available for quick action');
return;
}
const lastImage = inputImages[inputImages.length - 1];
// Select the last image
handleImageSelection(lastImage.path, false);
// Set the action prompt
setPrompt(action.prompt);
// Generate with the selected image
log.info(`🚀 Executing quick action: ${action.name}`);
await generateImage(action.prompt, [lastImage]);
};
const importPrompts = async () => {
try {
@ -581,6 +614,10 @@ function App() {
savePrompts={savePrompts}
importPrompts={importPrompts}
exportPrompts={exportPrompts}
quickStyles={QUICK_STYLES}
appendStyle={appendStyle}
quickActions={QUICK_ACTIONS}
executeQuickAction={executeQuickAction}
/>
{/* Debug Panel */}

View File

@ -1,5 +1,6 @@
import React from 'react';
import { ImageFile, PromptTemplate } from '../types';
import { QUICK_ACTIONS } from '../constants';
import ImageGallery from './ImageGallery';
import { useDropZone } from '../hooks/useDropZone';
import PromptManager from './PromptManager';
@ -30,6 +31,10 @@ interface PromptFormProps {
savePrompts: (prompts: PromptTemplate[]) => void;
importPrompts: () => void;
exportPrompts: () => void;
quickStyles: readonly string[];
appendStyle: (style: string) => void;
quickActions: typeof QUICK_ACTIONS;
executeQuickAction: (action: { name: string; prompt: string; icon: string }) => Promise<void>;
}
const PromptForm: React.FC<PromptFormProps> = ({
@ -58,6 +63,10 @@ const PromptForm: React.FC<PromptFormProps> = ({
savePrompts,
importPrompts,
exportPrompts,
quickStyles,
appendStyle,
quickActions,
executeQuickAction,
}) => {
const selectedCount = getSelectedImages().length;
const { ref: dropZoneRef, dragIn } = useDropZone({ onDrop: addFiles });
@ -89,6 +98,71 @@ const PromptForm: React.FC<PromptFormProps> = ({
}
}}
/>
{/* Quick Style Picker */}
<div className="mt-3">
<div className="flex flex-wrap gap-2">
<span className="text-xs font-medium text-slate-600 dark:text-slate-400 self-center mr-2">Quick styles:</span>
{quickStyles.map((style) => (
<button
key={style}
type="button"
onClick={() => appendStyle(style)}
className="text-xs px-3 py-1 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 transition-colors duration-200 border border-slate-200 dark:border-slate-700"
>
{style}
</button>
))}
</div>
</div>
{/* Quick Actions */}
<div className="mt-3">
<div className="flex flex-wrap gap-2">
<span className="text-xs font-medium text-slate-600 dark:text-slate-400 self-center mr-2">Quick actions:</span>
{quickActions.map((action) => {
const hasInputImages = files.filter(f => !f.isGenerated).length > 0;
return (
<button
key={action.name}
type="button"
onClick={() => executeQuickAction(action)}
disabled={isGenerating || !hasInputImages}
className={`text-xs px-3 py-1 rounded-full transition-colors duration-200 border ${
!hasInputImages
? 'bg-slate-50 text-slate-400 border-slate-200 dark:bg-slate-800 dark:text-slate-500 dark:border-slate-700 cursor-not-allowed'
: 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700'
} ${isGenerating ? 'opacity-50 cursor-not-allowed' : ''}`}
title={!hasInputImages ? 'Add an image first to use quick actions' : `Apply ${action.name} to the last image`}
>
{action.icon} {action.name}
</button>
);
})}
</div>
</div>
{/* Generate Button */}
<div className="flex gap-3 mt-4">
<button
type="button"
onClick={submit}
disabled={isGenerating || !prompt.trim()}
className="flex-1 glass-button bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white px-6 py-3 rounded-xl font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
>
{isGenerating ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Generating...
</span>
) : (
'🎨 Generate Image'
)}
</button>
</div>
</div>
<PromptManager
@ -194,27 +268,6 @@ const PromptForm: React.FC<PromptFormProps> = ({
)}
<div className="w-full mt-8 space-y-3">
<button
type="submit"
className={`w-full font-bold py-4 px-8 rounded-xl transition-all duration-300 shadow-lg ${
isGenerating
? 'bg-slate-400 dark:bg-slate-600 cursor-not-allowed'
: 'status-gradient-connected hover:shadow-xl hover:scale-[1.02]'
}`}
disabled={isGenerating}
>
{isGenerating ? (
<span className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-current/30 border-t-current rounded-full animate-spin"></div>
<span>Generating...</span>
</span>
) : (
<span className="flex items-center justify-center gap-2">
Generate Image
</span>
)}
</button>
{files.some(file => file.path.startsWith('generated_')) && (
<button
type="button"

View File

@ -17,3 +17,43 @@ export enum TauriEvent {
FILE_DELETED_SUCCESSFULLY = 'file-deleted-successfully',
FILE_DELETION_ERROR = 'file-deletion-error',
}
export const QUICK_STYLES = [
'cartoon style',
'photorealistic',
'noir style',
'product mockup',
'watercolor painting',
'digital art',
'minimalist design',
'vintage poster',
'cyberpunk aesthetic'
] as const;
export const QUICK_ACTIONS = [
{
name: 'Remove Background',
prompt: 'remove the background, make it transparent',
icon: '🔲'
},
{
name: 'Cleanup',
prompt: 'clean up and enhance the image, remove noise and artifacts',
icon: '✨'
},
{
name: 'Crop Foreground',
prompt: 'crop to focus on the main subject, remove unnecessary background',
icon: '✂️'
},
{
name: 'Improve Colors',
prompt: 'enhance colors, improve saturation and contrast',
icon: '🎨'
},
{
name: 'Product Presentation',
prompt: 'professional product photography style, clean background, studio lighting',
icon: '📦'
}
] as const;

View File

@ -83,10 +83,12 @@ export function useTauriListeners({
const isGeneratedImage = isGenerating || document.querySelector('[src^="data:image/svg+xml"]');
if (isGeneratedImage) {
const generatedImageFile: ImageFile = { path: imageData.filename, src, isGenerated: true };
const generatedImageFile: ImageFile = { path: imageData.filename, src, isGenerated: true, selected: true };
setFiles(prev => {
const withoutPlaceholder = prev.filter(file => !file.path.startsWith('generating_') && file.path !== imageData.filename);
return [...withoutPlaceholder, generatedImageFile];
// Deselect all other images and select the new generated one
const updatedFiles = withoutPlaceholder.map(file => ({ ...file, selected: false }));
return [...updatedFiles, generatedImageFile];
});
if (generationTimeoutId) {

View File

@ -113,12 +113,41 @@ export async function loadStore(callbacks: Pick<InitCallbacks, 'setPrompts'>): P
} catch (error) {
const err = error as Error;
log.info(`📂 Prompt store not found or failed to load. A new one will be created on save.`, {
log.info(`📂 Prompt store not found, creating initial store...`, {
error: err.message,
errorName: err.name,
storePath: STORE_FILE_NAME
});
return [];
// Create initial empty store
try {
const initialPrompts: PromptTemplate[] = [];
const configDir = await tauriApi.path.appDataDir();
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
log.debug(`📁 Ensuring directory exists: ${configDir}`);
// Tauri should create the APPDATA directory automatically, but let's verify
log.debug(`📁 APPDATA directory should exist: ${configDir}`);
const initialData = JSON.stringify({ prompts: initialPrompts }, null, 2);
log.debug(`💾 Writing initial store data to: ${storePath}`);
await tauriApi.fs.writeTextFile(storePath, initialData);
log.info(`✅ Initial store created successfully at ${storePath}`);
setPrompts(initialPrompts);
return initialPrompts;
} catch (createError) {
const configDir = await tauriApi.path.appDataDir().catch(() => 'unknown');
log.error('Failed to create initial store - directory may not exist', {
error: (createError as Error).message,
errorName: (createError as Error).name,
storePath: STORE_FILE_NAME,
configDir,
note: 'You may need to manually create the APPDATA directory first'
});
return [];
}
}
}