diff --git a/app/Api/Plugins/ApiPluginInterface.php b/app/Api/Plugins/ApiPluginInterface.php index b6febb2..2805c2c 100644 --- a/app/Api/Plugins/ApiPluginInterface.php +++ b/app/Api/Plugins/ApiPluginInterface.php @@ -11,6 +11,5 @@ interface ApiPluginInterface public function disable(): bool; public function getStatus(string $imageUUID): array; public function getProgress(string $imageUUID): array; - public function upload(string $imagePath): array; - public function styleChangeRequest(string $prompt, string $seedImageUUID): array; + public function processImageStyleChange(string $imagePath, string $prompt, string $modelId, ?string $parameters = null): array; } \ No newline at end of file diff --git a/app/Api/Plugins/RunwareAIPlugin.php b/app/Api/Plugins/RunwareAIPlugin.php deleted file mode 100644 index 09e7ae1..0000000 --- a/app/Api/Plugins/RunwareAIPlugin.php +++ /dev/null @@ -1,106 +0,0 @@ -apiUrl = env('RUNWARE_AI_API_URL', 'https://api.runware.ai/v1'); - $this->apiKey = env('RUNWARE_AI_API_KEY', 'YOUR_RUNWARE_AI_API_KEY'); - - $this->client = new Client([ - 'base_uri' => $this->apiUrl, - 'headers' => [ - 'Authorization' => 'Bearer ' . $this->apiKey, - 'Accept' => 'application/json', - ], - ]); - } - - public function getIdentifier(): string - { - return 'runware-ai'; - } - - public function getName(): string - { - return 'Runware AI'; - } - - public function isEnabled(): bool - { - // Implementieren Sie hier die Logik, um zu prüfen, ob das Plugin aktiviert ist - return true; - } - - public function enable(): bool - { - // Implementieren Sie hier die Logik zum Aktivieren des Plugins - return true; - } - - public function disable(): bool - { - // Implementieren Sie hier die Logik zum Deaktivieren des Plugins - return true; - } - - public function getStatus(string $imageUUID): array - { - try { - $response = $this->client->get("image-inference/status/{$imageUUID}"); - return json_decode($response->getBody()->getContents(), true); - } catch (RequestException $e) { - return ['error' => $e->getMessage()]; - } - } - - public function getProgress(string $imageUUID): array - { - // Runware AI hat keine separate Progress-API, Status enthält den Fortschritt - return $this->getStatus($imageUUID); - } - - public function upload(string $imagePath): array - { - try { - $response = $this->client->post('image-inference/upload', [ - 'multipart' => [ - [ - 'name' => 'image', - 'contents' => fopen($imagePath, 'r'), - 'filename' => basename($imagePath), - ], - ], - ]); - return json_decode($response->getBody()->getContents(), true); - } catch (RequestException $e) { - return ['error' => $e->getMessage()]; - } - } - - public function styleChangeRequest(string $prompt, string $seedImageUUID): array - { - try { - $response = $this->client->post('image-inference/image-to-image', [ - 'json' => [ - 'prompt' => $prompt, - 'seed_image_uuid' => $seedImageUUID, - ], - ]); - return json_decode($response->getBody()->getContents(), true); - } catch (RequestException $e) { - return ['error' => $e->getMessage()]; - } - } -} \ No newline at end of file diff --git a/app/Api/Plugins/RunwareAi.php b/app/Api/Plugins/RunwareAi.php index 4ebf6d2..2d37e17 100644 --- a/app/Api/Plugins/RunwareAi.php +++ b/app/Api/Plugins/RunwareAi.php @@ -71,7 +71,27 @@ class RunwareAi implements ApiPluginInterface return ['progress' => 0]; } - public function upload(string $imagePath): array + public function processImageStyleChange(string $imagePath, string $prompt, string $modelId, ?string $parameters = null): array + { + // Step 1: Upload the original image + $uploadResult = $this->upload($imagePath); + + if (!isset($uploadResult['data'][0]['imageUUID'])) { + throw new \Exception('Image upload to AI service failed or returned no UUID.'); + } + $seedImageUUID = $uploadResult['data'][0]['imageUUID']; + + // Step 2: Request style change using the uploaded image's UUID + $result = $this->styleChangeRequest($prompt, $seedImageUUID, $modelId, $parameters); + + if (!isset($result['base64Data'])) { + throw new \Exception('AI service did not return base64 image data.'); + } + + return $result; + } + + private function upload(string $imagePath): array { $this->logInfo('Attempting to upload image to RunwareAI.', ['image_path' => $imagePath]); if (!$this->apiProvider->api_url || !$this->apiProvider->token) { @@ -83,15 +103,19 @@ class RunwareAi implements ApiPluginInterface $token = $this->apiProvider->token; $taskUUID = (string) Str::uuid(); + $imageData = 'data:image/png;base64,' . base64_encode(file_get_contents($imagePath)); + try { $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json', - ])->attach( - 'image', file_get_contents($imagePath), basename($imagePath) - )->post($apiUrl, [ - 'taskType' => 'imageUpload', - 'taskUUID' => $taskUUID, + 'Content-Type' => 'application/json', + ])->post($apiUrl, [ + [ + 'taskType' => 'imageUpload', + 'taskUUID' => $taskUUID, + 'image' => $imageData, + ] ]); $response->throw(); @@ -103,7 +127,7 @@ class RunwareAi implements ApiPluginInterface } } - public function styleChangeRequest(string $prompt, string $seedImageUUID, ?string $parameters = null): array + private function styleChangeRequest(string $prompt, string $seedImageUUID, string $modelId, ?string $parameters = null): array { $this->logInfo('Attempting style change request to RunwareAI.', ['prompt' => $prompt, 'seed_image_uuid' => $seedImageUUID]); if (!$this->apiProvider->api_url || !$this->apiProvider->token) { @@ -121,6 +145,7 @@ class RunwareAi implements ApiPluginInterface 'positivePrompt' => $prompt, 'seedImage' => $seedImageUUID, 'outputType' => 'base64Data', + 'model' => $modelId, ]; $decodedParameters = json_decode($parameters, true) ?? []; @@ -132,13 +157,28 @@ class RunwareAi implements ApiPluginInterface $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json', - ])->post($apiUrl, $data); + ])->post($apiUrl, [ + $data + ]); $response->throw(); - $this->logInfo('Style change request successful to RunwareAI.', ['task_uuid' => $taskUUID, 'response' => $response->json()]); - return $response->json(); + $responseData = $response->json(); + + if (!isset($responseData['data'][0]['imageBase64Data'])) { + throw new \Exception('AI service did not return base64 image data.'); + } + + $base64Image = $responseData['data'][0]['imageBase64Data']; + + $this->logInfo('Style change request successful to RunwareAI.', ['task_uuid' => $taskUUID, 'response' => $responseData]); + return ['base64Data' => $base64Image]; } catch (\Exception $e) { - $this->logError('Style change request to RunwareAI failed.', ['error' => $e->getMessage(), 'task_uuid' => $taskUUID]); + $errorData = []; + /*if ($e instanceof \Illuminate\Http\Client\RequestException && $e->response) { + $errorData['response_body'] = $e->response->body(); + $errorData['response_status'] = $e->response->status(); + }*/ + $this->logError('Style change request to RunwareAI failed.', ['error' => $e->getMessage(), 'task_uuid' => $taskUUID] + $errorData); throw $e; } } diff --git a/app/Filament/Pages/InstallPluginPage.php b/app/Filament/Pages/InstallPluginPage.php index a5c86f0..8fa7859 100644 --- a/app/Filament/Pages/InstallPluginPage.php +++ b/app/Filament/Pages/InstallPluginPage.php @@ -18,7 +18,7 @@ class InstallPluginPage extends Page implements HasForms protected static string $view = 'filament.pages.install-plugin-page'; - protected static ?string $navigationGroup = 'Settings'; + protected static ?string $navigationGroup = 'Plugins'; protected static ?string $title = 'Install Plugin'; diff --git a/app/Filament/Pages/ListPlugins.php b/app/Filament/Pages/ListPlugins.php index 1bdd884..02fa6b4 100644 --- a/app/Filament/Pages/ListPlugins.php +++ b/app/Filament/Pages/ListPlugins.php @@ -24,7 +24,7 @@ class ListPlugins extends Page implements HasTable protected static string $view = 'filament.pages.list-plugins'; - protected static ?string $navigationGroup = 'Settings'; + protected static ?string $navigationGroup = 'Plugins'; protected static ?string $title = 'Plugins'; diff --git a/app/Filament/Resources/AiModelResource.php b/app/Filament/Resources/AiModelResource.php index c8c2e0a..1bf16ce 100644 --- a/app/Filament/Resources/AiModelResource.php +++ b/app/Filament/Resources/AiModelResource.php @@ -35,12 +35,16 @@ class AiModelResource extends Resource ->required() ->maxLength(255), TextInput::make('model_type') - ->label(__('filament.resource.ai_model.form.model_type')) ->nullable() ->maxLength(255), + Forms\Components\Toggle::make('enabled') + ->label(__('filament.resource.ai_model.form.enabled')) + ->default(true), Select::make('apiProviders') ->relationship('apiProviders', 'name') ->multiple() + ->preload() + ->searchable(false) ->label(__('filament.resource.ai_model.form.api_providers')), ]); } @@ -52,6 +56,9 @@ class AiModelResource extends Resource TextColumn::make('name')->label(__('filament.resource.ai_model.table.name'))->searchable()->sortable(), TextColumn::make('model_id')->label(__('filament.resource.ai_model.table.model_id'))->searchable()->sortable(), TextColumn::make('model_type')->label(__('filament.resource.ai_model.table.model_type'))->searchable()->sortable(), + Tables\Columns\IconColumn::make('enabled') + ->label(__('filament.resource.ai_model.table.enabled')) + ->boolean(), TextColumn::make('apiProviders.name')->label(__('filament.resource.ai_model.table.api_providers'))->searchable()->sortable(), ]) ->filters([ diff --git a/app/Filament/Resources/SettingResource.php b/app/Filament/Resources/SettingResource.php new file mode 100644 index 0000000..a517712 --- /dev/null +++ b/app/Filament/Resources/SettingResource.php @@ -0,0 +1,67 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + // + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]) + ->emptyStateActions([ + Tables\Actions\CreateAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListSettings::route('/'), + 'create' => Pages\CreateSetting::route('/create'), + 'edit' => Pages\EditSetting::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/SettingResource/Pages/CreateSetting.php b/app/Filament/Resources/SettingResource/Pages/CreateSetting.php new file mode 100644 index 0000000..8395ca5 --- /dev/null +++ b/app/Filament/Resources/SettingResource/Pages/CreateSetting.php @@ -0,0 +1,12 @@ +form->fill( + collect(Setting::all()) + ->mapWithKeys(fn (Setting $setting) => [$setting->key => $setting->value]) + ->all() + ); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + TextInput::make('gallery_heading') + ->label(__('settings.gallery_heading')), + ]) + ->statePath('data') + ->model(Setting::class); + } + + public function submit(): void + { + foreach ($this->form->getState() as $key => $value) { + Setting::updateOrCreate(['key' => $key], ['value' => $value]); + } + + Notification::make() + ->title(__('settings.saved_successfully')) + ->success() + ->send(); + } +} diff --git a/app/Http/Controllers/Api/ImageController.php b/app/Http/Controllers/Api/ImageController.php index 3de8123..5b525dd 100644 --- a/app/Http/Controllers/Api/ImageController.php +++ b/app/Http/Controllers/Api/ImageController.php @@ -8,18 +8,48 @@ use App\Api\Plugins\PluginLoader; use App\Models\ApiProvider; use App\Models\Style; use App\Models\Image; -use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\File; class ImageController extends Controller { public function index() { - $images = Storage::disk('public')->files('uploads'); + $publicUploadsPath = public_path('storage/uploads'); + + // Ensure the directory exists + if (!File::exists($publicUploadsPath)) { + File::makeDirectory($publicUploadsPath, 0755, true); + } + + // Get files from the public/storage/uploads directory + $diskFiles = File::files($publicUploadsPath); + $diskImagePaths = []; + foreach ($diskFiles as $file) { + // Store path relative to public/storage/ + $diskImagePaths[] = 'uploads/' . $file->getFilename(); + } + + $dbImagePaths = Image::pluck('path')->toArray(); + + // Add images from disk that are not in the database + $imagesToAdd = array_diff($diskImagePaths, $dbImagePaths); + foreach ($imagesToAdd as $path) { + Image::create(['path' => $path]); + } + + // Remove images from database that are not on disk + $imagesToRemove = array_diff($dbImagePaths, $diskImagePaths); + Image::whereIn('path', $imagesToRemove)->delete(); + + // Fetch all images from the database after synchronization + $images = Image::orderBy('updated_at', 'desc')->get(); $formattedImages = []; foreach ($images as $image) { $formattedImages[] = [ - 'path' => Storage::url($image), - 'name' => basename($image), + 'image_id' => $image->id, + 'path' => asset('storage/' . $image->path), + 'name' => basename($image->path), + 'is_temp' => (bool) $image->is_temp, ]; } return response()->json($formattedImages); @@ -31,21 +61,39 @@ class ImageController extends Controller 'image' => 'required|image|max:10240', // Max 10MB ]); - $path = $request->file('image')->store('uploads', 'public'); + $file = $request->file('image'); + $fileName = uniqid() . '.' . $file->getClientOriginalExtension(); + $destinationPath = public_path('storage/uploads'); + + // Ensure the directory exists + if (!File::exists($destinationPath)) { + File::makeDirectory($destinationPath, 0755, true); + } + + $file->move($destinationPath, $fileName); + $relativePath = 'uploads/' . $fileName; // Path relative to public/storage/ $image = Image::create([ - 'path' => $path, + 'path' => $relativePath, ]); return response()->json([ 'message' => __('api.image_uploaded_successfully'), 'image_id' => $image->id, - 'path' => Storage::url($path), + 'path' => asset('storage/' . $relativePath), ]); } public function styleChangeRequest(Request $request) { + // Same-origin check + $appUrl = config('app.url'); + $referer = $request->headers->get('referer'); + + if ($referer && parse_url($referer, PHP_URL_HOST) !== parse_url($appUrl, PHP_URL_HOST)) { + return response()->json(['error' => 'Unauthorized: Request must originate from the same domain.'], 403); + } + $request->validate([ 'image_id' => 'required|exists:images,id', 'style_id' => 'required|exists:styles,id', @@ -69,32 +117,29 @@ class ImageController extends Controller } $plugin = PluginLoader::getPlugin($apiProvider->plugin, $apiProvider); - // Step 1: Upload the original image - $originalImagePath = Storage::disk('public')->path($image->path); - $uploadResult = $plugin->upload($originalImagePath); - - if (!isset($uploadResult['imageUUID'])) { - throw new \Exception('Image upload to AI service failed or returned no UUID.'); - } - $seedImageUUID = $uploadResult['imageUUID']; - - // Step 2: Request style change using the uploaded image's UUID - $result = $plugin->styleChangeRequest($style->prompt, $seedImageUUID, $style->parameters); - - if (!isset($result['base64Data'])) { - throw new \Exception('AI service did not return base64 image data.'); - } + $result = $plugin->processImageStyleChange( + public_path('storage/' . $image->path), + $style->prompt, + $style->aiModel->model_id, + $style->parameters + ); $base64Image = $result['base64Data']; $decodedImage = base64_decode(preg_replace('#^data:image/\w+;base64, #i', '', $base64Image)); $newImageName = 'styled_' . uniqid() . '.png'; // Assuming PNG for now - $newImagePath = 'uploads/' . $newImageName; + $newImagePathRelative = 'uploads/' . $newImageName; // Path relative to public/storage/ + $newImageFullPath = public_path('storage/' . $newImagePathRelative); // Full path to save - Storage::disk('public')->put($newImagePath, $decodedImage); + // Ensure the directory exists + if (!File::exists(public_path('storage/uploads'))) { + File::makeDirectory(public_path('storage/uploads'), 0755, true); + } + + File::put($newImageFullPath, $decodedImage); // Save using File facade $newImage = Image::create([ - 'path' => $newImagePath, + 'path' => $newImagePathRelative, // Store relative path 'original_image_id' => $image->id, // Link to original image 'style_id' => $style->id, // Link to applied style 'is_temp' => true, // Mark as temporary until user keeps it @@ -104,7 +149,7 @@ class ImageController extends Controller 'message' => 'Style change successful', 'styled_image' => [ 'id' => $newImage->id, - 'path' => Storage::url($newImage->path), + 'path' => asset('storage/' . $newImage->path), 'is_temp' => $newImage->is_temp, ], ]); @@ -133,44 +178,9 @@ class ImageController extends Controller public function deleteImage(Image $image) { - // Ensure the image is temporary or belongs to the authenticated user if not temporary - // For simplicity, we'll allow deletion of any image passed for now. - // In a real app, you'd add authorization checks here. - try { - Storage::disk('public')->delete($image->path); - $image->delete(); - return response()->json(['message' => __('api.image_deleted_successfully')]); - } catch (\Exception $e) { - return response()->json(['error' => $e->getMessage()], 500); - } - } - - public function keepImage(Request $request) - { - $request->validate([ - 'image_id' => 'required|exists:images,id', - ]); - - $image = Image::find($request->image_id); - - if (!$image) { - return response()->json(['error' => __('api.image_not_found')], 404); - } - - try { - $image->is_temp = false; - $image->save(); - return response()->json(['message' => __('api.image_kept_successfully')]); - } catch (\Exception $e) { - return response()->json(['error' => $e->getMessage()], 500); - } - } - - public function deleteImage(Image $image) - { - try { - Storage::disk('public')->delete($image->path); + // Delete from the public/storage directory + File::delete(public_path('storage/' . $image->path)); $image->delete(); return response()->json(['message' => __('api.image_deleted_successfully')]); } catch (\Exception $e) { @@ -222,4 +232,5 @@ class ImageController extends Controller } catch (\Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } + } } \ No newline at end of file diff --git a/app/Http/Controllers/Api/StyleController.php b/app/Http/Controllers/Api/StyleController.php index b044cb2..18a963d 100644 --- a/app/Http/Controllers/Api/StyleController.php +++ b/app/Http/Controllers/Api/StyleController.php @@ -10,7 +10,8 @@ class StyleController extends Controller { public function index() { - $styles = Style::where('enabled', true) + $styles = Style::with(['aiModel.apiProviders']) + ->where('enabled', true) ->whereHas('aiModel', function ($query) { $query->where('enabled', true); $query->whereHas('apiProviders', function ($query) { @@ -19,6 +20,10 @@ class StyleController extends Controller }) ->get(); + if ($styles->isEmpty()) { + return response()->json(['message' => __('api.no_styles_available')], 404); + } + return response()->json($styles); } } \ No newline at end of file diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 6fbfb03..9a425ac 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use App\Models\Setting; use Inertia\Inertia; use Illuminate\Support\Facades\Lang; @@ -12,9 +13,11 @@ class HomeController extends Controller { $locale = app()->getLocale(); $translations = Lang::get('messages', [], $locale); + $galleryHeading = Setting::where('key', 'gallery_heading')->first()->value ?? 'Style Gallery'; return Inertia::render('Home', [ 'translations' => $translations, + 'galleryHeading' => $galleryHeading, ]); } } \ No newline at end of file diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 5ddce56..e3fcd92 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -35,6 +35,16 @@ class HandleInertiaRequests extends Middleware 'user' => $request->user(), ], 'locale' => app()->getLocale(), + 'lang' => function () { + $lang = [ + 'filament' => trans('filament'), + 'api' => trans('api'), + 'settings' => trans('settings'), + 'messages' => trans('messages'), + // Add other translation files as needed + ]; + return $lang; + }, ]; } } diff --git a/app/Models/Image.php b/app/Models/Image.php index 96c23f4..6669d51 100644 --- a/app/Models/Image.php +++ b/app/Models/Image.php @@ -8,10 +8,13 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids; class Image extends Model { - use HasFactory, HasUuids; + use HasFactory; protected $fillable = [ 'path', 'uuid', + 'original_image_id', + 'style_id', + 'is_temp', ]; } diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..3581c57 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,13 @@ +pages([ Pages\Dashboard::class, \App\Filament\Pages\InstallPluginPage::class, + Settings::class, ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ diff --git a/database/migrations/2025_07_30_123535_add_enabled_to_styles_table.php b/database/migrations/2025_07_30_143255_create_settings_table.php similarity index 58% rename from database/migrations/2025_07_30_123535_add_enabled_to_styles_table.php rename to database/migrations/2025_07_30_143255_create_settings_table.php index 1171f3b..ce3b578 100644 --- a/database/migrations/2025_07_30_123535_add_enabled_to_styles_table.php +++ b/database/migrations/2025_07_30_143255_create_settings_table.php @@ -11,8 +11,11 @@ return new class extends Migration */ public function up(): void { - Schema::table('styles', function (Blueprint $table) { - $table->boolean('enabled')->default(true)->after('ai_model_id'); + Schema::create('settings', function (Blueprint $table) { + $table->id(); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->timestamps(); }); } @@ -21,8 +24,6 @@ return new class extends Migration */ public function down(): void { - Schema::table('styles', function (Blueprint $table) { - $table->dropColumn('enabled'); - }); + Schema::dropIfExists('settings'); } }; diff --git a/database/migrations/2025_07_30_123238_add_enabled_to_styles_table.php b/database/migrations/2025_07_30_184244_add_enabled_to_ai_models_table.php similarity index 67% rename from database/migrations/2025_07_30_123238_add_enabled_to_styles_table.php rename to database/migrations/2025_07_30_184244_add_enabled_to_ai_models_table.php index 1171f3b..2f23dc4 100644 --- a/database/migrations/2025_07_30_123238_add_enabled_to_styles_table.php +++ b/database/migrations/2025_07_30_184244_add_enabled_to_ai_models_table.php @@ -11,8 +11,8 @@ return new class extends Migration */ public function up(): void { - Schema::table('styles', function (Blueprint $table) { - $table->boolean('enabled')->default(true)->after('ai_model_id'); + Schema::table('ai_models', function (Blueprint $table) { + $table->boolean('enabled')->default(true)->after('model_type'); }); } @@ -21,7 +21,7 @@ return new class extends Migration */ public function down(): void { - Schema::table('styles', function (Blueprint $table) { + Schema::table('ai_models', function (Blueprint $table) { $table->dropColumn('enabled'); }); } diff --git a/database/migrations/2025_07_30_201035_add_style_change_columns_to_images_table.php b/database/migrations/2025_07_30_201035_add_style_change_columns_to_images_table.php new file mode 100644 index 0000000..feb8648 --- /dev/null +++ b/database/migrations/2025_07_30_201035_add_style_change_columns_to_images_table.php @@ -0,0 +1,36 @@ +unsignedBigInteger('original_image_id')->nullable()->after('id'); + $table->foreign('original_image_id')->references('id')->on('images')->onDelete('set null'); + $table->unsignedBigInteger('style_id')->nullable()->after('original_image_id'); + $table->foreign('style_id')->references('id')->on('styles')->onDelete('set null'); + $table->boolean('is_temp')->default(false)->after('style_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('images', function (Blueprint $table) { + $table->dropForeign(['original_image_id']); + $table->dropColumn('original_image_id'); + $table->dropForeign(['style_id']); + $table->dropColumn('style_id'); + $table->dropColumn('is_temp'); + }); + } +}; diff --git a/resources/js/Components/LoadingSpinner.vue b/resources/js/Components/LoadingSpinner.vue new file mode 100644 index 0000000..47a800f --- /dev/null +++ b/resources/js/Components/LoadingSpinner.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/resources/js/Components/StyleSelector.vue b/resources/js/Components/StyleSelector.vue index b599509..f55d085 100644 --- a/resources/js/Components/StyleSelector.vue +++ b/resources/js/Components/StyleSelector.vue @@ -10,7 +10,7 @@ v-for="style in styles" :key="style.id" class="style-item" - @click="$emit('styleSelected', style)" + @click="selectStyle(style)" >
@@ -29,6 +29,19 @@ import { ref, onMounted } from 'vue'; const styles = ref([]); +const props = defineProps({ + position: { + type: Object, + required: true, + }, + image_id: { + type: Number, + required: true, + }, +}); + +const emits = defineEmits(['styleSelected', 'back', 'close']); + const fetchStyles = () => { axios.get('/api/styles') .then(response => { @@ -39,18 +52,14 @@ const fetchStyles = () => { }); }; +const selectStyle = (style) => { + console.log('StyleSelector.vue: emitting styleSelected with image_id:', props.image_id); + emits('styleSelected', style, props.image_id); +}; + onMounted(() => { fetchStyles(); }); - -const props = defineProps({ - position: { - type: Object, - required: true, - }, -}); - -const emits = defineEmits(['styleSelected', 'back', 'close']);