From d884ac0a581c3137d9577c69fa7f04f4d718d9fe Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 17 Feb 2026 21:10:54 +0100 Subject: [PATCH] feat(#149): add css_name and css_variables to DeviceModel and update related views --- .../Commands/GenerateDefaultImagesCommand.php | 3 +- app/Jobs/FetchDeviceModelsJob.php | 31 ++++ app/Models/DeviceModel.php | 1 + app/Models/PlaylistItem.php | 6 +- app/Models/Plugin.php | 12 +- app/Services/ImageGenerationService.php | 3 +- ...d_css_variables_to_device_models_table.php | 29 ++++ ...css_variables_for_seeded_device_models.php | 160 ++++++++++++++++++ .../views/default-screens/error.blade.php | 4 +- .../views/default-screens/setup.blade.php | 4 +- .../views/default-screens/sleep.blade.php | 4 +- .../livewire/device-models/index.blade.php | 16 +- .../views/trmnl-layouts/mashup.blade.php | 8 +- .../views/trmnl-layouts/single.blade.php | 9 +- .../vendor/trmnl/components/screen.blade.php | 21 ++- 15 files changed, 285 insertions(+), 26 deletions(-) create mode 100644 database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php create mode 100644 database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php diff --git a/app/Console/Commands/GenerateDefaultImagesCommand.php b/app/Console/Commands/GenerateDefaultImagesCommand.php index e2887df..42e22ba 100644 --- a/app/Console/Commands/GenerateDefaultImagesCommand.php +++ b/app/Console/Commands/GenerateDefaultImagesCommand.php @@ -184,7 +184,7 @@ class GenerateDefaultImagesCommand extends Command }; // Determine device properties from DeviceModel - $deviceVariant = $deviceModel->name ?? 'og'; + $deviceVariant = $deviceModel->css_name ?? $deviceModel->name ?? 'og'; $colorDepth = $deviceModel->color_depth ?? '1bit'; // Use the accessor method $scaleLevel = $deviceModel->scale_level; // Use the accessor method $darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode @@ -196,6 +196,7 @@ class GenerateDefaultImagesCommand extends Command 'deviceVariant' => $deviceVariant, 'colorDepth' => $colorDepth, 'scaleLevel' => $scaleLevel, + 'cssVariables' => $deviceModel->css_variables, ])->render(); } } diff --git a/app/Jobs/FetchDeviceModelsJob.php b/app/Jobs/FetchDeviceModelsJob.php index 9e148b9..2cd39d7 100644 --- a/app/Jobs/FetchDeviceModelsJob.php +++ b/app/Jobs/FetchDeviceModelsJob.php @@ -12,8 +12,10 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; final class FetchDeviceModelsJob implements ShouldQueue { @@ -209,12 +211,41 @@ final class FetchDeviceModelsJob implements ShouldQueue $attributes['palette_id'] = $firstPaletteId; } + $attributes['css_name'] = $this->parseCssNameFromApi($modelData['css'] ?? null); + $attributes['css_variables'] = $this->parseCssVariablesFromApi($modelData['css'] ?? null); + DeviceModel::updateOrCreate( ['name' => $name], $attributes ); } + /** + * Extract css_name from API css payload (strip "screen--" prefix from classes.device). + */ + private function parseCssNameFromApi(mixed $css): ?string + { + $deviceClass = is_array($css) ? Arr::get($css, 'classes.device') : null; + + return (is_string($deviceClass) ? Str::after($deviceClass, 'screen--') : null) ?: null; + } + + /** + * Extract css_variables from API css payload (convert [[key, value], ...] to associative array). + */ + private function parseCssVariablesFromApi(mixed $css): ?array + { + $pairs = is_array($css) ? Arr::get($css, 'variables', []) : []; + if (! is_array($pairs)) { + return null; + } + + $validPairs = Arr::where($pairs, fn (mixed $pair): bool => is_array($pair) && isset($pair[0], $pair[1])); + $variables = Arr::pluck($validPairs, 1, 0); + + return $variables !== [] ? $variables : null; + } + /** * Get the first palette ID from model data. */ diff --git a/app/Models/DeviceModel.php b/app/Models/DeviceModel.php index 6132a76..f2a757f 100644 --- a/app/Models/DeviceModel.php +++ b/app/Models/DeviceModel.php @@ -27,6 +27,7 @@ final class DeviceModel extends Model 'offset_x' => 'integer', 'offset_y' => 'integer', 'published_at' => 'datetime', + 'css_variables' => 'array', ]; public function getColorDepthAttribute(): ?string diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php index 31a6b69..744a012 100644 --- a/app/Models/PlaylistItem.php +++ b/app/Models/PlaylistItem.php @@ -140,8 +140,9 @@ class PlaylistItem extends Model if (! $this->isMashup()) { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->colorDepth(), - 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, 'slot' => $this->plugin instanceof Plugin ? $this->plugin->render('full', false, $device) : throw new Exception('Invalid plugin instance'), @@ -162,8 +163,9 @@ class PlaylistItem extends Model return view('trmnl-layouts.mashup', [ 'colorDepth' => $device?->colorDepth(), - 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, 'mashupLayout' => $this->getMashupLayoutType(), 'slot' => implode('', $pluginMarkups), ])->render(); diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index fab8203..31a08ad 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -584,10 +584,11 @@ class Plugin extends Model if ($size === 'full') { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->colorDepth(), - 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', 'noBleed' => $this->no_bleed, 'darkMode' => $this->dark_mode, 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, 'slot' => $renderedContent, ])->render(); } @@ -595,9 +596,10 @@ class Plugin extends Model return view('trmnl-layouts.mashup', [ 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), 'colorDepth' => $device?->colorDepth(), - 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', 'darkMode' => $this->dark_mode, 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, 'slot' => $renderedContent, ])->render(); @@ -617,10 +619,11 @@ class Plugin extends Model if ($size === 'full') { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->colorDepth(), - 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', 'noBleed' => $this->no_bleed, 'darkMode' => $this->dark_mode, 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, 'slot' => $renderedView, ])->render(); } @@ -628,9 +631,10 @@ class Plugin extends Model return view('trmnl-layouts.mashup', [ 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), 'colorDepth' => $device?->colorDepth(), - 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', 'darkMode' => $this->dark_mode, 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, 'slot' => $renderedView, ])->render(); } diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 87fb6d9..903a493 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -514,7 +514,7 @@ class ImageGenerationService }; // Determine device properties from DeviceModel or device settings - $deviceVariant = $device->deviceVariant(); + $deviceVariant = $device->deviceModel?->css_name ?? $device->deviceVariant(); $deviceOrientation = $device->rotate > 0 ? 'portrait' : 'landscape'; $colorDepth = $device->colorDepth() ?? '1bit'; $scaleLevel = $device->scaleLevel(); @@ -528,6 +528,7 @@ class ImageGenerationService 'deviceOrientation' => $deviceOrientation, 'colorDepth' => $colorDepth, 'scaleLevel' => $scaleLevel, + 'cssVariables' => $device->deviceModel?->css_variables, ]; // Add plugin name for error screens diff --git a/database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php b/database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php new file mode 100644 index 0000000..cd1b7db --- /dev/null +++ b/database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php @@ -0,0 +1,29 @@ +string('css_name')->nullable()->after('kind'); + $table->json('css_variables')->nullable()->after('css_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('device_models', function (Blueprint $table) { + $table->dropColumn(['css_name', 'css_variables']); + }); + } +}; diff --git a/database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php b/database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php new file mode 100644 index 0000000..728fe4f --- /dev/null +++ b/database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php @@ -0,0 +1,160 @@ +}> + */ + private const SEEDED_CSS = [ + 'og_png' => [ + 'css_name' => 'og_png', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'og_plus' => [ + 'css_name' => 'ogv2', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_2024' => [ + 'css_name' => 'amazon_kindle_2024', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '0.8', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_paperwhite_6th_gen' => [ + 'css_name' => 'amazon_kindle_paperwhite_6th_gen', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_paperwhite_7th_gen' => [ + 'css_name' => 'amazon_kindle_paperwhite_7th_gen', + 'css_variables' => [ + '--screen-w' => '905px', + '--screen-h' => '670px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'inkplate_10' => [ + 'css_name' => 'inkplate_10', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '547px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_7' => [ + 'css_name' => 'amazon_kindle_7', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'inky_impression_7_3' => [ + 'css_name' => 'inky_impression_7_3', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'kobo_libra_2' => [ + 'css_name' => 'kobo_libra_2', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '602px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_oasis_2' => [ + 'css_name' => 'amazon_kindle_oasis_2', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '602px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'kobo_aura_one' => [ + 'css_name' => 'kobo_aura_one', + 'css_variables' => [ + '--screen-w' => '1040px', + '--screen-h' => '780px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'kobo_aura_hd' => [ + 'css_name' => 'kobo_aura_hd', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'inky_impression_13_3' => [ + 'css_name' => 'inky_impression_13_3', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + ]; + + /** + * Run the migrations. + */ + public function up(): void + { + foreach (self::SEEDED_CSS as $name => $payload) { + DeviceModel::query() + ->where('name', $name) + ->update([ + 'css_name' => $payload['css_name'], + 'css_variables' => $payload['css_variables'], + ]); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DeviceModel::query() + ->whereIn('name', array_keys(self::SEEDED_CSS)) + ->update([ + 'css_name' => null, + 'css_variables' => null, + ]); + } +}; diff --git a/resources/views/default-screens/error.blade.php b/resources/views/default-screens/error.blade.php index be8063a..7f0d084 100644 --- a/resources/views/default-screens/error.blade.php +++ b/resources/views/default-screens/error.blade.php @@ -5,12 +5,14 @@ 'deviceOrientation' => null, 'colorDepth' => '1bit', 'scaleLevel' => null, + 'cssVariables' => null, 'pluginName' => 'Recipe', ]) + scale-level="{{$scaleLevel}}" + :css-variables="$cssVariables"> diff --git a/resources/views/default-screens/setup.blade.php b/resources/views/default-screens/setup.blade.php index 3b0ff05..ab7ec60 100644 --- a/resources/views/default-screens/setup.blade.php +++ b/resources/views/default-screens/setup.blade.php @@ -5,11 +5,13 @@ 'deviceOrientation' => null, 'colorDepth' => '1bit', 'scaleLevel' => null, + 'cssVariables' => null, ]) + scale-level="{{$scaleLevel}}" + :css-variables="$cssVariables"> diff --git a/resources/views/default-screens/sleep.blade.php b/resources/views/default-screens/sleep.blade.php index 89d6baa..fa0c8cd 100644 --- a/resources/views/default-screens/sleep.blade.php +++ b/resources/views/default-screens/sleep.blade.php @@ -5,11 +5,13 @@ 'deviceOrientation' => null, 'colorDepth' => '1bit', 'scaleLevel' => null, + 'cssVariables' => null, ]) + scale-level="{{$scaleLevel}}" + :css-variables="$cssVariables"> diff --git a/resources/views/livewire/device-models/index.blade.php b/resources/views/livewire/device-models/index.blade.php index 1aebeb1..7de5872 100644 --- a/resources/views/livewire/device-models/index.blade.php +++ b/resources/views/livewire/device-models/index.blade.php @@ -39,6 +39,8 @@ new class extends Component public $palette_id; + public $css_name; + protected $rules = [ 'name' => 'required|string|max:255|unique:device_models,name', 'label' => 'required|string|max:255', @@ -102,10 +104,11 @@ new class extends Component $this->offset_y = $deviceModel->offset_y; $this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i'); $this->palette_id = $deviceModel->palette_id; + $this->css_name = $deviceModel->css_name; } else { $this->editingDeviceModelId = null; $this->viewingDeviceModelId = null; - $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id']); + $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id', 'css_name']); $this->mime_type = 'image/png'; $this->scale_factor = 1.0; $this->rotation = 0; @@ -131,6 +134,7 @@ new class extends Component 'offset_y' => 'required|integer', 'published_at' => 'nullable|date', 'palette_id' => 'nullable|exists:device_palettes,id', + 'css_name' => 'nullable|string|max:255', ]; if ($this->editingDeviceModelId) { @@ -158,6 +162,7 @@ new class extends Component 'offset_y' => $this->offset_y, 'published_at' => $this->published_at, 'palette_id' => $this->palette_id ?: null, + 'css_name' => $this->css_name ?: null, ]); $message = 'Device model updated successfully.'; } else { @@ -176,12 +181,13 @@ new class extends Component 'offset_y' => $this->offset_y, 'published_at' => $this->published_at, 'palette_id' => $this->palette_id ?: null, + 'css_name' => $this->css_name ?: null, 'source' => 'manual', ]); $message = 'Device model created successfully.'; } - $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id', 'editingDeviceModelId', 'viewingDeviceModelId']); + $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id', 'css_name', 'editingDeviceModelId', 'viewingDeviceModelId']); Flux::modal('device-model-modal')->close(); $this->deviceModels = DeviceModel::all(); @@ -217,6 +223,7 @@ new class extends Component $this->offset_y = $deviceModel->offset_y; $this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i'); $this->palette_id = $deviceModel->palette_id; + $this->css_name = $deviceModel->css_name; $this->js('Flux.modal("device-model-modal").show()'); } @@ -344,6 +351,11 @@ new class extends Component +
+ +
+ @if (!$viewingDeviceModelId)
diff --git a/resources/views/trmnl-layouts/mashup.blade.php b/resources/views/trmnl-layouts/mashup.blade.php index 1d8321f..0e1cb3c 100644 --- a/resources/views/trmnl-layouts/mashup.blade.php +++ b/resources/views/trmnl-layouts/mashup.blade.php @@ -6,18 +6,22 @@ 'deviceOrientation' => null, 'colorDepth' => '1bit', 'scaleLevel' => null, + 'cssVariables' => null, ]) @if(config('app.puppeteer_window_size_strategy') === 'v2') + scale-level="{{$scaleLevel}}" + :css-variables="$cssVariables"> {!! $slot !!} @else - + {!! $slot !!} diff --git a/resources/views/trmnl-layouts/single.blade.php b/resources/views/trmnl-layouts/single.blade.php index c6d6499..09f5e52 100644 --- a/resources/views/trmnl-layouts/single.blade.php +++ b/resources/views/trmnl-layouts/single.blade.php @@ -5,16 +5,21 @@ 'deviceOrientation' => null, 'colorDepth' => '1bit', 'scaleLevel' => null, + 'cssVariables' => null, ]) @if(config('app.puppeteer_window_size_strategy') === 'v2') + scale-level="{{$scaleLevel}}" + :css-variables="$cssVariables"> {!! $slot !!} @else - + {!! $slot !!} @endif diff --git a/resources/views/vendor/trmnl/components/screen.blade.php b/resources/views/vendor/trmnl/components/screen.blade.php index 1ff7d23..320a34b 100644 --- a/resources/views/vendor/trmnl/components/screen.blade.php +++ b/resources/views/vendor/trmnl/components/screen.blade.php @@ -1,19 +1,13 @@ @props([ 'noBleed' => false, 'darkMode' => false, - 'deviceVariant' => 'og', + 'deviceVariant' => 'ogv2', 'deviceOrientation' => null, 'colorDepth' => '1bit', 'scaleLevel' => null, + 'cssVariables' => null, ]) -@php -// HOTFIX Github Issue https://github.com/usetrmnl/byos_laravel/issues/190 -if ($colorDepth == '2bit'){ - $deviceVariant = 'ogv2'; -} -@endphp - @@ -33,9 +27,18 @@ if ($colorDepth == '2bit'){ @endif {{ $title ?? config('app.name') }} + @if(config('app.puppeteer_window_size_strategy') === 'v2' && !empty($cssVariables) && is_array($cssVariables)) + + @endif -
+
{{ $slot }}