From 3f98a70ad9714165aff154c52efc1b14ddc4746e Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sun, 11 Jan 2026 19:34:11 +0100 Subject: [PATCH] feat(#102): added support for Alias plugin --- app/Models/Plugin.php | 3 +- app/Services/ImageGenerationService.php | 102 +++++++++++++----- ...1_11_173757_add_alias_to_plugins_table.php | 28 +++++ .../plugins/recipes/settings.blade.php | 32 ++++-- routes/api.php | 89 ++++++++++++++- 5 files changed, 222 insertions(+), 32 deletions(-) create mode 100644 database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 524f26a..68f8e7e 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -46,6 +46,7 @@ class Plugin extends Model 'dark_mode' => 'boolean', 'preferred_renderer' => 'string', 'plugin_type' => 'string', + 'alias' => 'boolean', ]; protected static function boot() @@ -153,7 +154,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 b8269a3..405ea3f 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -26,11 +26,44 @@ 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 if available, otherwise use device settings - $imageSettings = self::getImageSettings($device); + // Get image generation settings from DeviceModel or Device (for legacy devices) + $imageSettings = $deviceModel + ? self::getImageSettingsFromModel($deviceModel) + : ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null)); $fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png'; $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); @@ -45,7 +78,7 @@ class ImageGenerationService $browserStage->html($markup); // Set timezone from user or fall back to app timezone - $timezone = $device->user->timezone ?? config('app.timezone'); + $timezone = $user?->timezone ?? config('app.timezone'); $browserStage->timezone($timezone); if (config('app.puppeteer_window_size_strategy') === 'v2') { @@ -65,12 +98,12 @@ class ImageGenerationService $browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']); } - // Get palette from device or fallback to device model's default palette - $palette = $device->palette ?? $device->deviceModel?->palette; + // Get palette from parameter or fallback to device model's default palette $colorPalette = null; - if ($palette && $palette->colors) { $colorPalette = $palette->colors; + } elseif ($deviceModel?->palette && $deviceModel->palette->colors) { + $colorPalette = $deviceModel->palette->colors; } $imageStage = new ImageStage(); @@ -107,8 +140,7 @@ class ImageGenerationService throw new RuntimeException('Image file is empty: '.$outputPath); } - $device->update(['current_screen_image' => $uuid]); - Log::info("Device $device->id: updated with new image: $uuid"); + Log::info("Generated image: $uuid"); return $uuid; @@ -125,22 +157,7 @@ class ImageGenerationService { // If device has a DeviceModel, use its settings if ($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, - ]; + return self::getImageSettingsFromModel($device->deviceModel); } // Fallback to device settings @@ -164,6 +181,43 @@ 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/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 new file mode 100644 index 0000000..0a527d7 --- /dev/null +++ b/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php @@ -0,0 +1,28 @@ +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/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php index cb8028f..d07a243 100644 --- a/resources/views/livewire/plugins/recipes/settings.blade.php +++ b/resources/views/livewire/plugins/recipes/settings.blade.php @@ -11,21 +11,18 @@ new class extends Component { public Plugin $plugin; public string|null $trmnlp_id = null; public string|null $uuid = null; + public bool $alias = false; public int $resetIndex = 0; public function mount(): void - { - $this->loadData(); - } - - public function loadData(): void { $this->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 @@ -41,15 +38,21 @@ new class extends Component { ->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, ]); - //$this->loadData(); // Reload to ensure we have the latest data Flux::modal('trmnlp-settings')->close(); } + + public function getAliasUrlProperty(): string + { + return url("/api/display/{$this->uuid}/alias"); + } };?> @@ -70,6 +73,23 @@ new class extends Component { 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
diff --git a/routes/api.php b/routes/api.php index 5700a43..f3a31a1 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 = \Illuminate\Support\Str::uuid()->toString(); + $imageUuid = Str::uuid()->toString(); $filename = $imageUuid.'.'.$extension; $path = 'images/generated/'.$filename; @@ -678,3 +678,90 @@ 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');