diff --git a/.gitignore b/.gitignore index 9c0185e..838d9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ yarn-error.log /.opencode /build.sh /.junie +/.agents diff --git a/Dockerfile b/Dockerfile index 2d761ed..5af7b33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ENV TRMNL_LIQUID_ENABLED=1 # Switch to the root user so we can do root things USER root -COPY --chown=www-data:www-data --from=bnussbau/trmnl-liquid-cli:0.1.0 /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ +COPY --chown=www-data:www-data --from=bnussbau/trmnl-liquid-cli:0.2.0 /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ # Set the working directory WORKDIR /var/www/html diff --git a/README.md b/README.md index 2231b24..670b62c 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ php artisan db:seed --class=ExampleRecipesSeeder | `REGISTRATION_ENABLED` | Allow user registration via Webinterface | 1 | | `SSL_MODE` | SSL Mode, if not using a Reverse Proxy ([docs](https://serversideup.net/open-source/docker-php/docs/customizing-the-image/configuring-ssl)) | `off` | | `FORCE_HTTPS` | If your server handles SSL termination, enforce HTTPS. | 0 | +| `TRUSTED_PROXIES` | If your server handles SSL termination, allow mixed mode. e.g. `"172.0.0.0/8"` or `*` | null | | `PHP_OPCACHE_ENABLE` | Enable PHP Opcache | 0 | | `TRMNL_IMAGE_URL_TIMEOUT` | How long TRMNL waits for a response on the display endpoint. (sec) | 30 | | `APP_TIMEZONE` | Default timezone, which will be used by the PHP date functions | UTC | diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index bc46559..5eeeb6b 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -60,8 +60,14 @@ class Plugin extends Model }); static::updating(function ($model): void { - // Reset image cache when markup changes - if ($model->isDirty('render_markup')) { + // Reset image cache when any markup changes + if ($model->isDirty([ + 'render_markup', + 'render_markup_half_horizontal', + 'render_markup_half_vertical', + 'render_markup_quadrant', + 'render_markup_shared', + ])) { $model->current_image = null; } }); @@ -421,7 +427,9 @@ class Plugin extends Model throw new InvalidArgumentException('Render method is only applicable for recipe plugins.'); } - if ($this->render_markup) { + $markup = $this->getMarkupForSize($size); + + if ($markup) { $renderedContent = ''; if ($this->markup_language === 'liquid') { @@ -471,7 +479,7 @@ class Plugin extends Model // Check if external renderer should be used if ($this->preferred_renderer === 'trmnl-liquid' && config('services.trmnl.liquid_enabled')) { // Use external Ruby renderer - pass raw template without preprocessing - $renderedContent = $this->renderWithExternalLiquidRenderer($this->render_markup, $context); + $renderedContent = $this->renderWithExternalLiquidRenderer($markup, $context); } else { // Use PHP keepsuit/liquid renderer // Create a custom environment with inline templates support @@ -493,14 +501,14 @@ class Plugin extends Model $environment->tagRegistry->register(TemplateTag::class); // Apply Liquid replacements (including 'with' syntax conversion) - $processedMarkup = $this->applyLiquidReplacements($this->render_markup); + $processedMarkup = $this->applyLiquidReplacements($markup); $template = $environment->parseString($processedMarkup); $liquidContext = $environment->newRenderContext(data: $context); $renderedContent = $template->render($liquidContext); } } else { - $renderedContent = Blade::render($this->render_markup, [ + $renderedContent = Blade::render($markup, [ 'size' => $size, 'data' => $this->data_payload, 'config' => $this->configuration ?? [], @@ -581,6 +589,30 @@ class Plugin extends Model return $this->configuration[$key] ?? $default; } + /** + * Get the appropriate markup for a given size, including shared prepending logic + * + * @param string $size The layout size (full, half_horizontal, half_vertical, quadrant) + * @return string|null The markup code for the given size, with shared prepended if available + */ + public function getMarkupForSize(string $size): ?string + { + $markup = match ($size) { + 'full' => $this->render_markup, + 'half_horizontal' => $this->render_markup_half_horizontal ?? $this->render_markup, + 'half_vertical' => $this->render_markup_half_vertical ?? $this->render_markup, + 'quadrant' => $this->render_markup_quadrant ?? $this->render_markup, + default => $this->render_markup, + }; + + // Prepend shared markup if it exists + if ($markup && $this->render_markup_shared) { + $markup = $this->render_markup_shared."\n".$markup; + } + + return $markup; + } + public function getPreviewMashupLayoutForSize(string $size): string { return match ($size) { diff --git a/app/Services/PluginExportService.php b/app/Services/PluginExportService.php index 241764d..be98461 100644 --- a/app/Services/PluginExportService.php +++ b/app/Services/PluginExportService.php @@ -51,17 +51,35 @@ class PluginExportService $settings = $this->generateSettingsYaml($plugin); $settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); File::put($tempDir.'/settings.yml', $settingsYaml); - // Generate full template content - $fullTemplate = $this->generateFullTemplate($plugin); + $extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php'; - File::put($tempDir.'/full.'.$extension, $fullTemplate); - // Generate shared.liquid if needed (for liquid templates) - if ($plugin->markup_language === 'liquid') { - $sharedTemplate = $this->generateSharedTemplate(); - /** @phpstan-ignore-next-line */ - if ($sharedTemplate) { - File::put($tempDir.'/shared.liquid', $sharedTemplate); - } + + // Export full template if it exists + if ($plugin->render_markup) { + $fullTemplate = $this->generateLayoutTemplate($plugin->render_markup); + File::put($tempDir.'/full.'.$extension, $fullTemplate); + } + + // Export layout-specific templates if they exist + if ($plugin->render_markup_half_horizontal) { + $halfHorizontalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_horizontal); + File::put($tempDir.'/half_horizontal.'.$extension, $halfHorizontalTemplate); + } + + if ($plugin->render_markup_half_vertical) { + $halfVerticalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_vertical); + File::put($tempDir.'/half_vertical.'.$extension, $halfVerticalTemplate); + } + + if ($plugin->render_markup_quadrant) { + $quadrantTemplate = $this->generateLayoutTemplate($plugin->render_markup_quadrant); + File::put($tempDir.'/quadrant.'.$extension, $quadrantTemplate); + } + + // Export shared template if it exists + if ($plugin->render_markup_shared) { + $sharedTemplate = $this->generateLayoutTemplate($plugin->render_markup_shared); + File::put($tempDir.'/shared.'.$extension, $sharedTemplate); } // Create ZIP file $zipPath = $tempDir.'/plugin_'.$plugin->trmnlp_id.'.zip'; @@ -124,29 +142,21 @@ class PluginExportService } /** - * Generate the full template content + * Generate template content from markup, removing wrapper divs if present */ - private function generateFullTemplate(Plugin $plugin): string + private function generateLayoutTemplate(?string $markup): string { - $markup = $plugin->render_markup; + if (! $markup) { + return ''; + } - // Remove the wrapper div if it exists (it will be added during import) + // Remove the wrapper div if it exists (it will be added during import for liquid) $markup = preg_replace('/^
\s*/', '', $markup); $markup = preg_replace('/\s*<\/div>\s*$/', '', $markup); return mb_trim($markup); } - /** - * Generate the shared template content (for liquid templates) - */ - private function generateSharedTemplate(): null - { - // For now, we don't have a way to store shared templates separately - // TODO - add support for shared templates - return null; - } - /** * Add a directory and its contents to a ZIP file */ diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index 51a9aee..f3e7a5c 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -93,37 +93,59 @@ class PluginImportService $settings = Yaml::parse($settingsYaml); $this->validateYAML($settings); - // Determine which template file to use and read its content - $templatePath = null; + // Determine markup language from the first available file $markupLanguage = 'blade'; + $firstTemplatePath = $filePaths['fullLiquidPath'] + ?? ($filePaths['halfHorizontalLiquidPath'] ?? null) + ?? ($filePaths['halfVerticalLiquidPath'] ?? null) + ?? ($filePaths['quadrantLiquidPath'] ?? null) + ?? ($filePaths['sharedLiquidPath'] ?? null) + ?? ($filePaths['sharedBladePath'] ?? null); - if ($filePaths['fullLiquidPath']) { - $templatePath = $filePaths['fullLiquidPath']; - $fullLiquid = File::get($templatePath); + if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } - // 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'; + // Read full markup (don't prepend shared - it will be prepended at render time) + $fullLiquid = null; + if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) { + $fullLiquid = File::get($filePaths['fullLiquidPath']); + if ($markupLanguage === 'liquid') { $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; } - } elseif ($filePaths['sharedLiquidPath']) { - $templatePath = $filePaths['sharedLiquidPath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'liquid'; - $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } elseif ($filePaths['sharedBladePath']) { - $templatePath = $filePaths['sharedBladePath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'blade'; + } + + // Read shared markup separately + $sharedMarkup = null; + if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedMarkup = File::get($filePaths['sharedLiquidPath']); + } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedMarkup = File::get($filePaths['sharedBladePath']); + } + + // Read layout-specific markups + $halfHorizontalMarkup = null; + if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) { + $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
'; + } + } + + $halfVerticalMarkup = null; + if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) { + $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
'; + } + } + + $quadrantMarkup = null; + if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) { + $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']); + if ($markupLanguage === 'liquid') { + $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
'; + } } // Ensure custom_fields is properly formatted @@ -160,6 +182,10 @@ class PluginImportService 'polling_body' => $settings['polling_body'] ?? null, 'markup_language' => $markupLanguage, 'render_markup' => $fullLiquid ?? null, + 'render_markup_half_horizontal' => $halfHorizontalMarkup, + 'render_markup_half_vertical' => $halfVerticalMarkup, + 'render_markup_quadrant' => $quadrantMarkup, + 'render_markup_shared' => $sharedMarkup, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), ]); @@ -246,37 +272,59 @@ class PluginImportService $settings = Yaml::parse($settingsYaml); $this->validateYAML($settings); - // Determine which template file to use and read its content - $templatePath = null; + // Determine markup language from the first available file $markupLanguage = 'blade'; + $firstTemplatePath = $filePaths['fullLiquidPath'] + ?? ($filePaths['halfHorizontalLiquidPath'] ?? null) + ?? ($filePaths['halfVerticalLiquidPath'] ?? null) + ?? ($filePaths['quadrantLiquidPath'] ?? null) + ?? ($filePaths['sharedLiquidPath'] ?? null) + ?? ($filePaths['sharedBladePath'] ?? null); - if ($filePaths['fullLiquidPath']) { - $templatePath = $filePaths['fullLiquidPath']; - $fullLiquid = File::get($templatePath); + if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } - // 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'; + // Read full markup (don't prepend shared - it will be prepended at render time) + $fullLiquid = null; + if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) { + $fullLiquid = File::get($filePaths['fullLiquidPath']); + if ($markupLanguage === 'liquid') { $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; } - } elseif ($filePaths['sharedLiquidPath']) { - $templatePath = $filePaths['sharedLiquidPath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'liquid'; - $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } elseif ($filePaths['sharedBladePath']) { - $templatePath = $filePaths['sharedBladePath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'blade'; + } + + // Read shared markup separately + $sharedMarkup = null; + if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedMarkup = File::get($filePaths['sharedLiquidPath']); + } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedMarkup = File::get($filePaths['sharedBladePath']); + } + + // Read layout-specific markups + $halfHorizontalMarkup = null; + if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) { + $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
'; + } + } + + $halfVerticalMarkup = null; + if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) { + $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
'; + } + } + + $quadrantMarkup = null; + if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) { + $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']); + if ($markupLanguage === 'liquid') { + $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
'; + } } // Ensure custom_fields is properly formatted @@ -322,6 +370,10 @@ class PluginImportService 'polling_body' => $settings['polling_body'] ?? null, 'markup_language' => $markupLanguage, 'render_markup' => $fullLiquid ?? null, + 'render_markup_half_horizontal' => $halfHorizontalMarkup, + 'render_markup_half_vertical' => $halfVerticalMarkup, + 'render_markup_quadrant' => $quadrantMarkup, + 'render_markup_shared' => $sharedMarkup, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), 'preferred_renderer' => $preferredRenderer, @@ -357,6 +409,9 @@ class PluginImportService $fullLiquidPath = null; $sharedLiquidPath = null; $sharedBladePath = null; + $halfHorizontalLiquidPath = null; + $halfVerticalLiquidPath = null; + $quadrantLiquidPath = null; // If zipEntryPath is specified, look for files in that specific directory first if ($zipEntryPath) { @@ -377,6 +432,25 @@ class PluginImportService } elseif (File::exists($targetDir.'/shared.blade.php')) { $sharedBladePath = $targetDir.'/shared.blade.php'; } + + // Check for layout-specific files + if (File::exists($targetDir.'/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.liquid'; + } elseif (File::exists($targetDir.'/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.blade.php'; + } + + if (File::exists($targetDir.'/half_vertical.liquid')) { + $halfVerticalLiquidPath = $targetDir.'/half_vertical.liquid'; + } elseif (File::exists($targetDir.'/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $targetDir.'/half_vertical.blade.php'; + } + + if (File::exists($targetDir.'/quadrant.liquid')) { + $quadrantLiquidPath = $targetDir.'/quadrant.liquid'; + } elseif (File::exists($targetDir.'/quadrant.blade.php')) { + $quadrantLiquidPath = $targetDir.'/quadrant.blade.php'; + } } // Check if files are in src subdirectory of target directory @@ -394,6 +468,25 @@ class PluginImportService } elseif (File::exists($targetDir.'/src/shared.blade.php')) { $sharedBladePath = $targetDir.'/src/shared.blade.php'; } + + // Check for layout-specific files in src + if (File::exists($targetDir.'/src/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.liquid'; + } elseif (File::exists($targetDir.'/src/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.blade.php'; + } + + if (File::exists($targetDir.'/src/half_vertical.liquid')) { + $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.liquid'; + } elseif (File::exists($targetDir.'/src/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.blade.php'; + } + + if (File::exists($targetDir.'/src/quadrant.liquid')) { + $quadrantLiquidPath = $targetDir.'/src/quadrant.liquid'; + } elseif (File::exists($targetDir.'/src/quadrant.blade.php')) { + $quadrantLiquidPath = $targetDir.'/src/quadrant.blade.php'; + } } // If we found the required files in the target directory, return them @@ -425,6 +518,25 @@ class PluginImportService } elseif (File::exists($tempDir.'/src/shared.blade.php')) { $sharedBladePath = $tempDir.'/src/shared.blade.php'; } + + // Check for layout-specific files + if (File::exists($tempDir.'/src/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.liquid'; + } elseif (File::exists($tempDir.'/src/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.blade.php'; + } + + if (File::exists($tempDir.'/src/half_vertical.liquid')) { + $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.liquid'; + } elseif (File::exists($tempDir.'/src/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.blade.php'; + } + + if (File::exists($tempDir.'/src/quadrant.liquid')) { + $quadrantLiquidPath = $tempDir.'/src/quadrant.liquid'; + } elseif (File::exists($tempDir.'/src/quadrant.blade.php')) { + $quadrantLiquidPath = $tempDir.'/src/quadrant.blade.php'; + } } else { // Search for the files in the extracted directory structure $directories = new RecursiveDirectoryIterator($tempDir, RecursiveDirectoryIterator::SKIP_DOTS); @@ -442,6 +554,12 @@ class PluginImportService $sharedLiquidPath = $filepath; } elseif ($filename === 'shared.blade.php') { $sharedBladePath = $filepath; + } elseif ($filename === 'half_horizontal.liquid' || $filename === 'half_horizontal.blade.php') { + $halfHorizontalLiquidPath = $filepath; + } elseif ($filename === 'half_vertical.liquid' || $filename === 'half_vertical.blade.php') { + $halfVerticalLiquidPath = $filepath; + } elseif ($filename === 'quadrant.liquid' || $filename === 'quadrant.blade.php') { + $quadrantLiquidPath = $filepath; } } @@ -485,6 +603,25 @@ class PluginImportService $sharedBladePath = $newSrcDir.'/shared.blade.php'; } + // Copy layout-specific files if they exist + if ($halfHorizontalLiquidPath) { + $extension = pathinfo((string) $halfHorizontalLiquidPath, PATHINFO_EXTENSION); + File::copy($halfHorizontalLiquidPath, $newSrcDir.'/half_horizontal.'.$extension); + $halfHorizontalLiquidPath = $newSrcDir.'/half_horizontal.'.$extension; + } + + if ($halfVerticalLiquidPath) { + $extension = pathinfo((string) $halfVerticalLiquidPath, PATHINFO_EXTENSION); + File::copy($halfVerticalLiquidPath, $newSrcDir.'/half_vertical.'.$extension); + $halfVerticalLiquidPath = $newSrcDir.'/half_vertical.'.$extension; + } + + if ($quadrantLiquidPath) { + $extension = pathinfo((string) $quadrantLiquidPath, PATHINFO_EXTENSION); + File::copy($quadrantLiquidPath, $newSrcDir.'/quadrant.'.$extension); + $quadrantLiquidPath = $newSrcDir.'/quadrant.'.$extension; + } + // Update the paths $settingsYamlPath = $newSrcDir.'/settings.yml'; } @@ -496,6 +633,9 @@ class PluginImportService 'fullLiquidPath' => $fullLiquidPath, 'sharedLiquidPath' => $sharedLiquidPath, 'sharedBladePath' => $sharedBladePath, + 'halfHorizontalLiquidPath' => $halfHorizontalLiquidPath, + 'halfVerticalLiquidPath' => $halfVerticalLiquidPath, + 'quadrantLiquidPath' => $quadrantLiquidPath, ]; } diff --git a/composer.json b/composer.json index d856e75..96e0079 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "ext-imagick": "*", "ext-simplexml": "*", "ext-zip": "*", - "bnussbau/laravel-trmnl-blade": "2.2.*", + "bnussbau/laravel-trmnl-blade": "2.1.1", "bnussbau/trmnl-pipeline-php": "^0.6.0", "keepsuit/laravel-liquid": "^0.5.2", "laravel/fortify": "^1.30", diff --git a/composer.lock b/composer.lock index ec617d3..a61b132 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "581bacf794841fc11c540e152c704d16", + "content-hash": "60a7e51edd8408cffdb901e4a1c1684a", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.27", + "version": "3.369.29", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "f844afab2a74eb3cf881970a9c31de460510eb74" + "reference": "068195b2980cf5cf4ade2515850d461186db3310" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f844afab2a74eb3cf881970a9c31de460510eb74", - "reference": "f844afab2a74eb3cf881970a9c31de460510eb74", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/068195b2980cf5cf4ade2515850d461186db3310", + "reference": "068195b2980cf5cf4ade2515850d461186db3310", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.369.27" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.29" }, - "time": "2026-02-04T19:07:08+00:00" + "time": "2026-02-06T19:08:50+00:00" }, { "name": "bacon/bacon-qr-code", @@ -214,16 +214,16 @@ }, { "name": "bnussbau/laravel-trmnl-blade", - "version": "2.2.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "6db8a82a15ccedcaaffd3b37d0d337d276a26669" + "reference": "6ad96eba917ebc30ebe550e6fce4a995e94f6b35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/6db8a82a15ccedcaaffd3b37d0d337d276a26669", - "reference": "6db8a82a15ccedcaaffd3b37d0d337d276a26669", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/6ad96eba917ebc30ebe550e6fce4a995e94f6b35", + "reference": "6ad96eba917ebc30ebe550e6fce4a995e94f6b35", "shasum": "" }, "require": { @@ -278,7 +278,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.2.1" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.1" }, "funding": [ { @@ -294,7 +294,7 @@ "type": "github" } ], - "time": "2026-02-05T17:57:37+00:00" + "time": "2026-01-29T20:40:42+00:00" }, { "name": "bnussbau/trmnl-pipeline-php", @@ -3194,16 +3194,16 @@ }, { "name": "livewire/livewire", - "version": "v4.1.2", + "version": "v4.1.3", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "8adef21f35f4ffa87fd2f3655b350236df0c39a8" + "reference": "69c871cb15fb95f10cda5acd1ee7e63cd3c494c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/8adef21f35f4ffa87fd2f3655b350236df0c39a8", - "reference": "8adef21f35f4ffa87fd2f3655b350236df0c39a8", + "url": "https://api.github.com/repos/livewire/livewire/zipball/69c871cb15fb95f10cda5acd1ee7e63cd3c494c8", + "reference": "69c871cb15fb95f10cda5acd1ee7e63cd3c494c8", "shasum": "" }, "require": { @@ -3258,7 +3258,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v4.1.2" + "source": "https://github.com/livewire/livewire/tree/v4.1.3" }, "funding": [ { @@ -3266,7 +3266,7 @@ "type": "github" } ], - "time": "2026-02-03T03:01:29+00:00" + "time": "2026-02-06T12:19:55+00:00" }, { "name": "maennchen/zipstream-php", @@ -9066,16 +9066,16 @@ }, { "name": "laravel/boost", - "version": "v2.0.6", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "1e1cb76e8e87ca3dd3c3d64deccbc97f4de38215" + "reference": "1c7d6f44c96937a961056778b9143218b1183302" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/1e1cb76e8e87ca3dd3c3d64deccbc97f4de38215", - "reference": "1e1cb76e8e87ca3dd3c3d64deccbc97f4de38215", + "url": "https://api.github.com/repos/laravel/boost/zipball/1c7d6f44c96937a961056778b9143218b1183302", + "reference": "1c7d6f44c96937a961056778b9143218b1183302", "shasum": "" }, "require": { @@ -9128,7 +9128,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-02-04T10:10:48+00:00" + "time": "2026-02-06T10:41:29+00:00" }, { "name": "laravel/mcp", @@ -10484,16 +10484,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.2", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", - "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", "shasum": "" }, "require": { @@ -10549,7 +10549,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" }, "funding": [ { @@ -10569,7 +10569,7 @@ "type": "tidelift" } ], - "time": "2025-12-24T07:03:04+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10935,21 +10935,21 @@ }, { "name": "rector/rector", - "version": "2.3.5", + "version": "2.3.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070" + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070", - "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b", + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.36" + "phpstan/phpstan": "^2.1.38" }, "conflict": { "rector/rector-doctrine": "*", @@ -10983,7 +10983,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.5" + "source": "https://github.com/rectorphp/rector/tree/2.3.6" }, "funding": [ { @@ -10991,7 +10991,7 @@ "type": "github" } ], - "time": "2026-01-28T15:22:48+00:00" + "time": "2026-02-06T14:25:06+00:00" }, { "name": "sebastian/cli-parser", diff --git a/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php new file mode 100644 index 0000000..e56751c --- /dev/null +++ b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php @@ -0,0 +1,38 @@ +text('render_markup_half_horizontal')->nullable()->after('render_markup'); + $table->text('render_markup_half_vertical')->nullable()->after('render_markup_half_horizontal'); + $table->text('render_markup_quadrant')->nullable()->after('render_markup_half_vertical'); + $table->text('render_markup_shared')->nullable()->after('render_markup_quadrant'); + $table->text('transform_code')->nullable()->after('render_markup_shared'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn([ + 'render_markup_half_horizontal', + 'render_markup_half_vertical', + 'render_markup_quadrant', + 'render_markup_shared', + 'transform_code', + ]); + }); + } +}; diff --git a/resources/views/livewire/device-models/index.blade.php b/resources/views/livewire/device-models/index.blade.php index 6ec4014..1aebeb1 100644 --- a/resources/views/livewire/device-models/index.blade.php +++ b/resources/views/livewire/device-models/index.blade.php @@ -1,5 +1,6 @@ deviceModels = DeviceModel::all(); + $this->devicePalettes = DevicePalette::all(); + session()->flash('message', 'Device models updated from API.'); + } + public function openDeviceModelModal(?string $deviceModelId = null, bool $viewOnly = false): void { if ($deviceModelId) { @@ -229,9 +238,17 @@ new class extends Component
- - Add Device Model - + + + Add Device Model + + + + + Update from Models API + + + @if (session()->has('message'))
diff --git a/resources/views/livewire/device-palettes/index.blade.php b/resources/views/livewire/device-palettes/index.blade.php index 6640545..4e96c31 100644 --- a/resources/views/livewire/device-palettes/index.blade.php +++ b/resources/views/livewire/device-palettes/index.blade.php @@ -1,5 +1,6 @@ devicePalettes = DevicePalette::all(); + session()->flash('message', 'Device palettes updated from API.'); + } + public function openDevicePaletteModal(?string $devicePaletteId = null, bool $viewOnly = false): void { if ($devicePaletteId) { @@ -202,9 +210,17 @@ new class extends Component
- - Add Device Palette - + + + Add Device Palette + + + + + Update from API + + + @if (session()->has('message'))
diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index 93183cf..4c9b4c5 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -31,6 +31,10 @@ new class extends Component public $device_model_id; + public $is_mirror = false; + + public $mirror_device_id = null; + // Signal to device to use high compatibility approaches when redrawing content public $maximum_compatibility = false; @@ -98,6 +102,8 @@ new class extends Component $this->sleep_mode_from = optional($device->sleep_mode_from)->format('H:i'); $this->sleep_mode_to = optional($device->sleep_mode_to)->format('H:i'); $this->special_function = $device->special_function; + $this->is_mirror = $device->mirror_device_id !== null; + $this->mirror_device_id = $device->mirror_device_id; return view('livewire.devices.configure', [ 'image' => ($current_image_uuid) ? url($current_image_path) : null, @@ -145,6 +151,7 @@ new class extends Component 'rotate' => 'required|integer|min:0|max:359', 'image_format' => 'required|string', 'device_model_id' => 'nullable|exists:device_models,id', + 'mirror_device_id' => 'required_if:is_mirror,true', 'maximum_compatibility' => 'boolean', 'sleep_mode_enabled' => 'boolean', 'sleep_mode_from' => 'nullable|date_format:H:i', @@ -152,6 +159,13 @@ new class extends Component 'special_function' => 'nullable|string', ]); + if ($this->is_mirror) { + $mirrorDevice = auth()->user()->devices()->find($this->mirror_device_id); + abort_unless($mirrorDevice, 403, 'Invalid mirror device selected'); + abort_if($mirrorDevice->mirror_device_id !== null, 403, 'Cannot mirror a device that is already a mirror device'); + abort_if((int) $this->mirror_device_id === (int) $this->device->id, 403, 'Device cannot mirror itself'); + } + // Convert empty string to null for custom selection $deviceModelId = empty($this->device_model_id) ? null : $this->device_model_id; @@ -165,6 +179,7 @@ new class extends Component 'rotate' => $this->rotate, 'image_format' => $this->image_format, 'device_model_id' => $deviceModelId, + 'mirror_device_id' => $this->is_mirror ? $this->mirror_device_id : null, 'maximum_compatibility' => $this->maximum_compatibility, 'sleep_mode_enabled' => $this->sleep_mode_enabled, 'sleep_mode_from' => $this->sleep_mode_from, @@ -433,6 +448,18 @@ new class extends Component @endforeach + + @if($is_mirror) + + Select a device + @foreach(auth()->user()->devices->where('mirror_device_id', null)->where('id', '!=', $device->id) as $mirrorOption) + + {{ $mirrorOption->name }} ({{ $mirrorOption->friendly_id }}) + + @endforeach + + @endif + @if(empty($device_model_id)) diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index 90f8aa0..848fc67 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -258,7 +258,6 @@ new class extends Component
Limitations
    -
  • Only full view will be imported; shared markup will be prepended
  • Some Liquid filters may be not supported or behave differently
  • API responses in formats other than JSON are not yet supported
  • {{--
      --}} @@ -312,7 +311,6 @@ new class extends Component Limitations
        -
      • Only full view will be imported; shared markup will be prepended
      • Requires trmnl-liquid-cli executable.
      • API responses in formats other than JSON are not yet fully supported.
      • There are limitations in payload size (Data Payload, Template).
      • diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index cda019e..a7b3918 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -65,6 +65,12 @@ new class extends Component public string $preview_size = 'full'; + public array $markup_layouts = []; + + public array $active_tabs = []; + + public string $active_tab = 'full'; + public function mount(): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); @@ -91,7 +97,24 @@ new class extends Component $this->view_content = null; } } else { - $this->markup_code = $this->plugin->render_markup; + // Initialize layout markups from plugin columns + $this->markup_layouts = [ + 'full' => $this->plugin->render_markup ?? '', + 'half_horizontal' => $this->plugin->render_markup_half_horizontal ?? '', + 'half_vertical' => $this->plugin->render_markup_half_vertical ?? '', + 'quadrant' => $this->plugin->render_markup_quadrant ?? '', + 'shared' => $this->plugin->render_markup_shared ?? '', + ]; + + // Set active tabs based on which layouts have content + $this->active_tabs = ['full']; // Full is always active + foreach (['half_horizontal', 'half_vertical', 'quadrant', 'shared'] as $layout) { + if (! empty($this->markup_layouts[$layout])) { + $this->active_tabs[] = $layout; + } + } + + $this->markup_code = $this->markup_layouts['full']; $this->markup_language = $this->plugin->markup_language ?? 'blade'; } @@ -125,12 +148,108 @@ new class extends Component { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); $this->validate(); + + // Update markup_code for the active tab + if (isset($this->markup_layouts[$this->active_tab])) { + $this->markup_layouts[$this->active_tab] = $this->markup_code ?? ''; + } + + // Save all layout markups to respective columns $this->plugin->update([ - 'render_markup' => $this->markup_code ?? null, + 'render_markup' => $this->markup_layouts['full'] ?? null, + 'render_markup_half_horizontal' => ! empty($this->markup_layouts['half_horizontal']) ? $this->markup_layouts['half_horizontal'] : null, + 'render_markup_half_vertical' => ! empty($this->markup_layouts['half_vertical']) ? $this->markup_layouts['half_vertical'] : null, + 'render_markup_quadrant' => ! empty($this->markup_layouts['quadrant']) ? $this->markup_layouts['quadrant'] : null, + 'render_markup_shared' => ! empty($this->markup_layouts['shared']) ? $this->markup_layouts['shared'] : null, 'markup_language' => $this->markup_language ?? null, ]); } + public function addLayoutTab(string $layout): void + { + if (! in_array($layout, $this->active_tabs, true)) { + $this->active_tabs[] = $layout; + if (! isset($this->markup_layouts[$layout])) { + $this->markup_layouts[$layout] = ''; + } + $this->switchTab($layout); + } + } + + public function removeLayoutTab(string $layout): void + { + if ($layout !== 'full') { + $this->active_tabs = array_values(array_filter($this->active_tabs, fn ($tab) => $tab !== $layout)); + if (isset($this->markup_layouts[$layout])) { + $this->markup_layouts[$layout] = ''; + } + if ($this->active_tab === $layout) { + $this->active_tab = 'full'; + $this->markup_code = $this->markup_layouts['full'] ?? ''; + } + } + } + + public function switchTab(string $layout): void + { + if (in_array($layout, $this->active_tabs, true)) { + // Save current tab's content before switching + if (isset($this->markup_layouts[$this->active_tab])) { + $this->markup_layouts[$this->active_tab] = $this->markup_code ?? ''; + } + + $this->active_tab = $layout; + $this->markup_code = $this->markup_layouts[$layout] ?? ''; + } + } + + public function toggleLayoutTab(string $layout): void + { + if ($layout === 'full') { + return; + } + + if (in_array($layout, $this->active_tabs, true)) { + $this->removeLayoutTab($layout); + } else { + $this->addLayoutTab($layout); + } + } + + public function getAvailableLayouts(): array + { + return [ + 'half_horizontal' => 'Half Horizontal', + 'half_vertical' => 'Half Vertical', + 'quadrant' => 'Quadrant', + 'shared' => 'Shared', + ]; + } + + public function getLayoutLabel(string $layout): string + { + return match ($layout) { + 'full' => $this->getFullTabLabel(), + 'half_horizontal' => 'Half Horizontal', + 'half_vertical' => 'Half Vertical', + 'quadrant' => 'Quadrant', + 'shared' => 'Shared', + default => ucfirst($layout), + }; + } + + public function getFullTabLabel(): string + { + // Return "Full" if any layout-specific markup exists, otherwise "Responsive" + if (! empty($this->markup_layouts['half_horizontal']) + || ! empty($this->markup_layouts['half_vertical']) + || ! empty($this->markup_layouts['quadrant'])) { + return 'Full'; + } + + return 'Responsive'; + } + protected array $rules = [ 'name' => 'required|string|max:255', 'data_stale_minutes' => 'required|integer|min:1', @@ -1018,41 +1137,75 @@ HTML; @if(!$plugin->render_markup_view)
        - - @php - $textareaId = 'code-' . uniqid(); - @endphp - {{ $markup_language === 'liquid' ? 'Liquid Code' : 'Blade Code' }} -