diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 68f8e7e..524f26a 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -46,7 +46,6 @@ class Plugin extends Model 'dark_mode' => 'boolean', 'preferred_renderer' => 'string', 'plugin_type' => 'string', - 'alias' => 'boolean', ]; protected static function boot() @@ -154,7 +153,7 @@ class Plugin extends Model public function updateDataPayload(): void { - if ($this->data_strategy !== 'polling' || ! $this->polling_url) { + if ($this->data_strategy !== 'polling' || !$this->polling_url) { return; } $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 405ea3f..b8269a3 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -26,44 +26,11 @@ class ImageGenerationService public static function generateImage(string $markup, $deviceId): string { $device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId); - $uuid = self::generateImageFromModel( - markup: $markup, - deviceModel: $device->deviceModel, - user: $device->user, - palette: $device->palette ?? $device->deviceModel?->palette, - device: $device - ); - - $device->update(['current_screen_image' => $uuid]); - Log::info("Device $device->id: updated with new image: $uuid"); - - return $uuid; - } - - /** - * Generate an image from markup using a DeviceModel - * - * @param string $markup The HTML markup to render - * @param DeviceModel|null $deviceModel The device model to use for image generation - * @param \App\Models\User|null $user Optional user for timezone settings - * @param \App\Models\DevicePalette|null $palette Optional palette, falls back to device model's palette - * @param Device|null $device Optional device for legacy devices without DeviceModel - * @return string The UUID of the generated image - */ - public static function generateImageFromModel( - string $markup, - ?DeviceModel $deviceModel = null, - ?\App\Models\User $user = null, - ?\App\Models\DevicePalette $palette = null, - ?Device $device = null - ): string { $uuid = Uuid::uuid4()->toString(); try { - // Get image generation settings from DeviceModel or Device (for legacy devices) - $imageSettings = $deviceModel - ? self::getImageSettingsFromModel($deviceModel) - : ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null)); + // Get image generation settings from DeviceModel if available, otherwise use device settings + $imageSettings = self::getImageSettings($device); $fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png'; $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); @@ -78,7 +45,7 @@ class ImageGenerationService $browserStage->html($markup); // Set timezone from user or fall back to app timezone - $timezone = $user?->timezone ?? config('app.timezone'); + $timezone = $device->user->timezone ?? config('app.timezone'); $browserStage->timezone($timezone); if (config('app.puppeteer_window_size_strategy') === 'v2') { @@ -98,12 +65,12 @@ class ImageGenerationService $browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']); } - // Get palette from parameter or fallback to device model's default palette + // Get palette from device or fallback to device model's default palette + $palette = $device->palette ?? $device->deviceModel?->palette; $colorPalette = null; + if ($palette && $palette->colors) { $colorPalette = $palette->colors; - } elseif ($deviceModel?->palette && $deviceModel->palette->colors) { - $colorPalette = $deviceModel->palette->colors; } $imageStage = new ImageStage(); @@ -140,7 +107,8 @@ class ImageGenerationService throw new RuntimeException('Image file is empty: '.$outputPath); } - Log::info("Generated image: $uuid"); + $device->update(['current_screen_image' => $uuid]); + Log::info("Device $device->id: updated with new image: $uuid"); return $uuid; @@ -157,7 +125,22 @@ class ImageGenerationService { // If device has a DeviceModel, use its settings if ($device->deviceModel) { - return self::getImageSettingsFromModel($device->deviceModel); + /** @var DeviceModel $model */ + $model = $device->deviceModel; + + return [ + 'width' => $model->width, + 'height' => $model->height, + 'colors' => $model->colors, + 'bit_depth' => $model->bit_depth, + 'scale_factor' => $model->scale_factor, + 'rotation' => $model->rotation, + 'mime_type' => $model->mime_type, + 'offset_x' => $model->offset_x, + 'offset_y' => $model->offset_y, + 'image_format' => self::determineImageFormatFromModel($model), + 'use_model_settings' => true, + ]; } // Fallback to device settings @@ -181,43 +164,6 @@ class ImageGenerationService ]; } - /** - * Get image generation settings from a DeviceModel - */ - private static function getImageSettingsFromModel(?DeviceModel $deviceModel): array - { - if ($deviceModel) { - return [ - 'width' => $deviceModel->width, - 'height' => $deviceModel->height, - 'colors' => $deviceModel->colors, - 'bit_depth' => $deviceModel->bit_depth, - 'scale_factor' => $deviceModel->scale_factor, - 'rotation' => $deviceModel->rotation, - 'mime_type' => $deviceModel->mime_type, - 'offset_x' => $deviceModel->offset_x, - 'offset_y' => $deviceModel->offset_y, - 'image_format' => self::determineImageFormatFromModel($deviceModel), - 'use_model_settings' => true, - ]; - } - - // Default settings if no device model provided - return [ - 'width' => 800, - 'height' => 480, - 'colors' => 2, - 'bit_depth' => 1, - 'scale_factor' => 1.0, - 'rotation' => 0, - 'mime_type' => 'image/png', - 'offset_x' => 0, - 'offset_y' => 0, - 'image_format' => ImageFormat::AUTO->value, - 'use_model_settings' => false, - ]; - } - /** * Determine the appropriate ImageFormat based on DeviceModel settings */ diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index 49dce99..eeb5835 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -20,13 +20,12 @@ class PluginImportService /** * Validate YAML settings * - * @param array $settings The parsed YAML settings - * + * @param array $settings The parsed YAML settings * @throws Exception */ private function validateYAML(array $settings): void { - if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) { + if (!isset($settings['custom_fields']) || !is_array($settings['custom_fields'])) { return; } @@ -44,7 +43,6 @@ class PluginImportService } } } - /** * Import a plugin from a ZIP file * @@ -75,17 +73,12 @@ class PluginImportService $zip->extractTo($tempDir); $zip->close(); - // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php) + // Find the required files (settings.yml and full.liquid/full.blade.php) $filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath); // Validate that we found the required files - if (! $filePaths['settingsYamlPath']) { - throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.'); - } - - // Validate that we have at least one template file - if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) { - throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.'); + if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { + throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php } // Parse settings.yml @@ -93,37 +86,20 @@ class PluginImportService $settings = Yaml::parse($settingsYaml); $this->validateYAML($settings); - // Determine which template file to use and read its content - $templatePath = null; + // Read full.liquid content + $fullLiquid = File::get($filePaths['fullLiquidPath']); + + // Prepend shared.liquid content if available + if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedLiquid = File::get($filePaths['sharedLiquidPath']); + $fullLiquid = $sharedLiquid."\n".$fullLiquid; + } + + // Check if the file ends with .liquid to set markup language $markupLanguage = 'blade'; - - if ($filePaths['fullLiquidPath']) { - $templatePath = $filePaths['fullLiquidPath']; - $fullLiquid = File::get($templatePath); - - // Prepend shared.liquid or shared.blade.php content if available - if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { - $sharedLiquid = File::get($filePaths['sharedLiquidPath']); - $fullLiquid = $sharedLiquid."\n".$fullLiquid; - } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { - $sharedBlade = File::get($filePaths['sharedBladePath']); - $fullLiquid = $sharedBlade."\n".$fullLiquid; - } - - // Check if the file ends with .liquid to set markup language - if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') { - $markupLanguage = 'liquid'; - $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } - } elseif ($filePaths['sharedLiquidPath']) { - $templatePath = $filePaths['sharedLiquidPath']; - $fullLiquid = File::get($templatePath); + if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') { $markupLanguage = 'liquid'; $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } elseif ($filePaths['sharedBladePath']) { - $templatePath = $filePaths['sharedBladePath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'blade'; } // Ensure custom_fields is properly formatted @@ -228,17 +204,12 @@ class PluginImportService $zip->extractTo($tempDir); $zip->close(); - // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php) + // Find the required files (settings.yml and full.liquid/full.blade.php) $filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath); // Validate that we found the required files - if (! $filePaths['settingsYamlPath']) { - throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.'); - } - - // Validate that we have at least one template file - if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) { - throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.'); + if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { + throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.'); } // Parse settings.yml @@ -246,37 +217,20 @@ class PluginImportService $settings = Yaml::parse($settingsYaml); $this->validateYAML($settings); - // Determine which template file to use and read its content - $templatePath = null; + // Read full.liquid content + $fullLiquid = File::get($filePaths['fullLiquidPath']); + + // Prepend shared.liquid content if available + if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedLiquid = File::get($filePaths['sharedLiquidPath']); + $fullLiquid = $sharedLiquid."\n".$fullLiquid; + } + + // Check if the file ends with .liquid to set markup language $markupLanguage = 'blade'; - - if ($filePaths['fullLiquidPath']) { - $templatePath = $filePaths['fullLiquidPath']; - $fullLiquid = File::get($templatePath); - - // Prepend shared.liquid or shared.blade.php content if available - if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { - $sharedLiquid = File::get($filePaths['sharedLiquidPath']); - $fullLiquid = $sharedLiquid."\n".$fullLiquid; - } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { - $sharedBlade = File::get($filePaths['sharedBladePath']); - $fullLiquid = $sharedBlade."\n".$fullLiquid; - } - - // Check if the file ends with .liquid to set markup language - if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') { - $markupLanguage = 'liquid'; - $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } - } elseif ($filePaths['sharedLiquidPath']) { - $templatePath = $filePaths['sharedLiquidPath']; - $fullLiquid = File::get($templatePath); + if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') { $markupLanguage = 'liquid'; $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } elseif ($filePaths['sharedBladePath']) { - $templatePath = $filePaths['sharedBladePath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'blade'; } // Ensure custom_fields is properly formatted @@ -356,7 +310,6 @@ class PluginImportService $settingsYamlPath = null; $fullLiquidPath = null; $sharedLiquidPath = null; - $sharedBladePath = null; // If zipEntryPath is specified, look for files in that specific directory first if ($zipEntryPath) { @@ -374,8 +327,6 @@ class PluginImportService if (File::exists($targetDir.'/shared.liquid')) { $sharedLiquidPath = $targetDir.'/shared.liquid'; - } elseif (File::exists($targetDir.'/shared.blade.php')) { - $sharedBladePath = $targetDir.'/shared.blade.php'; } } @@ -391,18 +342,15 @@ class PluginImportService if (File::exists($targetDir.'/src/shared.liquid')) { $sharedLiquidPath = $targetDir.'/src/shared.liquid'; - } elseif (File::exists($targetDir.'/src/shared.blade.php')) { - $sharedBladePath = $targetDir.'/src/shared.blade.php'; } } // If we found the required files in the target directory, return them - if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) { + if ($settingsYamlPath && $fullLiquidPath) { return [ 'settingsYamlPath' => $settingsYamlPath, 'fullLiquidPath' => $fullLiquidPath, 'sharedLiquidPath' => $sharedLiquidPath, - 'sharedBladePath' => $sharedBladePath, ]; } } @@ -419,11 +367,9 @@ class PluginImportService $fullLiquidPath = $tempDir.'/src/full.blade.php'; } - // Check for shared.liquid or shared.blade.php in the same directory + // Check for shared.liquid in the same directory if (File::exists($tempDir.'/src/shared.liquid')) { $sharedLiquidPath = $tempDir.'/src/shared.liquid'; - } elseif (File::exists($tempDir.'/src/shared.blade.php')) { - $sharedBladePath = $tempDir.'/src/shared.blade.php'; } } else { // Search for the files in the extracted directory structure @@ -440,24 +386,20 @@ class PluginImportService $fullLiquidPath = $filepath; } elseif ($filename === 'shared.liquid') { $sharedLiquidPath = $filepath; - } elseif ($filename === 'shared.blade.php') { - $sharedBladePath = $filepath; } } - // Check if shared.liquid or shared.blade.php exists in the same directory as full.liquid - if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath && ! $sharedBladePath) { + // Check if shared.liquid exists in the same directory as full.liquid + if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath) { $fullLiquidDir = dirname((string) $fullLiquidPath); if (File::exists($fullLiquidDir.'/shared.liquid')) { $sharedLiquidPath = $fullLiquidDir.'/shared.liquid'; - } elseif (File::exists($fullLiquidDir.'/shared.blade.php')) { - $sharedBladePath = $fullLiquidDir.'/shared.blade.php'; } } // If we found the files but they're not in the src folder, // check if they're in the root of the ZIP or in a subfolder - if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) { + if ($settingsYamlPath && $fullLiquidPath) { // If the files are in the root of the ZIP, create a src folder and move them there $srcDir = dirname((string) $settingsYamlPath); @@ -468,25 +410,17 @@ class PluginImportService // Copy the files to the src directory File::copy($settingsYamlPath, $newSrcDir.'/settings.yml'); + File::copy($fullLiquidPath, $newSrcDir.'/full.liquid'); - // Copy full.liquid or full.blade.php if it exists - if ($fullLiquidPath) { - $extension = pathinfo((string) $fullLiquidPath, PATHINFO_EXTENSION); - File::copy($fullLiquidPath, $newSrcDir.'/full.'.$extension); - $fullLiquidPath = $newSrcDir.'/full.'.$extension; - } - - // Copy shared.liquid or shared.blade.php if it exists + // Copy shared.liquid if it exists if ($sharedLiquidPath) { File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid'); $sharedLiquidPath = $newSrcDir.'/shared.liquid'; - } elseif ($sharedBladePath) { - File::copy($sharedBladePath, $newSrcDir.'/shared.blade.php'); - $sharedBladePath = $newSrcDir.'/shared.blade.php'; } // Update the paths $settingsYamlPath = $newSrcDir.'/settings.yml'; + $fullLiquidPath = $newSrcDir.'/full.liquid'; } } } @@ -495,7 +429,6 @@ class PluginImportService 'settingsYamlPath' => $settingsYamlPath, 'fullLiquidPath' => $fullLiquidPath, 'sharedLiquidPath' => $sharedLiquidPath, - 'sharedBladePath' => $sharedBladePath, ]; } diff --git a/config/trustedproxy.php b/config/trustedproxy.php deleted file mode 100644 index 8557288..0000000 --- a/config/trustedproxy.php +++ /dev/null @@ -1,6 +0,0 @@ - ($proxies = env('TRUSTED_PROXIES', '')) === '*' ? '*' : array_filter(explode(',', $proxies)), -]; diff --git a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php deleted file mode 100644 index 9769505..0000000 --- a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php +++ /dev/null @@ -1,60 +0,0 @@ -select('user_id', 'trmnlp_id', DB::raw('COUNT(*) as count')) - ->whereNotNull('trmnlp_id') - ->groupBy('user_id', 'trmnlp_id') - ->having('count', '>', 1) - ->get(); - - // For each duplicate combination, keep the first one (by id) and set others to null - foreach ($duplicates as $duplicate) { - $plugins = DB::table('plugins') - ->where('user_id', $duplicate->user_id) - ->where('trmnlp_id', $duplicate->trmnlp_id) - ->orderBy('id') - ->get(); - - // Keep the first one, set the rest to null - $keepFirst = true; - foreach ($plugins as $plugin) { - if ($keepFirst) { - $keepFirst = false; - - continue; - } - - DB::table('plugins') - ->where('id', $plugin->id) - ->update(['trmnlp_id' => null]); - } - } - - Schema::table('plugins', function (Blueprint $table) { - $table->unique(['user_id', 'trmnlp_id']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('plugins', function (Blueprint $table) { - $table->dropUnique(['user_id', 'trmnlp_id']); - }); - } -}; diff --git a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php b/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php deleted file mode 100644 index 0a527d7..0000000 --- a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php +++ /dev/null @@ -1,28 +0,0 @@ -boolean('alias')->default(false); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('plugins', function (Blueprint $table) { - $table->dropColumn('alias'); - }); - } -}; diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index c9a1442..19ee3a7 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -478,6 +478,7 @@ HTML; + @@ -487,10 +488,6 @@ HTML; - - Recipe Settings - - Duplicate Plugin Delete Plugin @@ -649,8 +646,6 @@ HTML; - -
@@ -739,7 +734,7 @@ HTML; @endif
- Configuration Fields + Configuration
@endif diff --git a/resources/views/livewire/plugins/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php deleted file mode 100644 index d07a243..0000000 --- a/resources/views/livewire/plugins/recipes/settings.blade.php +++ /dev/null @@ -1,104 +0,0 @@ -resetErrorBag(); - // Reload data - $this->plugin = $this->plugin->fresh(); - $this->trmnlp_id = $this->plugin->trmnlp_id; - $this->uuid = $this->plugin->uuid; - $this->alias = $this->plugin->alias ?? false; - } - - public function saveTrmnlpId(): void - { - abort_unless(auth()->user()->plugins->contains($this->plugin), 403); - - $this->validate([ - 'trmnlp_id' => [ - 'nullable', - 'string', - 'max:255', - Rule::unique('plugins', 'trmnlp_id') - ->where('user_id', auth()->id()) - ->ignore($this->plugin->id), - ], - 'alias' => 'boolean', - ]); - - $this->plugin->update([ - 'trmnlp_id' => empty($this->trmnlp_id) ? null : $this->trmnlp_id, - 'alias' => $this->alias, - ]); - - Flux::modal('trmnlp-settings')->close(); - } - - public function getAliasUrlProperty(): string - { - return url("/api/display/{$this->uuid}/alias"); - } -};?> - - -
-
- Recipe Settings -
- -
-
- {{-- --}} - - TRMNLP Recipe ID - - - Recipe ID in the TRMNL Recipe Catalog. If set, it can be used with trmnlp. - - - - - Enable a public alias URL for this recipe. - - - @if($alias) - - Alias URL - - Use this URL to access the recipe image directly. Add ?device-model=name to specify a device model. - - @endif -
- -
- - - Cancel - - Save -
-
-
-
diff --git a/routes/api.php b/routes/api.php index f3a31a1..5700a43 100644 --- a/routes/api.php +++ b/routes/api.php @@ -613,7 +613,7 @@ Route::post('plugin_settings/{uuid}/image', function (Request $request, string $ } // Generate a new UUID for each image upload to prevent device caching - $imageUuid = Str::uuid()->toString(); + $imageUuid = \Illuminate\Support\Str::uuid()->toString(); $filename = $imageUuid.'.'.$extension; $path = 'images/generated/'.$filename; @@ -678,90 +678,3 @@ Route::post('plugin_settings/{trmnlp_id}/archive', function (Request $request, s ], ]); })->middleware('auth:sanctum'); - -Route::get('/display/{uuid}/alias', function (Request $request, string $uuid) { - $plugin = Plugin::where('uuid', $uuid)->firstOrFail(); - - // Check if alias is active - if (! $plugin->alias) { - return response()->json([ - 'message' => 'Alias is not active for this plugin', - ], 403); - } - - // Get device model name from query parameter, default to 'og_png' - $deviceModelName = $request->query('device-model', 'og_png'); - $deviceModel = DeviceModel::where('name', $deviceModelName)->first(); - - if (! $deviceModel) { - return response()->json([ - 'message' => "Device model '{$deviceModelName}' not found", - ], 404); - } - - // Check if we can use cached image (only for og_png and if data is not stale) - $useCache = $deviceModelName === 'og_png' && ! $plugin->isDataStale() && $plugin->current_image !== null; - - if ($useCache) { - // Return cached image - $imageUuid = $plugin->current_image; - $fileExtension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png'; - $imagePath = 'images/generated/'.$imageUuid.'.'.$fileExtension; - - // Check if image exists, otherwise fall back to generation - if (Storage::disk('public')->exists($imagePath)) { - return response()->file(Storage::disk('public')->path($imagePath), [ - 'Content-Type' => $deviceModel->mime_type, - ]); - } - } - - // Generate new image - try { - // Update data if needed - if ($plugin->isDataStale()) { - $plugin->updateDataPayload(); - $plugin->refresh(); - } - - // Load device model with palette relationship - $deviceModel->load('palette'); - - // Create a virtual device for rendering (Plugin::render needs a Device object) - $virtualDevice = new Device(); - $virtualDevice->setRelation('deviceModel', $deviceModel); - $virtualDevice->setRelation('user', $plugin->user); - $virtualDevice->setRelation('palette', $deviceModel->palette); - - // Render the plugin markup - $markup = $plugin->render(device: $virtualDevice); - - // Generate image using the new method that doesn't require a device - $imageUuid = ImageGenerationService::generateImageFromModel( - markup: $markup, - deviceModel: $deviceModel, - user: $plugin->user, - palette: $deviceModel->palette - ); - - // Update plugin cache if using og_png - if ($deviceModelName === 'og_png') { - $plugin->update(['current_image' => $imageUuid]); - } - - // Return the generated image - $fileExtension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png'; - $imagePath = Storage::disk('public')->path('images/generated/'.$imageUuid.'.'.$fileExtension); - - return response()->file($imagePath, [ - 'Content-Type' => $deviceModel->mime_type, - ]); - } catch (Exception $e) { - Log::error("Failed to generate alias image for plugin {$plugin->id} ({$plugin->name}): ".$e->getMessage()); - - return response()->json([ - 'message' => 'Failed to generate image', - 'error' => $e->getMessage(), - ], 500); - } -})->name('api.display.alias'); diff --git a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php deleted file mode 100644 index a04815f..0000000 --- a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php +++ /dev/null @@ -1,112 +0,0 @@ -create(); - $this->actingAs($user); - - $plugin = Plugin::factory()->create([ - 'user_id' => $user->id, - 'trmnlp_id' => null, - ]); - - $trmnlpId = (string) Str::uuid(); - - Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) - ->set('trmnlp_id', $trmnlpId) - ->call('saveTrmnlpId') - ->assertHasNoErrors(); - - expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId); -}); - -test('recipe settings validates trmnlp_id is unique per user', function (): void { - $user = User::factory()->create(); - $this->actingAs($user); - - $existingPlugin = Plugin::factory()->create([ - 'user_id' => $user->id, - 'trmnlp_id' => 'existing-id-123', - ]); - - $newPlugin = Plugin::factory()->create([ - 'user_id' => $user->id, - 'trmnlp_id' => null, - ]); - - Volt::test('plugins.recipes.settings', ['plugin' => $newPlugin]) - ->set('trmnlp_id', 'existing-id-123') - ->call('saveTrmnlpId') - ->assertHasErrors(['trmnlp_id' => 'unique']); - - expect($newPlugin->fresh()->trmnlp_id)->toBeNull(); -}); - -test('recipe settings allows same trmnlp_id for different users', function (): void { - $user1 = User::factory()->create(); - $user2 = User::factory()->create(); - - $plugin1 = Plugin::factory()->create([ - 'user_id' => $user1->id, - 'trmnlp_id' => 'shared-id-123', - ]); - - $plugin2 = Plugin::factory()->create([ - 'user_id' => $user2->id, - 'trmnlp_id' => null, - ]); - - $this->actingAs($user2); - - Volt::test('plugins.recipes.settings', ['plugin' => $plugin2]) - ->set('trmnlp_id', 'shared-id-123') - ->call('saveTrmnlpId') - ->assertHasNoErrors(); - - expect($plugin2->fresh()->trmnlp_id)->toBe('shared-id-123'); -}); - -test('recipe settings allows same trmnlp_id for the same plugin', function (): void { - $user = User::factory()->create(); - $this->actingAs($user); - - $trmnlpId = (string) Str::uuid(); - - $plugin = Plugin::factory()->create([ - 'user_id' => $user->id, - 'trmnlp_id' => $trmnlpId, - ]); - - Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) - ->set('trmnlp_id', $trmnlpId) - ->call('saveTrmnlpId') - ->assertHasNoErrors(); - - expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId); -}); - -test('recipe settings can clear trmnlp_id', function (): void { - $user = User::factory()->create(); - $this->actingAs($user); - - $plugin = Plugin::factory()->create([ - 'user_id' => $user->id, - 'trmnlp_id' => 'some-id', - ]); - - Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) - ->set('trmnlp_id', '') - ->call('saveTrmnlpId') - ->assertHasNoErrors(); - - expect($plugin->fresh()->trmnlp_id)->toBeNull(); -}); diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php index f3ef1fa..fae28a8 100644 --- a/tests/Feature/PluginImportTest.php +++ b/tests/Feature/PluginImportTest.php @@ -83,34 +83,19 @@ it('throws exception for invalid zip file', function (): void { ->toThrow(Exception::class, 'Could not open the ZIP file.'); }); -it('throws exception for missing settings.yml', function (): void { - $user = User::factory()->create(); - - $zipContent = createMockZipFile([ - 'src/full.liquid' => getValidFullLiquid(), - // Missing settings.yml - ]); - - $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); - - $pluginImportService = new PluginImportService(); - expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user)) - ->toThrow(Exception::class, 'Invalid ZIP structure. Required file settings.yml is missing.'); -}); - -it('throws exception for missing template files', function (): void { +it('throws exception for missing required files', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), - // Missing all template files + // Missing full.liquid ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user)) - ->toThrow(Exception::class, 'Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.'); + ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); }); it('sets default values when settings are missing', function (): void { @@ -446,7 +431,7 @@ it('throws exception when multi_string default value contains a comma', function $user = User::factory()->create(); // YAML with a comma in the 'default' field of a multi_string - $invalidYaml = <<<'YAML' + $invalidYaml = << $pluginImportService->importFromZip($zipFile, $user)) - ->toThrow(Exception::class, 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.'); + ->toThrow(Exception::class, "Validation Error: The default value for multistring fields like `api_key` cannot contain commas."); }); it('throws exception when multi_string placeholder contains a comma', function (): void { $user = User::factory()->create(); // YAML with a comma in the 'placeholder' field - $invalidYaml = <<<'YAML' + $invalidYaml = << $pluginImportService->importFromZip($zipFile, $user)) - ->toThrow(Exception::class, 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.'); -}); - -it('imports plugin with only shared.liquid file', function (): void { - $user = User::factory()->create(); - - $zipContent = createMockZipFile([ - 'src/settings.yml' => getValidSettingsYaml(), - 'src/shared.liquid' => '
{{ data.title }}
', - ]); - - $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); - - $pluginImportService = new PluginImportService(); - $plugin = $pluginImportService->importFromZip($zipFile, $user); - - expect($plugin)->toBeInstanceOf(Plugin::class) - ->and($plugin->markup_language)->toBe('liquid') - ->and($plugin->render_markup)->toContain('
') - ->and($plugin->render_markup)->toContain('
{{ data.title }}
'); -}); - -it('imports plugin with only shared.blade.php file', function (): void { - $user = User::factory()->create(); - - $zipContent = createMockZipFile([ - 'src/settings.yml' => getValidSettingsYaml(), - 'src/shared.blade.php' => '
{{ $data["title"] }}
', - ]); - - $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); - - $pluginImportService = new PluginImportService(); - $plugin = $pluginImportService->importFromZip($zipFile, $user); - - expect($plugin)->toBeInstanceOf(Plugin::class) - ->and($plugin->markup_language)->toBe('blade') - ->and($plugin->render_markup)->toBe('
{{ $data["title"] }}
') - ->and($plugin->render_markup)->not->toContain('
'); + ->toThrow(Exception::class, "Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas."); }); // Helper methods