From 9367c603439565dcf68a9ecec02fb282c2b3ff4e Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sat, 25 Oct 2025 20:51:06 +0200 Subject: [PATCH 1/7] feat: add TRMNL recipe catalog --- .../views/livewire/plugins/index.blade.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index ab42b67..086e402 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -178,7 +178,10 @@ new class extends Component { Import Recipe Archive - Import from Catalog + Import from OSS Catalog + + + Import from TRMNL Catalog Seed Example Recipes @@ -280,6 +283,18 @@ new class extends Component { + +
+
+ Import from TRMNL Recipe Catalog + Alpha + +
+{{-- IMPLEMENT p--}} +{{-- --}} +
+
+
From d54a7f3ed297a2fd555d2b491f42e071b41cf596 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sat, 25 Oct 2025 20:57:27 +0200 Subject: [PATCH 2/7] feat: add TRMNL recipe catalog --- .../views/livewire/catalog/trmnl.blade.php | 194 ++++++++++++++++++ .../views/livewire/plugins/index.blade.php | 3 +- tests/Feature/Volt/CatalogTrmnlTest.php | 66 ++++++ 3 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 resources/views/livewire/catalog/trmnl.blade.php create mode 100644 tests/Feature/Volt/CatalogTrmnlTest.php diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php new file mode 100644 index 0000000..7d318a6 --- /dev/null +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -0,0 +1,194 @@ +loadNewest(); + } + + private function loadNewest(): void + { + $this->error = ''; + try { + $this->recipes = Cache::remember('trmnl_recipes_newest', 300, function () { + $response = Http::get('https://usetrmnl.com/recipes.json', [ + 'sort-by' => 'newest', + ]); + + if (!$response->successful()) { + throw new \RuntimeException('Failed to fetch TRMNL recipes'); + } + + $json = $response->json(); + $data = $json['data'] ?? []; + return $this->mapRecipes($data); + }); + } catch (\Throwable $e) { + Log::error('TRMNL catalog load error: ' . $e->getMessage()); + $this->recipes = []; + $this->error = 'Failed to load the TRMNL catalog. Please try again later.'; + } + } + + private function searchRecipes(string $term): void + { + $this->error = ''; + $this->isSearching = true; + try { + $cacheKey = 'trmnl_recipes_search_' . md5($term); + $this->recipes = Cache::remember($cacheKey, 300, function () use ($term) { + $response = Http::get('https://usetrmnl.com/recipes.json', [ + 'search' => $term, + 'sort-by' => 'newest', + ]); + + if (!$response->successful()) { + throw new \RuntimeException('Failed to search TRMNL recipes'); + } + + $json = $response->json(); + $data = $json['data'] ?? []; + return $this->mapRecipes($data); + }); + } catch (\Throwable $e) { + Log::error('TRMNL catalog search error: ' . $e->getMessage()); + $this->recipes = []; + $this->error = 'Search failed. Please try again later.'; + } finally { + $this->isSearching = false; + } + } + + public function updatedSearch(): void + { + $term = trim($this->search); + if ($term === '') { + $this->loadNewest(); + return; + } + + if (strlen($term) < 2) { + // Require at least 2 chars to avoid noisy calls + return; + } + + $this->searchRecipes($term); + } + + /** + * @param array> $items + * @return array> + */ + private function mapRecipes(array $items): array + { + return collect($items) + ->map(function (array $item) { + return [ + 'id' => $item['id'] ?? null, + 'name' => $item['name'] ?? 'Untitled', + 'icon_url' => $item['icon_url'] ?? null, + 'screenshot_url' => $item['screenshot_url'] ?? null, + 'author_bio' => is_array($item['author_bio'] ?? null) + ? ($item['author_bio']['description'] ?? null) + : null, + 'stats' => [ + 'installs' => data_get($item, 'stats.installs'), + 'forks' => data_get($item, 'stats.forks'), + ], + 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/' . $item['id']) : null, + ]; + }) + ->toArray(); + } +}; ?> + +
+
+
+ +
+ Newest +
+ + @if($error) + + @endif + + @if(empty($recipes)) +
+ + No recipes found + Try a different search term +
+ @else +
+ @foreach($recipes as $recipe) +
+
+ @php($thumb = $recipe['icon_url'] ?? $recipe['screenshot_url']) + @if($thumb) + {{ $recipe['name'] }} + @else +
+ +
+ @endif + +
+
+
+

{{ $recipe['name'] }}

+ @if(data_get($recipe, 'stats.installs')) +

Installs: {{ data_get($recipe, 'stats.installs') }} ยท Forks: {{ data_get($recipe, 'stats.forks') }}

+ @endif +
+
+ @if($recipe['detail_url']) + + + + @endif +
+
+ + @if($recipe['author_bio']) +

{{ $recipe['author_bio'] }}

+ @endif + +
+ + + Install + + + + @if($recipe['detail_url']) + + View on TRMNL + + @endif +
+
+
+
+ @endforeach +
+ @endif +
diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index 086e402..dfc2c91 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -290,8 +290,7 @@ new class extends Component { Alpha
-{{-- IMPLEMENT p--}} -{{-- --}} +
diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php new file mode 100644 index 0000000..ed708c7 --- /dev/null +++ b/tests/Feature/Volt/CatalogTrmnlTest.php @@ -0,0 +1,66 @@ + Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + ]); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->assertSee('Install') + ->assertSee('Installs: 10'); +}); + +it('searches TRMNL recipes when search term is provided', function () { + Http::fake([ + // First call (mount -> newest) + 'usetrmnl.com/recipes.json?*' => Http::sequence() + ->push([ + 'data' => [ + [ + 'id' => 1, + 'name' => 'Initial Recipe', + 'icon_url' => null, + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 1, 'forks' => 0], + ], + ], + ], 200) + // Second call (search) + ->push([ + 'data' => [ + [ + 'id' => 2, + 'name' => 'Weather Search Result', + 'icon_url' => null, + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 3, 'forks' => 1], + ], + ], + ], 200), + ]); + + Volt::test('catalog.trmnl') + ->assertSee('Initial Recipe') + ->set('search', 'weather') + ->assertSee('Weather Search Result') + ->assertSee('Install'); +}); From c6ee36470dbad7b3e0e35d40afc3d22a340fec96 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 27 Oct 2025 12:14:37 +0100 Subject: [PATCH 3/7] strip tags --- resources/views/livewire/catalog/trmnl.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 7d318a6..81cab20 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -99,7 +99,7 @@ new class extends Component { 'icon_url' => $item['icon_url'] ?? null, 'screenshot_url' => $item['screenshot_url'] ?? null, 'author_bio' => is_array($item['author_bio'] ?? null) - ? ($item['author_bio']['description'] ?? null) + ? strip_tags($item['author_bio']['description'] ?? null) : null, 'stats' => [ 'installs' => data_get($item, 'stats.installs'), From 76687c156da9bd45fb0e133d84d1b78caf0fd4e1 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 29 Oct 2025 10:47:27 +0100 Subject: [PATCH 4/7] feat: add installation function --- .../views/livewire/catalog/trmnl.blade.php | 56 ++++++++++--- tests/Feature/Volt/CatalogTrmnlTest.php | 79 +++++++++++++++++++ 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 81cab20..6f04ca4 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -4,12 +4,14 @@ use Livewire\Volt\Component; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Cache; +use App\Services\PluginImportService; +use Illuminate\Support\Facades\Auth; new class extends Component { public array $recipes = []; public string $search = ''; - public string $error = ''; public bool $isSearching = false; + public string $installingPlugin = ''; public function mount(): void { @@ -18,7 +20,6 @@ new class extends Component { private function loadNewest(): void { - $this->error = ''; try { $this->recipes = Cache::remember('trmnl_recipes_newest', 300, function () { $response = Http::get('https://usetrmnl.com/recipes.json', [ @@ -36,13 +37,11 @@ new class extends Component { } catch (\Throwable $e) { Log::error('TRMNL catalog load error: ' . $e->getMessage()); $this->recipes = []; - $this->error = 'Failed to load the TRMNL catalog. Please try again later.'; } } private function searchRecipes(string $term): void { - $this->error = ''; $this->isSearching = true; try { $cacheKey = 'trmnl_recipes_search_' . md5($term); @@ -63,7 +62,6 @@ new class extends Component { } catch (\Throwable $e) { Log::error('TRMNL catalog search error: ' . $e->getMessage()); $this->recipes = []; - $this->error = 'Search failed. Please try again later.'; } finally { $this->isSearching = false; } @@ -85,6 +83,27 @@ new class extends Component { $this->searchRecipes($term); } + public function installPlugin(string $recipeId, PluginImportService $pluginImportService): void + { + abort_unless(auth()->user() !== null, 403); + + $this->installingPlugin = $recipeId; + + try { + $zipUrl = "https://usetrmnl.com/api/plugin_settings/{$recipeId}/archive"; + $plugin = $pluginImportService->importFromUrl($zipUrl, auth()->user()); + + $this->dispatch('plugin-installed'); + Flux::modal('import-from-trmnl-catalog')->close(); + + } catch (\Exception $e) { + Log::error('Plugin installation failed: ' . $e->getMessage()); + $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } finally { + $this->installingPlugin = ''; + } + } + /** * @param array> $items * @return array> @@ -124,9 +143,9 @@ new class extends Component { Newest - @if($error) - - @endif + @error('installation') + + @enderror @if(empty($recipes))
@@ -170,11 +189,22 @@ new class extends Component { @endif
- - - Install - - + @if($recipe['id']) + @if($installingPlugin === $recipe['id']) + + + + @else + + Install + + @endif + @endif @if($recipe['detail_url']) assertSee('Weather Search Result') ->assertSee('Install'); }); + +it('installs plugin successfully when user is authenticated', function () { + $user = User::factory()->create(); + + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('fake zip content', 200), + ]); + + $this->actingAs($user); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('installPlugin', '123') + ->assertSee('Error installing plugin'); // This will fail because we don't have a real zip file +}); + +it('shows error when user is not authenticated', function () { + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + ]); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('installPlugin', '123') + ->assertStatus(403); // This will return 403 because user is not authenticated +}); + +it('shows error when plugin installation fails', function () { + $user = User::factory()->create(); + + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('invalid zip content', 200), + ]); + + $this->actingAs($user); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('installPlugin', '123') + ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid +}); From 17a050471265207f2e7a59aeb189cd12e0e90cee Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 4 Nov 2025 18:35:08 +0100 Subject: [PATCH 5/7] feat: add trmnl-liquid renderer option --- app/Models/Plugin.php | 156 ++++++++++++------ config/services.php | 2 + ...dd_preferred_renderer_to_plugins_table.php | 28 ++++ 3 files changed, 135 insertions(+), 51 deletions(-) create mode 100644 database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 33a29d5..40569f8 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -19,6 +19,7 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Str; use Keepsuit\LaravelLiquid\LaravelLiquidExtension; use Keepsuit\Liquid\Exceptions\LiquidException; @@ -40,6 +41,7 @@ class Plugin extends Model 'configuration_template' => 'json', 'no_bleed' => 'boolean', 'dark_mode' => 'boolean', + 'preferred_renderer' => 'string', ]; protected static function boot() @@ -363,6 +365,50 @@ class Plugin extends Model return $liquidTemplate->render($context); } + /** + * Render template using external Ruby liquid renderer + * + * @param string $template The liquid template string + * @param array $context The render context data + * @return string The rendered HTML + * + * @throws LiquidException + */ + private function renderWithExternalLiquidRenderer(string $template, array $context): string + { + $liquidPath = config('services.trmnl.liquid_path'); + + if (empty($liquidPath)) { + throw new LiquidException('External liquid renderer path is not configured'); + } + + // HTML encode the template + $encodedTemplate = htmlspecialchars($template, ENT_QUOTES, 'UTF-8'); + + // Encode context as JSON + $jsonContext = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + if ($jsonContext === false) { + throw new LiquidException('Failed to encode render context as JSON: '.json_last_error_msg()); + } + + // Execute the external renderer + $process = Process::run([ + $liquidPath, + '--template', + $encodedTemplate, + '--context', + $jsonContext, + ]); + + if (! $process->successful()) { + $errorOutput = $process->errorOutput() ?: $process->output(); + throw new Exception('External liquid renderer failed: '.$errorOutput); + } + + return $process->output(); + } + /** * Render the plugin's markup * @@ -374,59 +420,67 @@ class Plugin extends Model $renderedContent = ''; if ($this->markup_language === 'liquid') { - // Create a custom environment with inline templates support - $inlineFileSystem = new InlineTemplatesFileSystem(); - $environment = new \Keepsuit\Liquid\Environment( - fileSystem: $inlineFileSystem, - extensions: [new StandardExtension(), new LaravelLiquidExtension()] - ); - - // Register all custom filters - $environment->filterRegistry->register(Data::class); - $environment->filterRegistry->register(Date::class); - $environment->filterRegistry->register(Localization::class); - $environment->filterRegistry->register(Numbers::class); - $environment->filterRegistry->register(StringMarkup::class); - $environment->filterRegistry->register(Uniqueness::class); - - // Register the template tag for inline templates - $environment->tagRegistry->register(TemplateTag::class); - - // Apply Liquid replacements (including 'with' syntax conversion) - $processedMarkup = $this->applyLiquidReplacements($this->render_markup); - - $template = $environment->parseString($processedMarkup); - $context = $environment->newRenderContext( - data: [ - 'size' => $size, - 'data' => $this->data_payload, - 'config' => $this->configuration ?? [], - ...(is_array($this->data_payload) ? $this->data_payload : []), - 'trmnl' => [ - 'system' => [ - 'timestamp_utc' => now()->utc()->timestamp, - ], - 'user' => [ - 'utc_offset' => '0', - 'name' => $this->user->name ?? 'Unknown User', - 'locale' => 'en', - 'time_zone_iana' => config('app.timezone'), - ], - 'plugin_settings' => [ - 'instance_name' => $this->name, - 'strategy' => $this->data_strategy, - 'dark_mode' => $this->dark_mode ? 'yes' : 'no', - 'no_screen_padding' => $this->no_bleed ? 'yes' : 'no', - 'polling_headers' => $this->polling_header, - 'polling_url' => $this->polling_url, - 'custom_fields_values' => [ - ...(is_array($this->configuration) ? $this->configuration : []), - ], + // Build render context + $context = [ + 'size' => $size, + 'data' => $this->data_payload, + 'config' => $this->configuration ?? [], + ...(is_array($this->data_payload) ? $this->data_payload : []), + 'trmnl' => [ + 'system' => [ + 'timestamp_utc' => now()->utc()->timestamp, + ], + 'user' => [ + 'utc_offset' => '0', + 'name' => $this->user->name ?? 'Unknown User', + 'locale' => 'en', + 'time_zone_iana' => config('app.timezone'), + ], + 'plugin_settings' => [ + 'instance_name' => $this->name, + 'strategy' => $this->data_strategy, + 'dark_mode' => $this->dark_mode ? 'yes' : 'no', + 'no_screen_padding' => $this->no_bleed ? 'yes' : 'no', + 'polling_headers' => $this->polling_header, + 'polling_url' => $this->polling_url, + 'custom_fields_values' => [ + ...(is_array($this->configuration) ? $this->configuration : []), ], ], - ] - ); - $renderedContent = $template->render($context); + ], + ]; + + // 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); + } else { + // Use PHP keepsuit/liquid renderer + // Create a custom environment with inline templates support + $inlineFileSystem = new InlineTemplatesFileSystem(); + $environment = new \Keepsuit\Liquid\Environment( + fileSystem: $inlineFileSystem, + extensions: [new StandardExtension(), new LaravelLiquidExtension()] + ); + + // Register all custom filters + $environment->filterRegistry->register(Data::class); + $environment->filterRegistry->register(Date::class); + $environment->filterRegistry->register(Localization::class); + $environment->filterRegistry->register(Numbers::class); + $environment->filterRegistry->register(StringMarkup::class); + $environment->filterRegistry->register(Uniqueness::class); + + // Register the template tag for inline templates + $environment->tagRegistry->register(TemplateTag::class); + + // Apply Liquid replacements (including 'with' syntax conversion) + $processedMarkup = $this->applyLiquidReplacements($this->render_markup); + + $template = $environment->parseString($processedMarkup); + $liquidContext = $environment->newRenderContext(data: $context); + $renderedContent = $template->render($liquidContext); + } } else { $renderedContent = Blade::render($this->render_markup, [ 'size' => $size, diff --git a/config/services.php b/config/services.php index 5cb8a74..d97255a 100644 --- a/config/services.php +++ b/config/services.php @@ -41,6 +41,8 @@ return [ 'proxy_refresh_cron' => env('TRMNL_PROXY_REFRESH_CRON'), 'override_orig_icon' => env('TRMNL_OVERRIDE_ORIG_ICON', false), 'image_url_timeout' => env('TRMNL_IMAGE_URL_TIMEOUT', 30), // 30 seconds; increase on low-powered devices + 'liquid_enabled' => env('TRMNL_LIQUID_ENABLED', false), + 'liquid_path' => env('TRMNL_LIQUID_PATH', '/usr/local/bin/trmnl-liquid-cli'), ], 'webhook' => [ diff --git a/database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php b/database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php new file mode 100644 index 0000000..a998420 --- /dev/null +++ b/database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php @@ -0,0 +1,28 @@ +string('preferred_renderer')->nullable()->after('markup_language'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('preferred_renderer'); + }); + } +}; From b8c847196a1f5c70ecf4ecb110ba53af74b363fb Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 4 Nov 2025 22:53:47 +0100 Subject: [PATCH 6/7] feat: set preferred_renderer when installing from catalog --- Dockerfile | 4 ++++ app/Services/PluginImportService.php | 3 ++- resources/views/livewire/catalog/trmnl.blade.php | 11 ++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 57a919f..4e50553 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,9 @@ ENV APP_VERSION=${APP_VERSION} ENV AUTORUN_ENABLED="true" +# Mark trmnl-liquid-cli as installed +ENV TRMNL_LIQUID_ENABLED=1 + # Switch to the root user so we can do root things USER root @@ -49,5 +52,6 @@ FROM base AS production COPY --chown=www-data:www-data --from=assets /app/public/build /var/www/html/public/build COPY --chown=www-data:www-data --from=assets /app/node_modules /var/www/html/node_modules +COPY --chown=www-data:www-data --from=bnussbau/trmnl-liquid-cli:latest /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ # Drop back to the www-data user USER www-data diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index a9d93b3..7c77767 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -143,7 +143,7 @@ class PluginImportService * * @throws Exception If the ZIP file is invalid or required files are missing */ - public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null): Plugin + public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null): Plugin { // Download the ZIP file $response = Http::timeout(60)->get($zipUrl); @@ -232,6 +232,7 @@ class PluginImportService 'render_markup' => $fullLiquid, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), + 'preferred_renderer' => $preferredRenderer, ]); if (! $plugin_updated) { diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 6f04ca4..4d1a60f 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -91,11 +91,16 @@ new class extends Component { try { $zipUrl = "https://usetrmnl.com/api/plugin_settings/{$recipeId}/archive"; - $plugin = $pluginImportService->importFromUrl($zipUrl, auth()->user()); - + + $plugin = $pluginImportService->importFromUrl( + $zipUrl, + auth()->user(), + preferredRenderer: config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null + ); + $this->dispatch('plugin-installed'); Flux::modal('import-from-trmnl-catalog')->close(); - + } catch (\Exception $e) { Log::error('Plugin installation failed: ' . $e->getMessage()); $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); From 799e66675cb4d7aea69f0902ec03872d6ed2f564 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 5 Nov 2025 13:36:00 +0100 Subject: [PATCH 7/7] fix: require trmnl-liquid to install recipes from TRMNL catalog --- resources/views/livewire/plugins/index.blade.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index dfc2c91..e5f19ec 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -180,9 +180,11 @@ new class extends Component { Import from OSS Catalog - - Import from TRMNL Catalog - + @if(config('services.trmnl.liquid_enabled')) + + Import from TRMNL Catalog + + @endif Seed Example Recipes