diff --git a/.gitignore b/.gitignore
index 0eb46d3..02f3d78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,11 +29,3 @@ yarn-error.log
/.junie/guidelines.md
/CLAUDE.md
/.mcp.json
-/.ai
-.DS_Store
-/boost.json
-/.gemini
-/GEMINI.md
-/.claude
-/AGENTS.md
-/opencode.json
diff --git a/app/Jobs/FetchDeviceModelsJob.php b/app/Jobs/FetchDeviceModelsJob.php
index 475c5c7..cb24d98 100644
--- a/app/Jobs/FetchDeviceModelsJob.php
+++ b/app/Jobs/FetchDeviceModelsJob.php
@@ -199,7 +199,6 @@ final class FetchDeviceModelsJob implements ShouldQueue
'offset_x' => $modelData['offset_x'] ?? 0,
'offset_y' => $modelData['offset_y'] ?? 0,
'published_at' => $modelData['published_at'] ?? null,
- 'kind' => $modelData['kind'] ?? null,
'source' => 'api',
];
diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index 68f8e7e..c4b45c8 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -11,6 +11,7 @@ use App\Liquid\Filters\StandardFilters;
use App\Liquid\Filters\StringMarkup;
use App\Liquid\Filters\Uniqueness;
use App\Liquid\Tags\TemplateTag;
+use App\Services\ImageGenerationService;
use App\Services\Plugin\Parsers\ResponseParserRegistry;
use App\Services\PluginImportService;
use Carbon\Carbon;
@@ -24,7 +25,6 @@ use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
-use InvalidArgumentException;
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
use Keepsuit\Liquid\Exceptions\LiquidException;
use Keepsuit\Liquid\Extensions\StandardExtension;
@@ -46,7 +46,6 @@ class Plugin extends Model
'dark_mode' => 'boolean',
'preferred_renderer' => 'string',
'plugin_type' => 'string',
- 'alias' => 'boolean',
];
protected static function boot()
@@ -154,67 +153,105 @@ class Plugin extends Model
public function updateDataPayload(): void
{
- if ($this->data_strategy !== 'polling' || ! $this->polling_url) {
- return;
- }
- $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
+ if ($this->data_strategy === 'polling' && $this->polling_url) {
- // resolve headers
- if ($this->polling_header) {
- $resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
- $headerLines = explode("\n", mb_trim($resolvedHeader));
- foreach ($headerLines as $line) {
- $parts = explode(':', $line, 2);
- if (count($parts) === 2) {
- $headers[mb_trim($parts[0])] = mb_trim($parts[1]);
+ $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
+
+ if ($this->polling_header) {
+ // Resolve Liquid variables in the polling header
+ $resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
+ $headerLines = explode("\n", mb_trim($resolvedHeader));
+ foreach ($headerLines as $line) {
+ $parts = explode(':', $line, 2);
+ if (count($parts) === 2) {
+ $headers[mb_trim($parts[0])] = mb_trim($parts[1]);
+ }
}
}
- }
- // resolve and clean URLs
- $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
- $urls = array_values(array_filter( // array_values ensures 0, 1, 2...
- array_map('trim', explode("\n", $resolvedPollingUrls)),
- fn ($url): bool => filled($url)
- ));
+ // Resolve Liquid variables in the entire polling_url field first, then split by newline
+ $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
+ $urls = array_filter(
+ array_map('trim', explode("\n", $resolvedPollingUrls)),
+ fn ($url): bool => ! empty($url)
+ );
- $combinedResponse = [];
+ // If only one URL, use the original logic without nesting
+ if (count($urls) === 1) {
+ $url = reset($urls);
+ $httpRequest = Http::withHeaders($headers);
- // Loop through all URLs (Handles 1 or many)
- foreach ($urls as $index => $url) {
- $httpRequest = Http::withHeaders($headers);
-
- if ($this->polling_verb === 'post' && $this->polling_body) {
- $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
- $httpRequest = $httpRequest->withBody($resolvedBody);
- }
-
- try {
- $httpResponse = ($this->polling_verb === 'post')
- ? $httpRequest->post($url)
- : $httpRequest->get($url);
-
- $response = $this->parseResponse($httpResponse);
-
- // Nest if it's a sequential array
- if (array_keys($response) === range(0, count($response) - 1)) {
- $combinedResponse["IDX_{$index}"] = ['data' => $response];
- } else {
- $combinedResponse["IDX_{$index}"] = $response;
+ if ($this->polling_verb === 'post' && $this->polling_body) {
+ // Resolve Liquid variables in the polling body
+ $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
+ $httpRequest = $httpRequest->withBody($resolvedBody);
}
- } catch (Exception $e) {
- Log::warning("Failed to fetch data from URL {$url}: ".$e->getMessage());
- $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
+
+ // URL is already resolved, use it directly
+ $resolvedUrl = $url;
+
+ try {
+ // Make the request based on the verb
+ $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
+
+ $response = $this->parseResponse($httpResponse);
+
+ $this->update([
+ 'data_payload' => $response,
+ 'data_payload_updated_at' => now(),
+ ]);
+ } catch (Exception $e) {
+ Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
+ $this->update([
+ 'data_payload' => ['error' => 'Failed to fetch data'],
+ 'data_payload_updated_at' => now(),
+ ]);
+ }
+
+ return;
}
+
+ // Multiple URLs - use nested response logic
+ $combinedResponse = [];
+
+ foreach ($urls as $index => $url) {
+ $httpRequest = Http::withHeaders($headers);
+
+ if ($this->polling_verb === 'post' && $this->polling_body) {
+ // Resolve Liquid variables in the polling body
+ $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
+ $httpRequest = $httpRequest->withBody($resolvedBody);
+ }
+
+ // URL is already resolved, use it directly
+ $resolvedUrl = $url;
+
+ try {
+ // Make the request based on the verb
+ $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
+
+ $response = $this->parseResponse($httpResponse);
+
+ // Check if response is an array at root level
+ if (array_keys($response) === range(0, count($response) - 1)) {
+ // Response is a sequential array, nest under .data
+ $combinedResponse["IDX_{$index}"] = ['data' => $response];
+ } else {
+ // Response is an object or associative array, keep as is
+ $combinedResponse["IDX_{$index}"] = $response;
+ }
+ } catch (Exception $e) {
+ // Log error and continue with other URLs
+ Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
+ $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
+ }
+ }
+
+ $this->update([
+ 'data_payload' => $combinedResponse,
+ 'data_payload_updated_at' => now(),
+ ]);
}
-
- // unwrap IDX_0 if only one URL
- $finalPayload = (count($urls) === 1) ? reset($combinedResponse) : $combinedResponse;
-
- $this->update([
- 'data_payload' => $finalPayload,
- 'data_payload_updated_at' => now(),
- ]);
}
private function parseResponse(Response $httpResponse): array
@@ -418,7 +455,7 @@ class Plugin extends Model
public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string
{
if ($this->plugin_type !== 'recipe') {
- throw new InvalidArgumentException('Render method is only applicable for recipe plugins.');
+ throw new \InvalidArgumentException('Render method is only applicable for recipe plugins.');
}
if ($this->render_markup) {
@@ -528,30 +565,17 @@ class Plugin extends Model
if ($this->render_markup_view) {
if ($standalone) {
- $renderedView = view($this->render_markup_view, [
- 'size' => $size,
- 'data' => $this->data_payload,
- 'config' => $this->configuration ?? [],
- ])->render();
-
- if ($size === 'full') {
- return view('trmnl-layouts.single', [
- 'colorDepth' => $device?->colorDepth(),
- 'deviceVariant' => $device?->deviceVariant() ?? 'og',
- 'noBleed' => $this->no_bleed,
- 'darkMode' => $this->dark_mode,
- 'scaleLevel' => $device?->scaleLevel(),
- 'slot' => $renderedView,
- ])->render();
- }
-
- return view('trmnl-layouts.mashup', [
- 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size),
+ return view('trmnl-layouts.single', [
'colorDepth' => $device?->colorDepth(),
'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'noBleed' => $this->no_bleed,
'darkMode' => $this->dark_mode,
'scaleLevel' => $device?->scaleLevel(),
- 'slot' => $renderedView,
+ 'slot' => view($this->render_markup_view, [
+ 'size' => $size,
+ 'data' => $this->data_payload,
+ 'config' => $this->configuration ?? [],
+ ])->render(),
])->render();
}
@@ -582,61 +606,4 @@ class Plugin extends Model
default => '1Tx1B',
};
}
-
- /**
- * Duplicate the plugin, copying all attributes and handling render_markup_view
- *
- * @param int|null $userId Optional user ID for the duplicate. If not provided, uses the original plugin's user_id.
- * @return Plugin The newly created duplicate plugin
- */
- public function duplicate(?int $userId = null): self
- {
- // Get all attributes except id and uuid
- // Use toArray() to get cast values (respects JSON casts)
- $attributes = $this->toArray();
- unset($attributes['id'], $attributes['uuid']);
-
- // Handle render_markup_view - copy file content to render_markup
- if ($this->render_markup_view) {
- try {
- $basePath = resource_path('views/'.str_replace('.', '/', $this->render_markup_view));
- $paths = [
- $basePath.'.blade.php',
- $basePath.'.liquid',
- ];
-
- $fileContent = null;
- $markupLanguage = null;
- foreach ($paths as $path) {
- if (file_exists($path)) {
- $fileContent = file_get_contents($path);
- // Determine markup language based on file extension
- $markupLanguage = str_ends_with($path, '.liquid') ? 'liquid' : 'blade';
- break;
- }
- }
-
- if ($fileContent !== null) {
- $attributes['render_markup'] = $fileContent;
- $attributes['markup_language'] = $markupLanguage;
- $attributes['render_markup_view'] = null;
- } else {
- // File doesn't exist, remove the view reference
- $attributes['render_markup_view'] = null;
- }
- } catch (Exception $e) {
- // If file reading fails, remove the view reference
- $attributes['render_markup_view'] = null;
- }
- }
-
- // Append " (Copy)" to the name
- $attributes['name'] = $this->name.' (Copy)';
-
- // Set user_id - use provided userId or fall back to original plugin's user_id
- $attributes['user_id'] = $userId ?? $this->user_id;
-
- // Create and return the new plugin
- return self::create($attributes);
- }
}
diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php
index 405ea3f..b8269a3 100644
--- a/app/Services/ImageGenerationService.php
+++ b/app/Services/ImageGenerationService.php
@@ -26,44 +26,11 @@ class ImageGenerationService
public static function generateImage(string $markup, $deviceId): string
{
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId);
- $uuid = self::generateImageFromModel(
- markup: $markup,
- deviceModel: $device->deviceModel,
- user: $device->user,
- palette: $device->palette ?? $device->deviceModel?->palette,
- device: $device
- );
-
- $device->update(['current_screen_image' => $uuid]);
- Log::info("Device $device->id: updated with new image: $uuid");
-
- return $uuid;
- }
-
- /**
- * Generate an image from markup using a DeviceModel
- *
- * @param string $markup The HTML markup to render
- * @param DeviceModel|null $deviceModel The device model to use for image generation
- * @param \App\Models\User|null $user Optional user for timezone settings
- * @param \App\Models\DevicePalette|null $palette Optional palette, falls back to device model's palette
- * @param Device|null $device Optional device for legacy devices without DeviceModel
- * @return string The UUID of the generated image
- */
- public static function generateImageFromModel(
- string $markup,
- ?DeviceModel $deviceModel = null,
- ?\App\Models\User $user = null,
- ?\App\Models\DevicePalette $palette = null,
- ?Device $device = null
- ): string {
$uuid = Uuid::uuid4()->toString();
try {
- // Get image generation settings from DeviceModel or Device (for legacy devices)
- $imageSettings = $deviceModel
- ? self::getImageSettingsFromModel($deviceModel)
- : ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null));
+ // Get image generation settings from DeviceModel if available, otherwise use device settings
+ $imageSettings = self::getImageSettings($device);
$fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png';
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
@@ -78,7 +45,7 @@ class ImageGenerationService
$browserStage->html($markup);
// Set timezone from user or fall back to app timezone
- $timezone = $user?->timezone ?? config('app.timezone');
+ $timezone = $device->user->timezone ?? config('app.timezone');
$browserStage->timezone($timezone);
if (config('app.puppeteer_window_size_strategy') === 'v2') {
@@ -98,12 +65,12 @@ class ImageGenerationService
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
}
- // Get palette from parameter or fallback to device model's default palette
+ // Get palette from device or fallback to device model's default palette
+ $palette = $device->palette ?? $device->deviceModel?->palette;
$colorPalette = null;
+
if ($palette && $palette->colors) {
$colorPalette = $palette->colors;
- } elseif ($deviceModel?->palette && $deviceModel->palette->colors) {
- $colorPalette = $deviceModel->palette->colors;
}
$imageStage = new ImageStage();
@@ -140,7 +107,8 @@ class ImageGenerationService
throw new RuntimeException('Image file is empty: '.$outputPath);
}
- Log::info("Generated image: $uuid");
+ $device->update(['current_screen_image' => $uuid]);
+ Log::info("Device $device->id: updated with new image: $uuid");
return $uuid;
@@ -157,7 +125,22 @@ class ImageGenerationService
{
// If device has a DeviceModel, use its settings
if ($device->deviceModel) {
- return self::getImageSettingsFromModel($device->deviceModel);
+ /** @var DeviceModel $model */
+ $model = $device->deviceModel;
+
+ return [
+ 'width' => $model->width,
+ 'height' => $model->height,
+ 'colors' => $model->colors,
+ 'bit_depth' => $model->bit_depth,
+ 'scale_factor' => $model->scale_factor,
+ 'rotation' => $model->rotation,
+ 'mime_type' => $model->mime_type,
+ 'offset_x' => $model->offset_x,
+ 'offset_y' => $model->offset_y,
+ 'image_format' => self::determineImageFormatFromModel($model),
+ 'use_model_settings' => true,
+ ];
}
// Fallback to device settings
@@ -181,43 +164,6 @@ class ImageGenerationService
];
}
- /**
- * Get image generation settings from a DeviceModel
- */
- private static function getImageSettingsFromModel(?DeviceModel $deviceModel): array
- {
- if ($deviceModel) {
- return [
- 'width' => $deviceModel->width,
- 'height' => $deviceModel->height,
- 'colors' => $deviceModel->colors,
- 'bit_depth' => $deviceModel->bit_depth,
- 'scale_factor' => $deviceModel->scale_factor,
- 'rotation' => $deviceModel->rotation,
- 'mime_type' => $deviceModel->mime_type,
- 'offset_x' => $deviceModel->offset_x,
- 'offset_y' => $deviceModel->offset_y,
- 'image_format' => self::determineImageFormatFromModel($deviceModel),
- 'use_model_settings' => true,
- ];
- }
-
- // Default settings if no device model provided
- return [
- 'width' => 800,
- 'height' => 480,
- 'colors' => 2,
- 'bit_depth' => 1,
- 'scale_factor' => 1.0,
- 'rotation' => 0,
- 'mime_type' => 'image/png',
- 'offset_x' => 0,
- 'offset_y' => 0,
- 'image_format' => ImageFormat::AUTO->value,
- 'use_model_settings' => false,
- ];
- }
-
/**
* Determine the appropriate ImageFormat based on DeviceModel settings
*/
diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php
index 49dce99..9207e3e 100644
--- a/app/Services/PluginImportService.php
+++ b/app/Services/PluginImportService.php
@@ -17,34 +17,6 @@ use ZipArchive;
class PluginImportService
{
- /**
- * Validate YAML settings
- *
- * @param array $settings The parsed YAML settings
- *
- * @throws Exception
- */
- private function validateYAML(array $settings): void
- {
- if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) {
- return;
- }
-
- foreach ($settings['custom_fields'] as $field) {
- if (isset($field['field_type']) && $field['field_type'] === 'multi_string') {
-
- if (isset($field['default']) && str_contains($field['default'], ',')) {
- throw new Exception("Validation Error: The default value for multistring fields like `{$field['keyname']}` cannot contain commas.");
- }
-
- if (isset($field['placeholder']) && str_contains($field['placeholder'], ',')) {
- throw new Exception("Validation Error: The placeholder value for multistring fields like `{$field['keyname']}` cannot contain commas.");
- }
-
- }
- }
- }
-
/**
* Import a plugin from a ZIP file
*
@@ -75,55 +47,32 @@ class PluginImportService
$zip->extractTo($tempDir);
$zip->close();
- // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php)
+ // Find the required files (settings.yml and full.liquid/full.blade.php)
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
// Validate that we found the required files
- if (! $filePaths['settingsYamlPath']) {
- throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
- }
-
- // Validate that we have at least one template file
- if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) {
- throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
+ if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
+ throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php
}
// Parse settings.yml
$settingsYaml = File::get($filePaths['settingsYamlPath']);
$settings = Yaml::parse($settingsYaml);
- $this->validateYAML($settings);
- // Determine which template file to use and read its content
- $templatePath = null;
+ // Read full.liquid content
+ $fullLiquid = File::get($filePaths['fullLiquidPath']);
+
+ // Prepend shared.liquid content if available
+ if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
+ $fullLiquid = $sharedLiquid."\n".$fullLiquid;
+ }
+
+ // Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
-
- if ($filePaths['fullLiquidPath']) {
- $templatePath = $filePaths['fullLiquidPath'];
- $fullLiquid = File::get($templatePath);
-
- // Prepend shared.liquid or shared.blade.php content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
- $sharedBlade = File::get($filePaths['sharedBladePath']);
- $fullLiquid = $sharedBlade."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
- if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
- $markupLanguage = 'liquid';
- $fullLiquid = '
'."\n".$fullLiquid."\n".'
';
- }
- } elseif ($filePaths['sharedLiquidPath']) {
- $templatePath = $filePaths['sharedLiquidPath'];
- $fullLiquid = File::get($templatePath);
+ if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
$fullLiquid = ''."\n".$fullLiquid."\n".'
';
- } elseif ($filePaths['sharedBladePath']) {
- $templatePath = $filePaths['sharedBladePath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'blade';
}
// Ensure custom_fields is properly formatted
@@ -195,12 +144,11 @@ class PluginImportService
* @param string|null $zipEntryPath Optional path to specific plugin in monorepo
* @param string|null $preferredRenderer Optional preferred renderer (e.g., 'trmnl-liquid')
* @param string|null $iconUrl Optional icon URL to set on the plugin
- * @param bool $allowDuplicate If true, generate a new UUID for trmnlp_id if a plugin with the same trmnlp_id already exists
* @return Plugin The created plugin instance
*
* @throws Exception If the ZIP file is invalid or required files are missing
*/
- public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null, bool $allowDuplicate = false): Plugin
+ public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null): Plugin
{
// Download the ZIP file
$response = Http::timeout(60)->get($zipUrl);
@@ -228,55 +176,32 @@ class PluginImportService
$zip->extractTo($tempDir);
$zip->close();
- // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php)
+ // Find the required files (settings.yml and full.liquid/full.blade.php)
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
// Validate that we found the required files
- if (! $filePaths['settingsYamlPath']) {
- throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
- }
-
- // Validate that we have at least one template file
- if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) {
- throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
+ if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
+ throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.');
}
// Parse settings.yml
$settingsYaml = File::get($filePaths['settingsYamlPath']);
$settings = Yaml::parse($settingsYaml);
- $this->validateYAML($settings);
- // Determine which template file to use and read its content
- $templatePath = null;
+ // Read full.liquid content
+ $fullLiquid = File::get($filePaths['fullLiquidPath']);
+
+ // Prepend shared.liquid content if available
+ if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
+ $fullLiquid = $sharedLiquid."\n".$fullLiquid;
+ }
+
+ // Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
-
- if ($filePaths['fullLiquidPath']) {
- $templatePath = $filePaths['fullLiquidPath'];
- $fullLiquid = File::get($templatePath);
-
- // Prepend shared.liquid or shared.blade.php content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
- $sharedBlade = File::get($filePaths['sharedBladePath']);
- $fullLiquid = $sharedBlade."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
- if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
- $markupLanguage = 'liquid';
- $fullLiquid = ''."\n".$fullLiquid."\n".'
';
- }
- } elseif ($filePaths['sharedLiquidPath']) {
- $templatePath = $filePaths['sharedLiquidPath'];
- $fullLiquid = File::get($templatePath);
+ if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
$fullLiquid = ''."\n".$fullLiquid."\n".'
';
- } elseif ($filePaths['sharedBladePath']) {
- $templatePath = $filePaths['sharedBladePath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'blade';
}
// Ensure custom_fields is properly formatted
@@ -292,26 +217,17 @@ class PluginImportService
'custom_fields' => $settings['custom_fields'],
];
- // Determine the trmnlp_id to use
- $trmnlpId = $settings['id'] ?? Uuid::v7();
-
- // If allowDuplicate is true and a plugin with this trmnlp_id already exists, generate a new UUID
- if ($allowDuplicate && isset($settings['id']) && Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists()) {
- $trmnlpId = Uuid::v7();
- }
-
- $plugin_updated = ! $allowDuplicate && isset($settings['id'])
+ $plugin_updated = isset($settings['id'])
&& Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists();
-
// Create a new plugin
$plugin = Plugin::updateOrCreate(
[
- 'user_id' => $user->id, 'trmnlp_id' => $trmnlpId,
+ 'user_id' => $user->id, 'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
],
[
'user_id' => $user->id,
'name' => $settings['name'] ?? 'Imported Plugin',
- 'trmnlp_id' => $trmnlpId,
+ 'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
'data_stale_minutes' => $settings['refresh_interval'] ?? 15,
'data_strategy' => $settings['strategy'] ?? 'static',
'polling_url' => $settings['polling_url'] ?? null,
@@ -356,7 +272,6 @@ class PluginImportService
$settingsYamlPath = null;
$fullLiquidPath = null;
$sharedLiquidPath = null;
- $sharedBladePath = null;
// If zipEntryPath is specified, look for files in that specific directory first
if ($zipEntryPath) {
@@ -374,8 +289,6 @@ class PluginImportService
if (File::exists($targetDir.'/shared.liquid')) {
$sharedLiquidPath = $targetDir.'/shared.liquid';
- } elseif (File::exists($targetDir.'/shared.blade.php')) {
- $sharedBladePath = $targetDir.'/shared.blade.php';
}
}
@@ -391,18 +304,15 @@ class PluginImportService
if (File::exists($targetDir.'/src/shared.liquid')) {
$sharedLiquidPath = $targetDir.'/src/shared.liquid';
- } elseif (File::exists($targetDir.'/src/shared.blade.php')) {
- $sharedBladePath = $targetDir.'/src/shared.blade.php';
}
}
// If we found the required files in the target directory, return them
- if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
+ if ($settingsYamlPath && $fullLiquidPath) {
return [
'settingsYamlPath' => $settingsYamlPath,
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
- 'sharedBladePath' => $sharedBladePath,
];
}
}
@@ -419,11 +329,9 @@ class PluginImportService
$fullLiquidPath = $tempDir.'/src/full.blade.php';
}
- // Check for shared.liquid or shared.blade.php in the same directory
+ // Check for shared.liquid in the same directory
if (File::exists($tempDir.'/src/shared.liquid')) {
$sharedLiquidPath = $tempDir.'/src/shared.liquid';
- } elseif (File::exists($tempDir.'/src/shared.blade.php')) {
- $sharedBladePath = $tempDir.'/src/shared.blade.php';
}
} else {
// Search for the files in the extracted directory structure
@@ -440,24 +348,20 @@ class PluginImportService
$fullLiquidPath = $filepath;
} elseif ($filename === 'shared.liquid') {
$sharedLiquidPath = $filepath;
- } elseif ($filename === 'shared.blade.php') {
- $sharedBladePath = $filepath;
}
}
- // Check if shared.liquid or shared.blade.php exists in the same directory as full.liquid
- if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath && ! $sharedBladePath) {
+ // Check if shared.liquid exists in the same directory as full.liquid
+ if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath) {
$fullLiquidDir = dirname((string) $fullLiquidPath);
if (File::exists($fullLiquidDir.'/shared.liquid')) {
$sharedLiquidPath = $fullLiquidDir.'/shared.liquid';
- } elseif (File::exists($fullLiquidDir.'/shared.blade.php')) {
- $sharedBladePath = $fullLiquidDir.'/shared.blade.php';
}
}
// If we found the files but they're not in the src folder,
// check if they're in the root of the ZIP or in a subfolder
- if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
+ if ($settingsYamlPath && $fullLiquidPath) {
// If the files are in the root of the ZIP, create a src folder and move them there
$srcDir = dirname((string) $settingsYamlPath);
@@ -468,25 +372,17 @@ class PluginImportService
// Copy the files to the src directory
File::copy($settingsYamlPath, $newSrcDir.'/settings.yml');
+ File::copy($fullLiquidPath, $newSrcDir.'/full.liquid');
- // Copy full.liquid or full.blade.php if it exists
- if ($fullLiquidPath) {
- $extension = pathinfo((string) $fullLiquidPath, PATHINFO_EXTENSION);
- File::copy($fullLiquidPath, $newSrcDir.'/full.'.$extension);
- $fullLiquidPath = $newSrcDir.'/full.'.$extension;
- }
-
- // Copy shared.liquid or shared.blade.php if it exists
+ // Copy shared.liquid if it exists
if ($sharedLiquidPath) {
File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid');
$sharedLiquidPath = $newSrcDir.'/shared.liquid';
- } elseif ($sharedBladePath) {
- File::copy($sharedBladePath, $newSrcDir.'/shared.blade.php');
- $sharedBladePath = $newSrcDir.'/shared.blade.php';
}
// Update the paths
$settingsYamlPath = $newSrcDir.'/settings.yml';
+ $fullLiquidPath = $newSrcDir.'/full.liquid';
}
}
}
@@ -495,7 +391,6 @@ class PluginImportService
'settingsYamlPath' => $settingsYamlPath,
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
- 'sharedBladePath' => $sharedBladePath,
];
}
diff --git a/boost.json b/boost.json
new file mode 100644
index 0000000..53962fa
--- /dev/null
+++ b/boost.json
@@ -0,0 +1,15 @@
+{
+ "agents": [
+ "claude_code",
+ "copilot",
+ "cursor",
+ "phpstorm"
+ ],
+ "editors": [
+ "claude_code",
+ "cursor",
+ "phpstorm",
+ "vscode"
+ ],
+ "guidelines": []
+}
diff --git a/composer.json b/composer.json
index 8903e17..0ced4da 100644
--- a/composer.json
+++ b/composer.json
@@ -25,7 +25,6 @@
"livewire/flux": "^2.0",
"livewire/volt": "^1.7",
"om/icalparser": "^3.2",
- "simplesoftwareio/simple-qrcode": "^4.2",
"spatie/browsershot": "^5.0",
"stevebauman/purify": "^6.3",
"symfony/yaml": "^7.3",
diff --git a/composer.lock b/composer.lock
index a469e55..4d72e3c 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": "4de5f1df0160f59d08f428e36e81262e",
+ "content-hash": "25c2a1a4a2f2594adefe25ddb6a072fb",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.369.12",
+ "version": "3.369.6",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "36ee8894743a254ae2650bad4968c874b76bc7de"
+ "reference": "b1e1846a4b6593b6916764d86fc0890a31727370"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/36ee8894743a254ae2650bad4968c874b76bc7de",
- "reference": "36ee8894743a254ae2650bad4968c874b76bc7de",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1e1846a4b6593b6916764d86fc0890a31727370",
+ "reference": "b1e1846a4b6593b6916764d86fc0890a31727370",
"shasum": ""
},
"require": {
@@ -153,63 +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.12"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.369.6"
},
- "time": "2026-01-13T19:12:08+00:00"
- },
- {
- "name": "bacon/bacon-qr-code",
- "version": "2.0.8",
- "source": {
- "type": "git",
- "url": "https://github.com/Bacon/BaconQrCode.git",
- "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
- "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
- "shasum": ""
- },
- "require": {
- "dasprid/enum": "^1.0.3",
- "ext-iconv": "*",
- "php": "^7.1 || ^8.0"
- },
- "require-dev": {
- "phly/keep-a-changelog": "^2.1",
- "phpunit/phpunit": "^7 | ^8 | ^9",
- "spatie/phpunit-snapshot-assertions": "^4.2.9",
- "squizlabs/php_codesniffer": "^3.4"
- },
- "suggest": {
- "ext-imagick": "to generate QR code images"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "BaconQrCode\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-2-Clause"
- ],
- "authors": [
- {
- "name": "Ben Scholzen 'DASPRiD'",
- "email": "mail@dasprids.de",
- "homepage": "https://dasprids.de/",
- "role": "Developer"
- }
- ],
- "description": "BaconQrCode is a QR code generator for PHP.",
- "homepage": "https://github.com/Bacon/BaconQrCode",
- "support": {
- "issues": "https://github.com/Bacon/BaconQrCode/issues",
- "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
- },
- "time": "2022-12-07T17:46:57+00:00"
+ "time": "2026-01-02T19:09:23+00:00"
},
{
"name": "bnussbau/laravel-trmnl-blade",
@@ -495,56 +441,6 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
- {
- "name": "dasprid/enum",
- "version": "1.0.7",
- "source": {
- "type": "git",
- "url": "https://github.com/DASPRiD/Enum.git",
- "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
- "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1 <9.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
- "squizlabs/php_codesniffer": "*"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "DASPRiD\\Enum\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-2-Clause"
- ],
- "authors": [
- {
- "name": "Ben Scholzen 'DASPRiD'",
- "email": "mail@dasprids.de",
- "homepage": "https://dasprids.de/",
- "role": "Developer"
- }
- ],
- "description": "PHP 7.1 enum implementation",
- "keywords": [
- "enum",
- "map"
- ],
- "support": {
- "issues": "https://github.com/DASPRiD/Enum/issues",
- "source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
- },
- "time": "2025-09-16T12:23:56+00:00"
- },
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -981,16 +877,16 @@
},
{
"name": "firebase/php-jwt",
- "version": "v7.0.2",
+ "version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
- "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65"
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65",
- "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"shasum": ""
},
"require": {
@@ -1038,9 +934,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
- "source": "https://github.com/firebase/php-jwt/tree/v7.0.2"
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
},
- "time": "2025-12-16T22:17:28+00:00"
+ "time": "2025-04-09T20:32:01+00:00"
},
{
"name": "fruitcake/php-cors",
@@ -1782,16 +1678,16 @@
},
{
"name": "laravel/framework",
- "version": "v12.47.0",
+ "version": "v12.44.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec"
+ "reference": "592bbf1c036042958332eb98e3e8131b29102f33"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/ab8114c2e78f32e64eb238fc4b495bea3f8b80ec",
- "reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33",
+ "reference": "592bbf1c036042958332eb98e3e8131b29102f33",
"shasum": ""
},
"require": {
@@ -2000,20 +1896,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-01-13T15:29:06+00:00"
+ "time": "2025-12-23T15:29:43+00:00"
},
{
"name": "laravel/prompts",
- "version": "v0.3.9",
+ "version": "v0.3.8",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4"
+ "reference": "096748cdfb81988f60090bbb839ce3205ace0d35"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/5c41bf0555b7cfefaad4e66d3046675829581ac4",
- "reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35",
+ "reference": "096748cdfb81988f60090bbb839ce3205ace0d35",
"shasum": ""
},
"require": {
@@ -2057,22 +1953,22 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.9"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.8"
},
- "time": "2026-01-07T21:00:29+00:00"
+ "time": "2025-11-21T20:52:52+00:00"
},
{
"name": "laravel/sanctum",
- "version": "v4.2.3",
+ "version": "v4.2.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
- "reference": "47d26f1d310879ff757b971f5a6fc631d18663fd"
+ "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sanctum/zipball/47d26f1d310879ff757b971f5a6fc631d18663fd",
- "reference": "47d26f1d310879ff757b971f5a6fc631d18663fd",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664",
+ "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"shasum": ""
},
"require": {
@@ -2122,20 +2018,20 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
- "time": "2026-01-11T18:20:25+00:00"
+ "time": "2025-11-21T13:59:03+00:00"
},
{
"name": "laravel/serializable-closure",
- "version": "v2.0.8",
+ "version": "v2.0.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b"
+ "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b",
- "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd",
+ "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd",
"shasum": ""
},
"require": {
@@ -2183,25 +2079,25 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2026-01-08T16:22:46+00:00"
+ "time": "2025-11-21T20:52:36+00:00"
},
{
"name": "laravel/socialite",
- "version": "v5.24.2",
+ "version": "v5.24.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613"
+ "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613",
- "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/1d19358c28e8951dde6e36603b89d8f09e6cfbfd",
+ "reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd",
"shasum": ""
},
"require": {
"ext-json": "*",
- "firebase/php-jwt": "^6.4|^7.0",
+ "firebase/php-jwt": "^6.4",
"guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
@@ -2255,20 +2151,20 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
- "time": "2026-01-10T16:07:28+00:00"
+ "time": "2025-12-09T15:37:06+00:00"
},
{
"name": "laravel/tinker",
- "version": "v2.11.0",
+ "version": "v2.10.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
- "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468"
+ "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468",
- "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c",
+ "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c",
"shasum": ""
},
"require": {
@@ -2277,7 +2173,7 @@
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.5|^8.0",
"psy/psysh": "^0.11.1|^0.12.0",
- "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0"
+ "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2",
@@ -2319,9 +2215,9 @@
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
- "source": "https://github.com/laravel/tinker/tree/v2.11.0"
+ "source": "https://github.com/laravel/tinker/tree/v2.10.2"
},
- "time": "2025-12-19T19:16:45+00:00"
+ "time": "2025-11-20T16:29:12+00:00"
},
{
"name": "league/commonmark",
@@ -4922,74 +4818,6 @@
},
"time": "2025-12-14T04:43:48+00:00"
},
- {
- "name": "simplesoftwareio/simple-qrcode",
- "version": "4.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git",
- "reference": "916db7948ca6772d54bb617259c768c9cdc8d537"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537",
- "reference": "916db7948ca6772d54bb617259c768c9cdc8d537",
- "shasum": ""
- },
- "require": {
- "bacon/bacon-qr-code": "^2.0",
- "ext-gd": "*",
- "php": ">=7.2|^8.0"
- },
- "require-dev": {
- "mockery/mockery": "~1",
- "phpunit/phpunit": "~9"
- },
- "suggest": {
- "ext-imagick": "Allows the generation of PNG QrCodes.",
- "illuminate/support": "Allows for use within Laravel."
- },
- "type": "library",
- "extra": {
- "laravel": {
- "aliases": {
- "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode"
- },
- "providers": [
- "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider"
- ]
- }
- },
- "autoload": {
- "psr-4": {
- "SimpleSoftwareIO\\QrCode\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Simple Software LLC",
- "email": "support@simplesoftware.io"
- }
- ],
- "description": "Simple QrCode is a QR code generator made for Laravel.",
- "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode",
- "keywords": [
- "Simple",
- "generator",
- "laravel",
- "qrcode",
- "wrapper"
- ],
- "support": {
- "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues",
- "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0"
- },
- "time": "2021-02-08T20:43:55+00:00"
- },
{
"name": "spatie/browsershot",
"version": "5.2.0",
@@ -5121,16 +4949,16 @@
},
{
"name": "spatie/temporary-directory",
- "version": "2.3.1",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/temporary-directory.git",
- "reference": "662e481d6ec07ef29fd05010433428851a42cd07"
+ "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07",
- "reference": "662e481d6ec07ef29fd05010433428851a42cd07",
+ "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b",
+ "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b",
"shasum": ""
},
"require": {
@@ -5166,7 +4994,7 @@
],
"support": {
"issues": "https://github.com/spatie/temporary-directory/issues",
- "source": "https://github.com/spatie/temporary-directory/tree/2.3.1"
+ "source": "https://github.com/spatie/temporary-directory/tree/2.3.0"
},
"funding": [
{
@@ -5178,7 +5006,7 @@
"type": "github"
}
],
- "time": "2026-01-12T07:42:22+00:00"
+ "time": "2025-01-13T13:04:43+00:00"
},
{
"name": "stevebauman/purify",
@@ -8268,16 +8096,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
- "version": "v7.16.1",
+ "version": "v7.16.0",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
- "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b"
+ "reference": "a10878ed0fe0bbc2f57c980f7a08065338b970b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
- "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
+ "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a10878ed0fe0bbc2f57c980f7a08065338b970b6",
+ "reference": "a10878ed0fe0bbc2f57c980f7a08065338b970b6",
"shasum": ""
},
"require": {
@@ -8288,10 +8116,10 @@
"fidry/cpu-core-counter": "^1.3.0",
"jean85/pretty-package-versions": "^2.1.1",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
- "phpunit/php-code-coverage": "^12.5.2",
+ "phpunit/php-code-coverage": "^12.5.1",
"phpunit/php-file-iterator": "^6",
"phpunit/php-timer": "^8",
- "phpunit/phpunit": "^12.5.4",
+ "phpunit/phpunit": "^12.5.2",
"sebastian/environment": "^8.0.3",
"symfony/console": "^7.3.4 || ^8.0.0",
"symfony/process": "^7.3.4 || ^8.0.0"
@@ -8303,7 +8131,7 @@
"ext-posix": "*",
"phpstan/phpstan": "^2.1.33",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
- "phpstan/phpstan-phpunit": "^2.0.11",
+ "phpstan/phpstan-phpunit": "^2.0.10",
"phpstan/phpstan-strict-rules": "^2.0.7",
"symfony/filesystem": "^7.3.2 || ^8.0.0"
},
@@ -8345,7 +8173,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
- "source": "https://github.com/paratestphp/paratest/tree/v7.16.1"
+ "source": "https://github.com/paratestphp/paratest/tree/v7.16.0"
},
"funding": [
{
@@ -8357,7 +8185,7 @@
"type": "paypal"
}
],
- "time": "2026-01-08T07:23:06+00:00"
+ "time": "2025-12-09T20:03:26+00:00"
},
{
"name": "doctrine/deprecations",
@@ -8846,16 +8674,16 @@
},
{
"name": "laravel/boost",
- "version": "v1.8.9",
+ "version": "v1.8.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
- "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd"
+ "reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd",
- "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/7a5709a8134ed59d3e7f34fccbd74689830e296c",
+ "reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c",
"shasum": ""
},
"require": {
@@ -8908,20 +8736,20 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
- "time": "2026-01-07T18:43:11+00:00"
+ "time": "2025-12-19T15:04:12+00:00"
},
{
"name": "laravel/mcp",
- "version": "v0.5.2",
+ "version": "v0.5.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/mcp.git",
- "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c"
+ "reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
- "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
+ "url": "https://api.github.com/repos/laravel/mcp/zipball/10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4",
+ "reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4",
"shasum": ""
},
"require": {
@@ -8981,7 +8809,7 @@
"issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp"
},
- "time": "2025-12-19T19:32:34+00:00"
+ "time": "2025-12-17T06:14:23+00:00"
},
{
"name": "laravel/pail",
@@ -9064,16 +8892,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.27.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
+ "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
- "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
+ "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
"shasum": ""
},
"require": {
@@ -9084,9 +8912,9 @@
"php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.92.4",
- "illuminate/view": "^12.44.0",
- "larastan/larastan": "^3.8.1",
+ "friendsofphp/php-cs-fixer": "^3.90.0",
+ "illuminate/view": "^12.40.1",
+ "larastan/larastan": "^3.8.0",
"laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
@@ -9127,7 +8955,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2026-01-05T16:49:17+00:00"
+ "time": "2025-11-25T21:15:52+00:00"
},
{
"name": "laravel/roster",
@@ -9192,16 +9020,16 @@
},
{
"name": "laravel/sail",
- "version": "v1.52.0",
+ "version": "v1.51.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
- "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3"
+ "reference": "1c74357df034e869250b4365dd445c9f6ba5d068"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
- "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
+ "url": "https://api.github.com/repos/laravel/sail/zipball/1c74357df034e869250b4365dd445c9f6ba5d068",
+ "reference": "1c74357df034e869250b4365dd445c9f6ba5d068",
"shasum": ""
},
"require": {
@@ -9251,7 +9079,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
- "time": "2026-01-01T02:46:03+00:00"
+ "time": "2025-12-09T13:33:49+00:00"
},
{
"name": "mockery/mockery",
@@ -10321,16 +10149,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "2.3.1",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374"
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
- "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
"shasum": ""
},
"require": {
@@ -10362,9 +10190,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
},
- "time": "2026-01-12T11:33:04+00:00"
+ "time": "2025-08-30T15:50:23+00:00"
},
{
"name": "phpstan/phpstan",
@@ -10860,16 +10688,16 @@
},
{
"name": "rector/rector",
- "version": "2.3.1",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "9afc1bb43571b25629f353c61a9315b5ef31383a"
+ "reference": "f7166355dcf47482f27be59169b0825995f51c7d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/9afc1bb43571b25629f353c61a9315b5ef31383a",
- "reference": "9afc1bb43571b25629f353c61a9315b5ef31383a",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d",
+ "reference": "f7166355dcf47482f27be59169b0825995f51c7d",
"shasum": ""
},
"require": {
@@ -10908,7 +10736,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.3.1"
+ "source": "https://github.com/rectorphp/rector/tree/2.3.0"
},
"funding": [
{
@@ -10916,7 +10744,7 @@
"type": "github"
}
],
- "time": "2026-01-13T15:13:58+00:00"
+ "time": "2025-12-25T22:00:18+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -11978,16 +11806,16 @@
},
{
"name": "webmozart/assert",
- "version": "2.1.2",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
- "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649"
+ "reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649",
- "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/1b34b004e35a164bc5bb6ebd33c844b2d8069a54",
+ "reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54",
"shasum": ""
},
"require": {
@@ -12034,9 +11862,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
- "source": "https://github.com/webmozarts/assert/tree/2.1.2"
+ "source": "https://github.com/webmozarts/assert/tree/2.0.0"
},
- "time": "2026-01-13T14:02:24+00:00"
+ "time": "2025-12-16T21:36:00+00:00"
}
],
"aliases": [],
diff --git a/config/trustedproxy.php b/config/trustedproxy.php
deleted file mode 100644
index 8557288..0000000
--- a/config/trustedproxy.php
+++ /dev/null
@@ -1,6 +0,0 @@
- ($proxies = env('TRUSTED_PROXIES', '')) === '*' ? '*' : array_filter(explode(',', $proxies)),
-];
diff --git a/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php b/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php
deleted file mode 100644
index d230657..0000000
--- a/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php
+++ /dev/null
@@ -1,33 +0,0 @@
-string('kind')->nullable()->index();
- });
-
- // Set existing og_png and og_plus to kind "trmnl"
- DeviceModel::whereIn('name', ['og_png', 'og_plus'])->update(['kind' => 'trmnl']);
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('device_models', function (Blueprint $table) {
- $table->dropIndex(['kind']);
- $table->dropColumn('kind');
- });
- }
-};
diff --git a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php
deleted file mode 100644
index 3b9b1b7..0000000
--- a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php
+++ /dev/null
@@ -1,58 +0,0 @@
-selectRaw('user_id, trmnlp_id, COUNT(*) as duplicate_count')
- ->whereNotNull('trmnlp_id')
- ->groupBy('user_id', 'trmnlp_id')
- ->havingRaw('COUNT(*) > ?', [1])
- ->get();
-
- // For each duplicate combination, keep the first one (by id) and set others to null
- foreach ($duplicates as $duplicate) {
- $plugins = Plugin::query()
- ->where('user_id', $duplicate->user_id)
- ->where('trmnlp_id', $duplicate->trmnlp_id)
- ->orderBy('id')
- ->get();
-
- // Keep the first one, set the rest to null
- $keepFirst = true;
- foreach ($plugins as $plugin) {
- if ($keepFirst) {
- $keepFirst = false;
-
- continue;
- }
-
- $plugin->update(['trmnlp_id' => null]);
- }
- }
-
- Schema::table('plugins', function (Blueprint $table) {
- $table->unique(['user_id', 'trmnlp_id']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('plugins', function (Blueprint $table) {
- $table->dropUnique(['user_id', 'trmnlp_id']);
- });
- }
-};
diff --git a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php b/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php
deleted file mode 100644
index 0a527d7..0000000
--- a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-boolean('alias')->default(false);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('plugins', function (Blueprint $table) {
- $table->dropColumn('alias');
- });
- }
-};
diff --git a/database/seeders/ExampleRecipesSeeder.php b/database/seeders/ExampleRecipesSeeder.php
index 890eed9..5474615 100644
--- a/database/seeders/ExampleRecipesSeeder.php
+++ b/database/seeders/ExampleRecipesSeeder.php
@@ -13,8 +13,8 @@ class ExampleRecipesSeeder extends Seeder
public function run($user_id = 1): void
{
Plugin::updateOrCreate(
- ['uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec'],
[
+ 'uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec',
'name' => 'ÖBB Departures',
'user_id' => $user_id,
'data_payload' => null,
@@ -32,8 +32,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '3b046eda-34e9-4232-b935-c33b989a284b'],
[
+ 'uuid' => '3b046eda-34e9-4232-b935-c33b989a284b',
'name' => 'Weather',
'user_id' => $user_id,
'data_payload' => null,
@@ -51,8 +51,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54'],
[
+ 'uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54',
'name' => 'Zen Quotes',
'user_id' => $user_id,
'data_payload' => null,
@@ -70,8 +70,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f'],
[
+ 'uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f',
'name' => 'This Day in History',
'user_id' => $user_id,
'data_payload' => null,
@@ -89,8 +89,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d'],
[
+ 'uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d',
'name' => 'Home Assistant',
'user_id' => $user_id,
'data_payload' => null,
@@ -108,8 +108,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80'],
[
+ 'uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80',
'name' => 'Sunrise/Sunset',
'user_id' => $user_id,
'data_payload' => null,
@@ -127,8 +127,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe'],
[
+ 'uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe',
'name' => 'Pollen Forecast',
'user_id' => $user_id,
'data_payload' => null,
@@ -146,8 +146,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90'],
[
+ 'uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90',
'name' => 'Holidays (iCal)',
'user_id' => $user_id,
'data_payload' => null,
diff --git a/public/mirror/assets/apple-touch-icon-120x120.png b/public/mirror/assets/apple-touch-icon-120x120.png
deleted file mode 100644
index 5e51318..0000000
Binary files a/public/mirror/assets/apple-touch-icon-120x120.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-152x152.png b/public/mirror/assets/apple-touch-icon-152x152.png
deleted file mode 100644
index 9f8d9e3..0000000
Binary files a/public/mirror/assets/apple-touch-icon-152x152.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-167x167.png b/public/mirror/assets/apple-touch-icon-167x167.png
deleted file mode 100644
index 79d1211..0000000
Binary files a/public/mirror/assets/apple-touch-icon-167x167.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-180x180.png b/public/mirror/assets/apple-touch-icon-180x180.png
deleted file mode 100644
index 0499ff4..0000000
Binary files a/public/mirror/assets/apple-touch-icon-180x180.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-76x76.png b/public/mirror/assets/apple-touch-icon-76x76.png
deleted file mode 100644
index df3943a..0000000
Binary files a/public/mirror/assets/apple-touch-icon-76x76.png and /dev/null differ
diff --git a/public/mirror/assets/favicon-16x16.png b/public/mirror/assets/favicon-16x16.png
deleted file mode 100644
index b36f23b..0000000
Binary files a/public/mirror/assets/favicon-16x16.png and /dev/null differ
diff --git a/public/mirror/assets/favicon-32x32.png b/public/mirror/assets/favicon-32x32.png
deleted file mode 100644
index ae12e60..0000000
Binary files a/public/mirror/assets/favicon-32x32.png and /dev/null differ
diff --git a/public/mirror/assets/favicon.ico b/public/mirror/assets/favicon.ico
deleted file mode 100644
index da17cd5..0000000
Binary files a/public/mirror/assets/favicon.ico and /dev/null differ
diff --git a/public/mirror/assets/logo--brand.svg b/public/mirror/assets/logo--brand.svg
deleted file mode 100644
index 1b84f50..0000000
--- a/public/mirror/assets/logo--brand.svg
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/mirror/index.html b/public/mirror/index.html
deleted file mode 100644
index 64746fe..0000000
--- a/public/mirror/index.html
+++ /dev/null
@@ -1,521 +0,0 @@
-
-
-
-
-
- TRMNL BYOS Laravel Mirror
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/mirror/manifest.json b/public/mirror/manifest.json
deleted file mode 100644
index 4d44e44..0000000
--- a/public/mirror/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "TRMNL BYOS Laravel Mirror",
- "short_name": "TRMNL BYOS",
- "display": "standalone",
- "background_color": "#ffffff",
- "theme_color": "#ffffff"
-}
diff --git a/resources/css/app.css b/resources/css/app.css
index de95b81..30cb7a1 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -72,39 +72,3 @@ select:focus[data-flux-control] {
/* \[:where(&)\]:size-4 {
@apply size-4;
} */
-
-@layer components {
- /* standard container for app */
- .styled-container,
- .tab-button {
- @apply rounded-xl border bg-white dark:bg-stone-950 dark:border-zinc-700 text-stone-800 shadow-xs;
- }
-
- .tab-button {
- @apply flex items-center gap-2 px-4 py-2 text-sm font-medium;
- @apply rounded-b-none shadow-none bg-inherit;
-
- /* This makes the button sit slightly over the box border */
- margin-bottom: -1px;
- position: relative;
- z-index: 1;
- }
-
- .tab-button.is-active {
- @apply text-zinc-700 dark:text-zinc-300;
- @apply border-b-white dark:border-b-zinc-800;
-
- /* Z-index 10 ensures the bottom border of the tab hides the top border of the box */
- z-index: 10;
- }
-
- .tab-button:not(.is-active) {
- @apply text-zinc-500 border-transparent;
- }
-
- .tab-button:not(.is-active):hover {
- @apply text-zinc-700 dark:text-zinc-300;
- @apply border-zinc-300 dark:border-zinc-700;
- cursor: pointer;
- }
-}
diff --git a/resources/views/components/layouts/auth/card.blade.php b/resources/views/components/layouts/auth/card.blade.php
index b5a62c6..1a316ef 100644
--- a/resources/views/components/layouts/auth/card.blade.php
+++ b/resources/views/components/layouts/auth/card.blade.php
@@ -15,7 +15,7 @@
-
diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php
index fdf7f34..7257ab0 100644
--- a/resources/views/livewire/catalog/index.blade.php
+++ b/resources/views/livewire/catalog/index.blade.php
@@ -113,8 +113,7 @@ class extends Component
auth()->user(),
$plugin['zip_entry_path'] ?? null,
null,
- $plugin['logo_url'] ?? null,
- allowDuplicate: true
+ $plugin['logo_url'] ?? null
);
$this->dispatch('plugin-installed');
diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php
index cc8b070..9ecad1a 100644
--- a/resources/views/livewire/catalog/trmnl.blade.php
+++ b/resources/views/livewire/catalog/trmnl.blade.php
@@ -164,8 +164,7 @@ class extends Component
auth()->user(),
null,
config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null,
- $recipe['icon_url'] ?? null,
- allowDuplicate: true
+ $recipe['icon_url'] ?? null
);
$this->dispatch('plugin-installed');
diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php
index 7fd48a8..5db65d1 100644
--- a/resources/views/livewire/device-dashboard.blade.php
+++ b/resources/views/livewire/device-dashboard.blade.php
@@ -16,7 +16,7 @@ new class extends Component {
@if($devices->isEmpty())
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
Add your first device
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
@php
$current_image_uuid =$device->current_screen_image;
diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php
index f9d49ca..30b4481 100644
--- a/resources/views/livewire/devices/configure.blade.php
+++ b/resources/views/livewire/devices/configure.blade.php
@@ -309,7 +309,7 @@ new class extends Component {
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
@php
$current_image_uuid =$device->current_screen_image;
@@ -368,10 +368,6 @@ new class extends Component {
Update Firmware
Show Logs
-
- Mirror URL
-
-
Delete Device
@@ -502,26 +498,6 @@ new class extends Component {
-
- @php
- $mirrorUrl = url('/mirror/index.html') . '?mac_address=' . urlencode($device->mac_address) . '&api_key=' . urlencode($device->api_key);
- @endphp
-
-
-
- Mirror WebUI
- Mirror this device onto older devices with a web browser — Safari is supported back to iOS 9.
-
-
-
-
-
-
@if(!$device->mirror_device_id)
@if($current_image_path)
diff --git a/resources/views/livewire/playlists/index.blade.php b/resources/views/livewire/playlists/index.blade.php
index 6c979e6..3e786b4 100644
--- a/resources/views/livewire/playlists/index.blade.php
+++ b/resources/views/livewire/playlists/index.blade.php
@@ -332,7 +332,7 @@ new class extends Component {
@endforeach
@if($devices->isEmpty() || $devices->every(fn($device) => $device->playlists->isEmpty()))
-
+
No playlists found
Add playlists to your devices to see them here.
diff --git a/resources/views/livewire/plugins/config-modal.blade.php b/resources/views/livewire/plugins/config-modal.blade.php
deleted file mode 100644
index 7aaacbb..0000000
--- a/resources/views/livewire/plugins/config-modal.blade.php
+++ /dev/null
@@ -1,516 +0,0 @@
- loadData();
- }
-
- public function loadData(): void
- {
- $this->resetErrorBag();
- // Reload data
- $this->plugin = $this->plugin->fresh();
-
- $this->configuration_template = $this->plugin->configuration_template ?? [];
- $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
-
- // Initialize multiValues by exploding the CSV strings from the DB
- foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
- if (($field['field_type'] ?? null) === 'multi_string') {
- $fieldKey = $field['keyname'];
- $rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? '');
-
- $currentValue = is_array($rawValue) ? '' : (string)$rawValue;
-
- $this->multiValues[$fieldKey] = $currentValue !== ''
- ? array_values(array_filter(explode(',', $currentValue)))
- : [''];
- }
- }
- }
-
- /**
- * Triggered by @close on the modal to discard any typed but unsaved changes
- */
- public int $resetIndex = 0; // Add this property
- public function resetForm(): void
- {
- $this->loadData();
- $this->resetIndex++; // Increment to force DOM refresh
- }
-
- public function saveConfiguration()
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- // final validation layer
- $this->validate([
- 'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'],
- ], [
- 'multiValues.*.*.regex' => 'Items cannot contain commas.',
- ]);
-
- // Prepare config copy to send to db
- $finalValues = $this->configuration;
- foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
- $fieldKey = $field['keyname'];
-
- // Handle multi_string: Join array back to CSV string
- if ($field['field_type'] === 'multi_string' && isset($this->multiValues[$fieldKey])) {
- $finalValues[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
- }
-
- // Handle code fields: Try to JSON decode if necessary (standard TRMNL behavior)
- if ($field['field_type'] === 'code' && isset($finalValues[$fieldKey]) && is_string($finalValues[$fieldKey])) {
- $decoded = json_decode($finalValues[$fieldKey], true);
- if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
- $finalValues[$fieldKey] = $decoded;
- }
- }
- }
-
- // send to db
- $this->plugin->update(['configuration' => $finalValues]);
- $this->configuration = $finalValues; // update local state
- $this->dispatch('config-updated'); // notifies listeners
- Flux::modal('configuration-modal')->close();
- }
-
- // ------------------------------------This section contains helper functions for interacting with the form------------------------------------------------
- public function addMultiItem(string $fieldKey): void
- {
- $this->multiValues[$fieldKey][] = '';
- }
-
- public function removeMultiItem(string $fieldKey, int $index): void
- {
- unset($this->multiValues[$fieldKey][$index]);
-
- $this->multiValues[$fieldKey] = array_values($this->multiValues[$fieldKey]);
-
- if (empty($this->multiValues[$fieldKey])) {
- $this->multiValues[$fieldKey][] = '';
- }
- }
-
- // Livewire magic method to validate MultiValue input boxes
- // Runs on every debounce
- public function updatedMultiValues($value, $key)
- {
- $this->validate([
- 'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'],
- ], [
- 'multiValues.*.*.regex' => 'Items cannot contain commas.',
- ]);
- }
-
- public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- try {
- $requestData = [];
- if ($query !== null) {
- $requestData = [
- 'function' => $fieldKey,
- 'query' => $query
- ];
- }
-
- $response = $query !== null
- ? Http::post($endpoint, $requestData)
- : Http::post($endpoint);
-
- if ($response->successful()) {
- $this->xhrSelectOptions[$fieldKey] = $response->json();
- } else {
- $this->xhrSelectOptions[$fieldKey] = [];
- }
- } catch (\Exception $e) {
- $this->xhrSelectOptions[$fieldKey] = [];
- }
- }
-
- public function searchXhrSelect(string $fieldKey, string $endpoint): void
- {
- $query = $this->searchQueries[$fieldKey] ?? '';
- if (!empty($query)) {
- $this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
- }
- }
-};?>
-
-
-
-
-
- Configuration
- Configure your plugin settings
-
-
-
-
-
-
diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php
index d902183..4347aaf 100644
--- a/resources/views/livewire/plugins/index.blade.php
+++ b/resources/views/livewire/plugins/index.blade.php
@@ -395,7 +395,7 @@ new class extends Component {
wire:key="plugin-{{ $plugin['id'] ?? $plugin['name'] ?? $index }}"
x-data="{ pluginName: {{ json_encode(strtolower($plugin['name'] ?? '')) }} }"
x-show="searchTerm.length <= 1 || pluginName.includes(searchTerm.toLowerCase())"
- class="styled-container">
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php
index 0e29e76..bda8221 100644
--- a/resources/views/livewire/plugins/recipe.blade.php
+++ b/resources/views/livewire/plugins/recipe.blade.php
@@ -1,16 +1,12 @@
user()->plugins->contains($this->plugin), 403);
$this->blade_code = $this->plugin->render_markup;
- // required to render some stuff
$this->configuration_template = $this->plugin->configuration_template ?? [];
+ $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
if ($this->plugin->render_markup_view) {
try {
@@ -78,11 +76,24 @@ new class extends Component {
$this->fillformFields();
$this->data_payload_updated_at = $this->plugin->data_payload_updated_at;
- // Set default preview device model
- if ($this->preview_device_model_id === null) {
- $defaultModel = DeviceModel::where('name', 'og_plus')->first() ?? DeviceModel::first();
- $this->preview_device_model_id = $defaultModel?->id;
+ foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
+ if (($field['field_type'] ?? null) !== 'multi_string') {
+ continue;
+ }
+
+ $fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
+
+ // Get the existing value from the plugin's configuration
+ $rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? '');
+
+ $currentValue = is_array($rawValue) ? '' : (string)$rawValue;
+
+ // Split CSV into array for UI boxes
+ $this->multiValues[$fieldKey] = $currentValue !== ''
+ ? array_values(array_filter(explode(',', $currentValue)))
+ : [''];
}
+
}
public function fillFormFields(): void
@@ -276,6 +287,47 @@ new class extends Component {
Flux::modal('add-to-playlist')->close();
}
+ public function saveConfiguration()
+ {
+ abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
+
+ foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
+ $fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name'];
+
+ if ($field['field_type'] === 'multi_string' && isset($this->multiValues[$fieldKey])) {
+ // Join the boxes into a CSV string, trimming whitespace and filtering empties
+ $this->configuration[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
+ }
+ }
+ $configurationValues = [];
+ if (isset($this->configuration_template['custom_fields'])) {
+ foreach ($this->configuration_template['custom_fields'] as $field) {
+ $fieldKey = $field['keyname'];
+ if (isset($this->configuration[$fieldKey])) {
+ $value = $this->configuration[$fieldKey];
+
+ // For code fields, if the value is a JSON string and the original was an array, decode it
+ if ($field['field_type'] === 'code' && is_string($value)) {
+ $decoded = json_decode($value, true);
+ // If it's valid JSON and decodes to an array/object, use the decoded value
+ // Otherwise, keep the string as-is
+ if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
+ $value = $decoded;
+ }
+ }
+
+ $configurationValues[$fieldKey] = $value;
+ }
+ }
+ }
+
+ $this->plugin->update([
+ 'configuration' => $configurationValues
+ ]);
+
+ Flux::modal('configuration-modal')->close();
+ }
+
public function getDevicePlaylists($deviceId)
{
return \App\Models\Playlist::where('device_id', $deviceId)->get();
@@ -296,6 +348,8 @@ new class extends Component {
return $this->configuration[$key] ?? $default;
}
+
+
public function renderExample(string $example)
{
switch ($example) {
@@ -364,17 +418,13 @@ HTML;
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
- $this->preview_size = $size;
-
// If data strategy is polling and data_payload is null, fetch the data first
if ($this->plugin->data_strategy === 'polling' && $this->plugin->data_payload === null) {
$this->updateData();
}
try {
- // Create a device object with og_plus model and the selected bitdepth
- $device = $this->createPreviewDevice();
- $previewMarkup = $this->plugin->render($size, true, $device);
+ $previewMarkup = $this->plugin->render($size);
$this->dispatch('preview-updated', preview: $previewMarkup);
} catch (LiquidException $e) {
$this->dispatch('preview-error', message: $e->toLiquidErrorMessage());
@@ -383,38 +433,6 @@ HTML;
}
}
- private function createPreviewDevice(): \App\Models\Device
- {
- $deviceModel = DeviceModel::with(['palette'])->find($this->preview_device_model_id)
- ?? DeviceModel::with(['palette'])->first();
-
- $device = new Device();
- $device->setRelation('deviceModel', $deviceModel);
-
- return $device;
- }
-
- public function getDeviceModels()
- {
- return DeviceModel::whereKind('trmnl')->orderBy('label')->get();
- }
-
- public function updatedPreviewDeviceModelId(): void
- {
- $this->renderPreview($this->preview_size);
- }
-
- public function duplicatePlugin(): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- // Use the model's duplicate method
- $newPlugin = $this->plugin->duplicate(auth()->id());
-
- // Redirect to the new plugin's detail page
- $this->redirect(route('plugins.recipe', ['plugin' => $newPlugin]));
- }
-
public function deletePlugin(): void
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
@@ -422,31 +440,58 @@ HTML;
$this->redirect(route('plugins.index'));
}
- #[On('config-updated')]
- public function refreshPlugin()
- {
- // This pulls the fresh 'configuration' from the DB
- // and re-triggers the @if check in the Blade template
- $this->plugin = $this->plugin->fresh();
- }
+ public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
+ {
+ abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
- // Laravel Livewire computed property: access with $this->parsed_urls
- #[Computed]
- private function parsedUrls()
- {
- if (!isset($this->polling_url)) {
- return null;
+ try {
+ $requestData = [];
+ if ($query !== null) {
+ $requestData = [
+ 'function' => $fieldKey,
+ 'query' => $query
+ ];
+ }
+
+ $response = $query !== null
+ ? Http::post($endpoint, $requestData)
+ : Http::post($endpoint);
+
+ if ($response->successful()) {
+ $this->xhrSelectOptions[$fieldKey] = $response->json();
+ } else {
+ $this->xhrSelectOptions[$fieldKey] = [];
+ }
+ } catch (\Exception $e) {
+ $this->xhrSelectOptions[$fieldKey] = [];
+ }
}
- try {
- return $this->plugin->resolveLiquidVariables($this->polling_url);
-
- } catch (\Exception $e) {
- return 'PARSE_ERROR: ' . $e->getMessage();
+ public function searchXhrSelect(string $fieldKey, string $endpoint): void
+ {
+ $query = $this->searchQueries[$fieldKey] ?? '';
+ if (!empty($query)) {
+ $this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
+ }
}
- }
+ public function addMultiItem(string $fieldKey): void
+ {
+ $this->multiValues[$fieldKey][] = '';
+ }
+
+ public function removeMultiItem(string $fieldKey, int $index): void
+ {
+ unset($this->multiValues[$fieldKey][$index]);
+
+ $this->multiValues[$fieldKey] = array_values($this->multiValues[$fieldKey]);
+
+ if (empty($this->multiValues[$fieldKey])) {
+ $this->multiValues[$fieldKey][] = '';
+ }
+ }
}
+
?>
@@ -478,6 +523,7 @@ HTML;
+
@@ -487,11 +533,6 @@ HTML;
-
- Recipe Settings
-
-
- Duplicate Plugin
Delete Plugin
@@ -633,15 +674,8 @@ HTML;
-
+
Preview {{ $plugin->name }}
-
-
- @foreach($this->getDeviceModels() as $model)
- {{ $model->label ?? $model->name }}
- @endforeach
-
-
@@ -649,9 +683,355 @@ HTML;
-
+
+
+
+ Configuration
+ Configure your plugin settings
+
-
+
+
+
Settings
@@ -739,7 +1119,7 @@ HTML;
@endif
- Configuration Fields
+ Configuration
@endif
@@ -752,62 +1132,15 @@ HTML;
@if($data_strategy === 'polling')
-
Polling URL
-
-
-
-
-
- Settings
-
-
-
-
- Preview URL
-
-
-
-
-
-
- Enter the URL(s) to poll for data:
-
+
-
- {!! 'Hint: Supports multiple requests via line break separation. You can also use configuration variables with Liquid syntax . ' !!}
-
-
-
-
-
-
- Preview computed URLs here (readonly):
-
- {{ $this->parsed_urls }}
-
-
-
-
-
+ class="block w-full" type="text" name="polling_url" autofocus>
+
+
Fetch data now
-
@@ -950,7 +1283,7 @@ HTML;
/>
diff --git a/resources/views/livewire/plugins/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php
deleted file mode 100644
index 8ae3d6f..0000000
--- a/resources/views/livewire/plugins/recipes/settings.blade.php
+++ /dev/null
@@ -1,104 +0,0 @@
-resetErrorBag();
- // Reload data
- $this->plugin = $this->plugin->fresh();
- $this->trmnlp_id = $this->plugin->trmnlp_id;
- $this->uuid = $this->plugin->uuid;
- $this->alias = $this->plugin->alias ?? false;
- }
-
- public function saveTrmnlpId(): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- $this->validate([
- 'trmnlp_id' => [
- 'nullable',
- 'string',
- 'max:255',
- Rule::unique('plugins', 'trmnlp_id')
- ->where('user_id', auth()->id())
- ->ignore($this->plugin->id),
- ],
- 'alias' => 'boolean',
- ]);
-
- $this->plugin->update([
- 'trmnlp_id' => empty($this->trmnlp_id) ? null : $this->trmnlp_id,
- 'alias' => $this->alias,
- ]);
-
- Flux::modal('trmnlp-settings')->close();
- }
-
- public function getAliasUrlProperty(): string
- {
- return url("/api/display/{$this->uuid}/alias");
- }
-};?>
-
-
-
-
- Recipe Settings
-
-
-
-
-
diff --git a/resources/views/recipes/zen.blade.php b/resources/views/recipes/zen.blade.php
index 0ae920f..5e01eac 100644
--- a/resources/views/recipes/zen.blade.php
+++ b/resources/views/recipes/zen.blade.php
@@ -3,11 +3,11 @@
- {{$data['data'][0]['a'] ?? ''}}
- @if (strlen($data['data'][0]['q'] ?? '') < 300 && $size != 'quadrant')
- {{ $data['data'][0]['q'] ?? '' }}
+ {{$data[0]['a']}}
+ @if (strlen($data[0]['q']) < 300 && $size != 'quadrant')
+ {{ $data[0]['q'] }}
@else
- {{ $data['data'][0]['q'] ?? '' }}
+ {{ $data[0]['q'] }}
@endif
diff --git a/routes/api.php b/routes/api.php
index d201312..5700a43 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -18,13 +18,15 @@ use Illuminate\Support\Str;
Route::get('/display', function (Request $request) {
$mac_address = $request->header('id');
$access_token = $request->header('access-token');
- $device = Device::where('api_key', $access_token)->first();
+ $device = Device::where('mac_address', mb_strtoupper($mac_address ?? ''))
+ ->where('api_key', $access_token)
+ ->first();
if (! $device) {
// Check if there's a user with assign_new_devices enabled
$auto_assign_user = User::where('assign_new_devices', true)->first();
- if ($auto_assign_user && $mac_address) {
+ if ($auto_assign_user) {
// Create a new device and assign it to this user
$device = Device::create([
'mac_address' => mb_strtoupper($mac_address ?? ''),
@@ -37,7 +39,7 @@ Route::get('/display', function (Request $request) {
]);
} else {
return response()->json([
- 'message' => 'MAC Address not registered (or not set), or invalid access token',
+ 'message' => 'MAC Address not registered or invalid access token',
], 404);
}
}
@@ -611,7 +613,7 @@ Route::post('plugin_settings/{uuid}/image', function (Request $request, string $
}
// Generate a new UUID for each image upload to prevent device caching
- $imageUuid = Str::uuid()->toString();
+ $imageUuid = \Illuminate\Support\Str::uuid()->toString();
$filename = $imageUuid.'.'.$extension;
$path = 'images/generated/'.$filename;
@@ -676,90 +678,3 @@ Route::post('plugin_settings/{trmnlp_id}/archive', function (Request $request, s
],
]);
})->middleware('auth:sanctum');
-
-Route::get('/display/{uuid}/alias', function (Request $request, string $uuid) {
- $plugin = Plugin::where('uuid', $uuid)->firstOrFail();
-
- // Check if alias is active
- if (! $plugin->alias) {
- return response()->json([
- 'message' => 'Alias is not active for this plugin',
- ], 403);
- }
-
- // Get device model name from query parameter, default to 'og_png'
- $deviceModelName = $request->query('device-model', 'og_png');
- $deviceModel = DeviceModel::where('name', $deviceModelName)->first();
-
- if (! $deviceModel) {
- return response()->json([
- 'message' => "Device model '{$deviceModelName}' not found",
- ], 404);
- }
-
- // Check if we can use cached image (only for og_png and if data is not stale)
- $useCache = $deviceModelName === 'og_png' && ! $plugin->isDataStale() && $plugin->current_image !== null;
-
- if ($useCache) {
- // Return cached image
- $imageUuid = $plugin->current_image;
- $fileExtension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
- $imagePath = 'images/generated/'.$imageUuid.'.'.$fileExtension;
-
- // Check if image exists, otherwise fall back to generation
- if (Storage::disk('public')->exists($imagePath)) {
- return response()->file(Storage::disk('public')->path($imagePath), [
- 'Content-Type' => $deviceModel->mime_type,
- ]);
- }
- }
-
- // Generate new image
- try {
- // Update data if needed
- if ($plugin->isDataStale()) {
- $plugin->updateDataPayload();
- $plugin->refresh();
- }
-
- // Load device model with palette relationship
- $deviceModel->load('palette');
-
- // Create a virtual device for rendering (Plugin::render needs a Device object)
- $virtualDevice = new Device();
- $virtualDevice->setRelation('deviceModel', $deviceModel);
- $virtualDevice->setRelation('user', $plugin->user);
- $virtualDevice->setRelation('palette', $deviceModel->palette);
-
- // Render the plugin markup
- $markup = $plugin->render(device: $virtualDevice);
-
- // Generate image using the new method that doesn't require a device
- $imageUuid = ImageGenerationService::generateImageFromModel(
- markup: $markup,
- deviceModel: $deviceModel,
- user: $plugin->user,
- palette: $deviceModel->palette
- );
-
- // Update plugin cache if using og_png
- if ($deviceModelName === 'og_png') {
- $plugin->update(['current_image' => $imageUuid]);
- }
-
- // Return the generated image
- $fileExtension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
- $imagePath = Storage::disk('public')->path('images/generated/'.$imageUuid.'.'.$fileExtension);
-
- return response()->file($imagePath, [
- 'Content-Type' => $deviceModel->mime_type,
- ]);
- } catch (Exception $e) {
- Log::error("Failed to generate alias image for plugin {$plugin->id} ({$plugin->name}): ".$e->getMessage());
-
- return response()->json([
- 'message' => 'Failed to generate image',
- 'error' => $e->getMessage(),
- ], 500);
- }
-})->name('api.display.alias');
diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php
index c98cb2f..2925a5e 100644
--- a/tests/Feature/Api/DeviceEndpointsTest.php
+++ b/tests/Feature/Api/DeviceEndpointsTest.php
@@ -263,7 +263,7 @@ test('invalid device credentials return error', function (): void {
])->get('/api/display');
$response->assertNotFound()
- ->assertJson(['message' => 'MAC Address not registered (or not set), or invalid access token']);
+ ->assertJson(['message' => 'MAC Address not registered or invalid access token']);
});
test('log endpoint requires valid device credentials', function (): void {
diff --git a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
index f0be135..7674d7f 100644
--- a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
+++ b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
@@ -44,7 +44,6 @@ test('fetch device models job handles successful api response', function (): voi
'mime_type' => 'image/png',
'offset_x' => 0,
'offset_y' => 0,
- 'kind' => 'trmnl',
'published_at' => '2023-01-01T00:00:00Z',
],
],
@@ -75,7 +74,6 @@ test('fetch device models job handles successful api response', function (): voi
expect($deviceModel->mime_type)->toBe('image/png');
expect($deviceModel->offset_x)->toBe(0);
expect($deviceModel->offset_y)->toBe(0);
- // expect($deviceModel->kind)->toBe('trmnl');
expect($deviceModel->source)->toBe('api');
});
@@ -314,7 +312,6 @@ test('fetch device models job handles device model with partial data', function
expect($deviceModel->mime_type)->toBe('');
expect($deviceModel->offset_x)->toBe(0);
expect($deviceModel->offset_y)->toBe(0);
- expect($deviceModel->kind)->toBeNull();
expect($deviceModel->source)->toBe('api');
});
diff --git a/tests/Feature/Livewire/Plugins/ConfigModalTest.php b/tests/Feature/Livewire/Plugins/ConfigModalTest.php
deleted file mode 100644
index 4372991..0000000
--- a/tests/Feature/Livewire/Plugins/ConfigModalTest.php
+++ /dev/null
@@ -1,124 +0,0 @@
-create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration_template' => [
- 'custom_fields' => [[
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Reading Days',
- 'default' => 'alpha,beta',
- ]]
- ],
- 'configuration' => ['tags' => 'alpha,beta']
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->assertSet('multiValues.tags', ['alpha', 'beta']);
-});
-
-test('config modal validates against commas in multi_string boxes', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration_template' => [
- 'custom_fields' => [[
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Reading Days',
- ]]
- ]
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->set('multiValues.tags.0', 'no,commas,allowed')
- ->call('saveConfiguration')
- ->assertHasErrors(['multiValues.tags.0' => 'regex']);
-
- // Assert DB remains unchanged
- expect($plugin->fresh()->configuration['tags'] ?? '')->not->toBe('no,commas,allowed');
-});
-
-test('config modal merges multi_string boxes into a single CSV string on save', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration_template' => [
- 'custom_fields' => [[
- 'keyname' => 'items',
- 'field_type' => 'multi_string',
- 'name' => 'Reading Days',
- ]]
- ],
- 'configuration' => []
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->set('multiValues.items.0', 'First')
- ->call('addMultiItem', 'items')
- ->set('multiValues.items.1', 'Second')
- ->call('saveConfiguration')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->configuration['items'])->toBe('First,Second');
-});
-
-test('config modal resetForm clears dirty state and increments resetIndex', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration' => ['simple_key' => 'original_value']
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->set('configuration.simple_key', 'dirty_value')
- ->call('resetForm')
- ->assertSet('configuration.simple_key', 'original_value')
- ->assertSet('resetIndex', 1);
-});
-
-test('config modal dispatches update event for parent warning refresh', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static'
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->call('saveConfiguration')
- ->assertDispatched('config-updated');
-});
diff --git a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php
deleted file mode 100644
index a04815f..0000000
--- a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
-create();
- $this->actingAs($user);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => null,
- ]);
-
- $trmnlpId = (string) Str::uuid();
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin])
- ->set('trmnlp_id', $trmnlpId)
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId);
-});
-
-test('recipe settings validates trmnlp_id is unique per user', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $existingPlugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => 'existing-id-123',
- ]);
-
- $newPlugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => null,
- ]);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $newPlugin])
- ->set('trmnlp_id', 'existing-id-123')
- ->call('saveTrmnlpId')
- ->assertHasErrors(['trmnlp_id' => 'unique']);
-
- expect($newPlugin->fresh()->trmnlp_id)->toBeNull();
-});
-
-test('recipe settings allows same trmnlp_id for different users', function (): void {
- $user1 = User::factory()->create();
- $user2 = User::factory()->create();
-
- $plugin1 = Plugin::factory()->create([
- 'user_id' => $user1->id,
- 'trmnlp_id' => 'shared-id-123',
- ]);
-
- $plugin2 = Plugin::factory()->create([
- 'user_id' => $user2->id,
- 'trmnlp_id' => null,
- ]);
-
- $this->actingAs($user2);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin2])
- ->set('trmnlp_id', 'shared-id-123')
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin2->fresh()->trmnlp_id)->toBe('shared-id-123');
-});
-
-test('recipe settings allows same trmnlp_id for the same plugin', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $trmnlpId = (string) Str::uuid();
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => $trmnlpId,
- ]);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin])
- ->set('trmnlp_id', $trmnlpId)
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId);
-});
-
-test('recipe settings can clear trmnlp_id', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => 'some-id',
- ]);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin])
- ->set('trmnlp_id', '')
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->trmnlp_id)->toBeNull();
-});
diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php
index f3ef1fa..1b20f93 100644
--- a/tests/Feature/PluginImportTest.php
+++ b/tests/Feature/PluginImportTest.php
@@ -83,34 +83,19 @@ it('throws exception for invalid zip file', function (): void {
->toThrow(Exception::class, 'Could not open the ZIP file.');
});
-it('throws exception for missing settings.yml', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/full.liquid' => getValidFullLiquid(),
- // Missing settings.yml
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Invalid ZIP structure. Required file settings.yml is missing.');
-});
-
-it('throws exception for missing template files', function (): void {
+it('throws exception for missing required files', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
'src/settings.yml' => getValidSettingsYaml(),
- // Missing all template files
+ // Missing full.liquid
]);
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
$pluginImportService = new PluginImportService();
expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
+ ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.');
});
it('sets default values when settings are missing', function (): void {
@@ -442,103 +427,6 @@ YAML;
->and($displayIncidentField['default'])->toBe('true');
});
-it('throws exception when multi_string default value contains a comma', function (): void {
- $user = User::factory()->create();
-
- // YAML with a comma in the 'default' field of a multi_string
- $invalidYaml = <<<'YAML'
-name: Test Plugin
-refresh_interval: 30
-strategy: static
-polling_verb: get
-static_data: '{"test": "data"}'
-custom_fields:
- - keyname: api_key
- field_type: multi_string
- default: default-api-key1,default-api-key2
- label: API Key
-YAML;
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => $invalidYaml,
- 'src/full.liquid' => getValidFullLiquid(),
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('invalid-default.zip', $zipContent);
- $pluginImportService = new PluginImportService();
-
- expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.');
-});
-
-it('throws exception when multi_string placeholder contains a comma', function (): void {
- $user = User::factory()->create();
-
- // YAML with a comma in the 'placeholder' field
- $invalidYaml = <<<'YAML'
-name: Test Plugin
-refresh_interval: 30
-strategy: static
-polling_verb: get
-static_data: '{"test": "data"}'
-custom_fields:
- - keyname: api_key
- field_type: multi_string
- default: default-api-key
- label: API Key
- placeholder: "value1, value2"
-YAML;
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => $invalidYaml,
- 'src/full.liquid' => getValidFullLiquid(),
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('invalid-placeholder.zip', $zipContent);
- $pluginImportService = new PluginImportService();
-
- expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.');
-});
-
-it('imports plugin with only shared.liquid file', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => getValidSettingsYaml(),
- 'src/shared.liquid' => '{{ data.title }}
',
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- $plugin = $pluginImportService->importFromZip($zipFile, $user);
-
- expect($plugin)->toBeInstanceOf(Plugin::class)
- ->and($plugin->markup_language)->toBe('liquid')
- ->and($plugin->render_markup)->toContain('')
- ->and($plugin->render_markup)->toContain('
{{ data.title }}
');
-});
-
-it('imports plugin with only shared.blade.php file', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => getValidSettingsYaml(),
- 'src/shared.blade.php' => '
{{ $data["title"] }}
',
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- $plugin = $pluginImportService->importFromZip($zipFile, $user);
-
- expect($plugin)->toBeInstanceOf(Plugin::class)
- ->and($plugin->markup_language)->toBe('blade')
- ->and($plugin->render_markup)->toBe('
{{ $data["title"] }}
')
- ->and($plugin->render_markup)->not->toContain('
');
-});
-
// Helper methods
function createMockZipFile(array $files): string
{
diff --git a/tests/Unit/Models/PluginTest.php b/tests/Unit/Models/PluginTest.php
index aa9a28e..0847e36 100644
--- a/tests/Unit/Models/PluginTest.php
+++ b/tests/Unit/Models/PluginTest.php
@@ -99,35 +99,6 @@ test('updateDataPayload handles multiple URLs with IDX_ prefixes', function ():
expect($plugin->data_payload['IDX_2'])->toBe(['headline' => 'test']);
});
-test('updateDataPayload skips empty lines in polling_url and maintains sequential IDX keys', function (): void {
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- // empty lines and extra spaces between the URL to generate empty entries
- 'polling_url' => "https://api1.example.com/data\n \n\nhttps://api2.example.com/weather\n ",
- 'polling_verb' => 'get',
- ]);
-
- // Mock only the valid URLs
- Http::fake([
- 'https://api1.example.com/data' => Http::response(['item' => 'first'], 200),
- 'https://api2.example.com/weather' => Http::response(['item' => 'second'], 200),
- ]);
-
- $plugin->updateDataPayload();
-
- // payload should only have 2 items, and they should be indexed 0 and 1
- expect($plugin->data_payload)->toHaveCount(2);
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
-
- // data is correct
- expect($plugin->data_payload['IDX_0'])->toBe(['item' => 'first']);
- expect($plugin->data_payload['IDX_1'])->toBe(['item' => 'second']);
-
- // no empty index exists
- expect($plugin->data_payload)->not->toHaveKey('IDX_2');
-});
-
test('updateDataPayload handles single URL without nesting', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
@@ -766,175 +737,3 @@ test('plugin model preserves multi_string csv format', function (): void {
expect($plugin->fresh()->configuration['tags'])->toBe('laravel,pest,security');
});
-
-test('plugin duplicate copies all attributes except id and uuid', function (): void {
- $user = User::factory()->create();
-
- $original = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'name' => 'Original Plugin',
- 'data_stale_minutes' => 30,
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://api.example.com/data',
- 'polling_verb' => 'get',
- 'polling_header' => 'Authorization: Bearer token123',
- 'polling_body' => '{"query": "test"}',
- 'render_markup' => '
Test markup
',
- 'markup_language' => 'blade',
- 'configuration' => ['api_key' => 'secret123'],
- 'configuration_template' => [
- 'custom_fields' => [
- [
- 'keyname' => 'api_key',
- 'field_type' => 'string',
- ],
- ],
- ],
- 'no_bleed' => true,
- 'dark_mode' => true,
- 'data_payload' => ['test' => 'data'],
- ]);
-
- $duplicate = $original->duplicate();
-
- // Refresh to ensure casts are applied
- $original->refresh();
- $duplicate->refresh();
-
- expect($duplicate->id)->not->toBe($original->id)
- ->and($duplicate->uuid)->not->toBe($original->uuid)
- ->and($duplicate->name)->toBe('Original Plugin (Copy)')
- ->and($duplicate->user_id)->toBe($original->user_id)
- ->and($duplicate->data_stale_minutes)->toBe($original->data_stale_minutes)
- ->and($duplicate->data_strategy)->toBe($original->data_strategy)
- ->and($duplicate->polling_url)->toBe($original->polling_url)
- ->and($duplicate->polling_verb)->toBe($original->polling_verb)
- ->and($duplicate->polling_header)->toBe($original->polling_header)
- ->and($duplicate->polling_body)->toBe($original->polling_body)
- ->and($duplicate->render_markup)->toBe($original->render_markup)
- ->and($duplicate->markup_language)->toBe($original->markup_language)
- ->and($duplicate->configuration)->toBe($original->configuration)
- ->and($duplicate->configuration_template)->toBe($original->configuration_template)
- ->and($duplicate->no_bleed)->toBe($original->no_bleed)
- ->and($duplicate->dark_mode)->toBe($original->dark_mode)
- ->and($duplicate->data_payload)->toBe($original->data_payload)
- ->and($duplicate->render_markup_view)->toBeNull();
-});
-
-test('plugin duplicate copies render_markup_view file content to render_markup', function (): void {
- $user = User::factory()->create();
-
- // Create a test blade file
- $testViewPath = resource_path('views/recipes/test-duplicate.blade.php');
- $testContent = '
Test Content
';
-
- // Ensure directory exists
- if (! is_dir(dirname($testViewPath))) {
- mkdir(dirname($testViewPath), 0755, true);
- }
-
- file_put_contents($testViewPath, $testContent);
-
- try {
- $original = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'name' => 'View Plugin',
- 'render_markup' => null,
- 'render_markup_view' => 'recipes.test-duplicate',
- 'markup_language' => null,
- ]);
-
- $duplicate = $original->duplicate();
-
- expect($duplicate->render_markup)->toBe($testContent)
- ->and($duplicate->markup_language)->toBe('blade')
- ->and($duplicate->render_markup_view)->toBeNull()
- ->and($duplicate->name)->toBe('View Plugin (Copy)');
- } finally {
- // Clean up test file
- if (file_exists($testViewPath)) {
- unlink($testViewPath);
- }
- }
-});
-
-test('plugin duplicate handles liquid file extension', function (): void {
- $user = User::factory()->create();
-
- // Create a test liquid file
- $testViewPath = resource_path('views/recipes/test-duplicate-liquid.liquid');
- $testContent = '
{{ data.message }}
';
-
- // Ensure directory exists
- if (! is_dir(dirname($testViewPath))) {
- mkdir(dirname($testViewPath), 0755, true);
- }
-
- file_put_contents($testViewPath, $testContent);
-
- try {
- $original = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'name' => 'Liquid Plugin',
- 'render_markup' => null,
- 'render_markup_view' => 'recipes.test-duplicate-liquid',
- 'markup_language' => null,
- ]);
-
- $duplicate = $original->duplicate();
-
- expect($duplicate->render_markup)->toBe($testContent)
- ->and($duplicate->markup_language)->toBe('liquid')
- ->and($duplicate->render_markup_view)->toBeNull();
- } finally {
- // Clean up test file
- if (file_exists($testViewPath)) {
- unlink($testViewPath);
- }
- }
-});
-
-test('plugin duplicate handles missing view file gracefully', function (): void {
- $user = User::factory()->create();
-
- $original = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'name' => 'Missing View Plugin',
- 'render_markup' => null,
- 'render_markup_view' => 'recipes.nonexistent-view',
- 'markup_language' => null,
- ]);
-
- $duplicate = $original->duplicate();
-
- expect($duplicate->render_markup_view)->toBeNull()
- ->and($duplicate->name)->toBe('Missing View Plugin (Copy)');
-});
-
-test('plugin duplicate uses provided user_id', function (): void {
- $user1 = User::factory()->create();
- $user2 = User::factory()->create();
-
- $original = Plugin::factory()->create([
- 'user_id' => $user1->id,
- 'name' => 'Original Plugin',
- ]);
-
- $duplicate = $original->duplicate($user2->id);
-
- expect($duplicate->user_id)->toBe($user2->id)
- ->and($duplicate->user_id)->not->toBe($original->user_id);
-});
-
-test('plugin duplicate falls back to original user_id when no user_id provided', function (): void {
- $user = User::factory()->create();
-
- $original = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'name' => 'Original Plugin',
- ]);
-
- $duplicate = $original->duplicate();
-
- expect($duplicate->user_id)->toBe($original->user_id);
-});