mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-14 15:37:53 +00:00
Compare commits
No commits in common. "131d99a2e3610da801804c09a19f62fa04366efa" and "a86315c5c773d51fd2839bbe28ce1d04d2cf79a7" have entirely different histories.
131d99a2e3
...
a86315c5c7
11 changed files with 73 additions and 650 deletions
|
|
@ -46,7 +46,6 @@ class Plugin extends Model
|
||||||
'dark_mode' => 'boolean',
|
'dark_mode' => 'boolean',
|
||||||
'preferred_renderer' => 'string',
|
'preferred_renderer' => 'string',
|
||||||
'plugin_type' => 'string',
|
'plugin_type' => 'string',
|
||||||
'alias' => 'boolean',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function boot()
|
protected static function boot()
|
||||||
|
|
@ -154,7 +153,7 @@ class Plugin extends Model
|
||||||
|
|
||||||
public function updateDataPayload(): void
|
public function updateDataPayload(): void
|
||||||
{
|
{
|
||||||
if ($this->data_strategy !== 'polling' || ! $this->polling_url) {
|
if ($this->data_strategy !== 'polling' || !$this->polling_url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
|
$headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
|
||||||
|
|
|
||||||
|
|
@ -26,44 +26,11 @@ class ImageGenerationService
|
||||||
public static function generateImage(string $markup, $deviceId): string
|
public static function generateImage(string $markup, $deviceId): string
|
||||||
{
|
{
|
||||||
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId);
|
$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();
|
$uuid = Uuid::uuid4()->toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get image generation settings from DeviceModel or Device (for legacy devices)
|
// Get image generation settings from DeviceModel if available, otherwise use device settings
|
||||||
$imageSettings = $deviceModel
|
$imageSettings = self::getImageSettings($device);
|
||||||
? self::getImageSettingsFromModel($deviceModel)
|
|
||||||
: ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null));
|
|
||||||
|
|
||||||
$fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png';
|
$fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png';
|
||||||
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
|
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
|
||||||
|
|
@ -78,7 +45,7 @@ class ImageGenerationService
|
||||||
$browserStage->html($markup);
|
$browserStage->html($markup);
|
||||||
|
|
||||||
// Set timezone from user or fall back to app timezone
|
// 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);
|
$browserStage->timezone($timezone);
|
||||||
|
|
||||||
if (config('app.puppeteer_window_size_strategy') === 'v2') {
|
if (config('app.puppeteer_window_size_strategy') === 'v2') {
|
||||||
|
|
@ -98,12 +65,12 @@ class ImageGenerationService
|
||||||
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
|
$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;
|
$colorPalette = null;
|
||||||
|
|
||||||
if ($palette && $palette->colors) {
|
if ($palette && $palette->colors) {
|
||||||
$colorPalette = $palette->colors;
|
$colorPalette = $palette->colors;
|
||||||
} elseif ($deviceModel?->palette && $deviceModel->palette->colors) {
|
|
||||||
$colorPalette = $deviceModel->palette->colors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$imageStage = new ImageStage();
|
$imageStage = new ImageStage();
|
||||||
|
|
@ -140,7 +107,8 @@ class ImageGenerationService
|
||||||
throw new RuntimeException('Image file is empty: '.$outputPath);
|
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;
|
return $uuid;
|
||||||
|
|
||||||
|
|
@ -157,7 +125,22 @@ class ImageGenerationService
|
||||||
{
|
{
|
||||||
// If device has a DeviceModel, use its settings
|
// If device has a DeviceModel, use its settings
|
||||||
if ($device->deviceModel) {
|
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
|
// 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
|
* Determine the appropriate ImageFormat based on DeviceModel settings
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,11 @@ class PluginImportService
|
||||||
* Validate YAML settings
|
* Validate YAML settings
|
||||||
*
|
*
|
||||||
* @param array $settings The parsed YAML settings
|
* @param array $settings The parsed YAML settings
|
||||||
*
|
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private function validateYAML(array $settings): void
|
private function validateYAML(array $settings): void
|
||||||
{
|
{
|
||||||
if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) {
|
if (!isset($settings['custom_fields']) || !is_array($settings['custom_fields'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +43,6 @@ class PluginImportService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import a plugin from a ZIP file
|
* Import a plugin from a ZIP file
|
||||||
*
|
*
|
||||||
|
|
@ -75,17 +73,12 @@ class PluginImportService
|
||||||
$zip->extractTo($tempDir);
|
$zip->extractTo($tempDir);
|
||||||
$zip->close();
|
$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);
|
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
|
||||||
|
|
||||||
// Validate that we found the required files
|
// Validate that we found the required files
|
||||||
if (! $filePaths['settingsYamlPath']) {
|
if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
|
||||||
throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
|
throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php
|
||||||
}
|
|
||||||
|
|
||||||
// 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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse settings.yml
|
// Parse settings.yml
|
||||||
|
|
@ -93,37 +86,20 @@ class PluginImportService
|
||||||
$settings = Yaml::parse($settingsYaml);
|
$settings = Yaml::parse($settingsYaml);
|
||||||
$this->validateYAML($settings);
|
$this->validateYAML($settings);
|
||||||
|
|
||||||
// Determine which template file to use and read its content
|
// Read full.liquid content
|
||||||
$templatePath = null;
|
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
||||||
$markupLanguage = 'blade';
|
|
||||||
|
|
||||||
if ($filePaths['fullLiquidPath']) {
|
// Prepend shared.liquid content if available
|
||||||
$templatePath = $filePaths['fullLiquidPath'];
|
|
||||||
$fullLiquid = File::get($templatePath);
|
|
||||||
|
|
||||||
// Prepend shared.liquid or shared.blade.php content if available
|
|
||||||
if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
|
if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
|
||||||
$sharedLiquid = File::get($filePaths['sharedLiquidPath']);
|
$sharedLiquid = File::get($filePaths['sharedLiquidPath']);
|
||||||
$fullLiquid = $sharedLiquid."\n".$fullLiquid;
|
$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
|
// Check if the file ends with .liquid to set markup language
|
||||||
if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
|
|
||||||
$markupLanguage = 'liquid';
|
|
||||||
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
|
||||||
}
|
|
||||||
} elseif ($filePaths['sharedLiquidPath']) {
|
|
||||||
$templatePath = $filePaths['sharedLiquidPath'];
|
|
||||||
$fullLiquid = File::get($templatePath);
|
|
||||||
$markupLanguage = 'liquid';
|
|
||||||
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
|
||||||
} elseif ($filePaths['sharedBladePath']) {
|
|
||||||
$templatePath = $filePaths['sharedBladePath'];
|
|
||||||
$fullLiquid = File::get($templatePath);
|
|
||||||
$markupLanguage = 'blade';
|
$markupLanguage = 'blade';
|
||||||
|
if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
|
||||||
|
$markupLanguage = 'liquid';
|
||||||
|
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure custom_fields is properly formatted
|
// Ensure custom_fields is properly formatted
|
||||||
|
|
@ -228,17 +204,12 @@ class PluginImportService
|
||||||
$zip->extractTo($tempDir);
|
$zip->extractTo($tempDir);
|
||||||
$zip->close();
|
$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);
|
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
|
||||||
|
|
||||||
// Validate that we found the required files
|
// Validate that we found the required files
|
||||||
if (! $filePaths['settingsYamlPath']) {
|
if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
|
||||||
throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
|
throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are 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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse settings.yml
|
// Parse settings.yml
|
||||||
|
|
@ -246,37 +217,20 @@ class PluginImportService
|
||||||
$settings = Yaml::parse($settingsYaml);
|
$settings = Yaml::parse($settingsYaml);
|
||||||
$this->validateYAML($settings);
|
$this->validateYAML($settings);
|
||||||
|
|
||||||
// Determine which template file to use and read its content
|
// Read full.liquid content
|
||||||
$templatePath = null;
|
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
||||||
$markupLanguage = 'blade';
|
|
||||||
|
|
||||||
if ($filePaths['fullLiquidPath']) {
|
// Prepend shared.liquid content if available
|
||||||
$templatePath = $filePaths['fullLiquidPath'];
|
|
||||||
$fullLiquid = File::get($templatePath);
|
|
||||||
|
|
||||||
// Prepend shared.liquid or shared.blade.php content if available
|
|
||||||
if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
|
if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
|
||||||
$sharedLiquid = File::get($filePaths['sharedLiquidPath']);
|
$sharedLiquid = File::get($filePaths['sharedLiquidPath']);
|
||||||
$fullLiquid = $sharedLiquid."\n".$fullLiquid;
|
$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
|
// Check if the file ends with .liquid to set markup language
|
||||||
if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
|
|
||||||
$markupLanguage = 'liquid';
|
|
||||||
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
|
||||||
}
|
|
||||||
} elseif ($filePaths['sharedLiquidPath']) {
|
|
||||||
$templatePath = $filePaths['sharedLiquidPath'];
|
|
||||||
$fullLiquid = File::get($templatePath);
|
|
||||||
$markupLanguage = 'liquid';
|
|
||||||
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
|
||||||
} elseif ($filePaths['sharedBladePath']) {
|
|
||||||
$templatePath = $filePaths['sharedBladePath'];
|
|
||||||
$fullLiquid = File::get($templatePath);
|
|
||||||
$markupLanguage = 'blade';
|
$markupLanguage = 'blade';
|
||||||
|
if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
|
||||||
|
$markupLanguage = 'liquid';
|
||||||
|
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure custom_fields is properly formatted
|
// Ensure custom_fields is properly formatted
|
||||||
|
|
@ -356,7 +310,6 @@ class PluginImportService
|
||||||
$settingsYamlPath = null;
|
$settingsYamlPath = null;
|
||||||
$fullLiquidPath = null;
|
$fullLiquidPath = null;
|
||||||
$sharedLiquidPath = null;
|
$sharedLiquidPath = null;
|
||||||
$sharedBladePath = null;
|
|
||||||
|
|
||||||
// If zipEntryPath is specified, look for files in that specific directory first
|
// If zipEntryPath is specified, look for files in that specific directory first
|
||||||
if ($zipEntryPath) {
|
if ($zipEntryPath) {
|
||||||
|
|
@ -374,8 +327,6 @@ class PluginImportService
|
||||||
|
|
||||||
if (File::exists($targetDir.'/shared.liquid')) {
|
if (File::exists($targetDir.'/shared.liquid')) {
|
||||||
$sharedLiquidPath = $targetDir.'/shared.liquid';
|
$sharedLiquidPath = $targetDir.'/shared.liquid';
|
||||||
} elseif (File::exists($targetDir.'/shared.blade.php')) {
|
|
||||||
$sharedBladePath = $targetDir.'/shared.blade.php';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,18 +342,15 @@ class PluginImportService
|
||||||
|
|
||||||
if (File::exists($targetDir.'/src/shared.liquid')) {
|
if (File::exists($targetDir.'/src/shared.liquid')) {
|
||||||
$sharedLiquidPath = $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 we found the required files in the target directory, return them
|
||||||
if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
|
if ($settingsYamlPath && $fullLiquidPath) {
|
||||||
return [
|
return [
|
||||||
'settingsYamlPath' => $settingsYamlPath,
|
'settingsYamlPath' => $settingsYamlPath,
|
||||||
'fullLiquidPath' => $fullLiquidPath,
|
'fullLiquidPath' => $fullLiquidPath,
|
||||||
'sharedLiquidPath' => $sharedLiquidPath,
|
'sharedLiquidPath' => $sharedLiquidPath,
|
||||||
'sharedBladePath' => $sharedBladePath,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -419,11 +367,9 @@ class PluginImportService
|
||||||
$fullLiquidPath = $tempDir.'/src/full.blade.php';
|
$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')) {
|
if (File::exists($tempDir.'/src/shared.liquid')) {
|
||||||
$sharedLiquidPath = $tempDir.'/src/shared.liquid';
|
$sharedLiquidPath = $tempDir.'/src/shared.liquid';
|
||||||
} elseif (File::exists($tempDir.'/src/shared.blade.php')) {
|
|
||||||
$sharedBladePath = $tempDir.'/src/shared.blade.php';
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Search for the files in the extracted directory structure
|
// Search for the files in the extracted directory structure
|
||||||
|
|
@ -440,24 +386,20 @@ class PluginImportService
|
||||||
$fullLiquidPath = $filepath;
|
$fullLiquidPath = $filepath;
|
||||||
} elseif ($filename === 'shared.liquid') {
|
} elseif ($filename === 'shared.liquid') {
|
||||||
$sharedLiquidPath = $filepath;
|
$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
|
// Check if shared.liquid exists in the same directory as full.liquid
|
||||||
if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath && ! $sharedBladePath) {
|
if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath) {
|
||||||
$fullLiquidDir = dirname((string) $fullLiquidPath);
|
$fullLiquidDir = dirname((string) $fullLiquidPath);
|
||||||
if (File::exists($fullLiquidDir.'/shared.liquid')) {
|
if (File::exists($fullLiquidDir.'/shared.liquid')) {
|
||||||
$sharedLiquidPath = $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,
|
// 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
|
// 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
|
// If the files are in the root of the ZIP, create a src folder and move them there
|
||||||
$srcDir = dirname((string) $settingsYamlPath);
|
$srcDir = dirname((string) $settingsYamlPath);
|
||||||
|
|
||||||
|
|
@ -468,25 +410,17 @@ class PluginImportService
|
||||||
|
|
||||||
// Copy the files to the src directory
|
// Copy the files to the src directory
|
||||||
File::copy($settingsYamlPath, $newSrcDir.'/settings.yml');
|
File::copy($settingsYamlPath, $newSrcDir.'/settings.yml');
|
||||||
|
File::copy($fullLiquidPath, $newSrcDir.'/full.liquid');
|
||||||
|
|
||||||
// Copy full.liquid or full.blade.php if it exists
|
// Copy shared.liquid 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
|
|
||||||
if ($sharedLiquidPath) {
|
if ($sharedLiquidPath) {
|
||||||
File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid');
|
File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid');
|
||||||
$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
|
// Update the paths
|
||||||
$settingsYamlPath = $newSrcDir.'/settings.yml';
|
$settingsYamlPath = $newSrcDir.'/settings.yml';
|
||||||
|
$fullLiquidPath = $newSrcDir.'/full.liquid';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -495,7 +429,6 @@ class PluginImportService
|
||||||
'settingsYamlPath' => $settingsYamlPath,
|
'settingsYamlPath' => $settingsYamlPath,
|
||||||
'fullLiquidPath' => $fullLiquidPath,
|
'fullLiquidPath' => $fullLiquidPath,
|
||||||
'sharedLiquidPath' => $sharedLiquidPath,
|
'sharedLiquidPath' => $sharedLiquidPath,
|
||||||
'sharedBladePath' => $sharedBladePath,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
// Comma‑separated list from .env, e.g. "10.0.0.0/8,172.16.0.0/12" or '*'
|
|
||||||
'proxies' => ($proxies = env('TRUSTED_PROXIES', '')) === '*' ? '*' : array_filter(explode(',', $proxies)),
|
|
||||||
];
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
// Find and handle duplicate (user_id, trmnlp_id) combinations
|
|
||||||
$duplicates = DB::table('plugins')
|
|
||||||
->select('user_id', 'trmnlp_id', DB::raw('COUNT(*) as count'))
|
|
||||||
->whereNotNull('trmnlp_id')
|
|
||||||
->groupBy('user_id', 'trmnlp_id')
|
|
||||||
->having('count', '>', 1)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
// For each duplicate combination, keep the first one (by id) and set others to null
|
|
||||||
foreach ($duplicates as $duplicate) {
|
|
||||||
$plugins = DB::table('plugins')
|
|
||||||
->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;
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::table('plugins')
|
|
||||||
->where('id', $plugin->id)
|
|
||||||
->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']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('plugins', function (Blueprint $table) {
|
|
||||||
$table->boolean('alias')->default(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('plugins', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('alias');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -478,6 +478,7 @@ HTML;
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
</flux:menu>
|
</flux:menu>
|
||||||
</flux:dropdown>
|
</flux:dropdown>
|
||||||
|
|
||||||
</flux:button.group>
|
</flux:button.group>
|
||||||
<flux:button.group>
|
<flux:button.group>
|
||||||
<flux:modal.trigger name="add-to-playlist">
|
<flux:modal.trigger name="add-to-playlist">
|
||||||
|
|
@ -487,10 +488,6 @@ HTML;
|
||||||
<flux:dropdown>
|
<flux:dropdown>
|
||||||
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
||||||
<flux:menu>
|
<flux:menu>
|
||||||
<flux:modal.trigger name="trmnlp-settings">
|
|
||||||
<flux:menu.item icon="cog">Recipe Settings</flux:menu.item>
|
|
||||||
</flux:modal.trigger>
|
|
||||||
<flux:menu.separator />
|
|
||||||
<flux:menu.item icon="document-duplicate" wire:click="duplicatePlugin">Duplicate Plugin</flux:menu.item>
|
<flux:menu.item icon="document-duplicate" wire:click="duplicatePlugin">Duplicate Plugin</flux:menu.item>
|
||||||
<flux:modal.trigger name="delete-plugin">
|
<flux:modal.trigger name="delete-plugin">
|
||||||
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
||||||
|
|
@ -649,8 +646,6 @@ HTML;
|
||||||
</div>
|
</div>
|
||||||
</flux:modal>
|
</flux:modal>
|
||||||
|
|
||||||
<livewire:plugins.recipes.settings :plugin="$plugin" />
|
|
||||||
|
|
||||||
<livewire:plugins.config-modal :plugin="$plugin" />
|
<livewire:plugins.config-modal :plugin="$plugin" />
|
||||||
|
|
||||||
<div class="mt-5 mb-5">
|
<div class="mt-5 mb-5">
|
||||||
|
|
@ -739,7 +734,7 @@ HTML;
|
||||||
@endif
|
@endif
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:modal.trigger name="configuration-modal">
|
<flux:modal.trigger name="configuration-modal">
|
||||||
<flux:button icon="variable" class="block mt-1 w-full">Configuration Fields</flux:button>
|
<flux:button icon="cog" class="block mt-1 w-full">Configuration</flux:button>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use App\Models\Plugin;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
use Livewire\Volt\Component;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This component contains the TRMNL Plugin Settings modal
|
|
||||||
*/
|
|
||||||
new class extends Component {
|
|
||||||
public Plugin $plugin;
|
|
||||||
public string|null $trmnlp_id = null;
|
|
||||||
public string|null $uuid = null;
|
|
||||||
public bool $alias = false;
|
|
||||||
|
|
||||||
public int $resetIndex = 0;
|
|
||||||
|
|
||||||
public function mount(): void
|
|
||||||
{
|
|
||||||
$this->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");
|
|
||||||
}
|
|
||||||
};?>
|
|
||||||
|
|
||||||
<flux:modal name="trmnlp-settings" class="min-w-[400px] space-y-6">
|
|
||||||
<div wire:key="trmnlp-settings-form-{{ $resetIndex }}" class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<flux:heading size="lg">Recipe Settings</flux:heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form wire:submit="saveTrmnlpId">
|
|
||||||
<div class="grid gap-6">
|
|
||||||
{{-- <flux:input label="UUID" wire:model="uuid" readonly copyable /> --}}
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>TRMNLP Recipe ID</flux:label>
|
|
||||||
<flux:input
|
|
||||||
wire:model="trmnlp_id"
|
|
||||||
placeholder="TRMNL Recipe ID"
|
|
||||||
/>
|
|
||||||
<flux:error name="trmnlp_id" />
|
|
||||||
<flux:description>Recipe ID in the TRMNL Recipe Catalog. If set, it can be used with <code>trmnlp</code>. </flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
<flux:field>
|
|
||||||
<flux:checkbox wire:model.live="alias" label="Enable Alias" />
|
|
||||||
<flux:description>Enable a public alias URL for this recipe.</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
|
|
||||||
@if($alias)
|
|
||||||
<flux:field>
|
|
||||||
<flux:label>Alias URL</flux:label>
|
|
||||||
<flux:input
|
|
||||||
value="{{ $this->aliasUrl }}"
|
|
||||||
readonly
|
|
||||||
copyable
|
|
||||||
/>
|
|
||||||
<flux:description>Use this URL to access the recipe image directly. Add <code>?device-model=name</code> to specify a device model.</flux:description>
|
|
||||||
</flux:field>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2 mt-4">
|
|
||||||
<flux:spacer/>
|
|
||||||
<flux:modal.close>
|
|
||||||
<flux:button variant="ghost">Cancel</flux:button>
|
|
||||||
</flux:modal.close>
|
|
||||||
<flux:button type="submit" variant="primary">Save</flux:button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</flux:modal>
|
|
||||||
|
|
@ -613,7 +613,7 @@ Route::post('plugin_settings/{uuid}/image', function (Request $request, string $
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new UUID for each image upload to prevent device caching
|
// 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;
|
$filename = $imageUuid.'.'.$extension;
|
||||||
$path = 'images/generated/'.$filename;
|
$path = 'images/generated/'.$filename;
|
||||||
|
|
||||||
|
|
@ -678,90 +678,3 @@ Route::post('plugin_settings/{trmnlp_id}/archive', function (Request $request, s
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
})->middleware('auth:sanctum');
|
})->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');
|
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use App\Models\Plugin;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Livewire\Volt\Volt;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
|
||||||
|
|
||||||
test('recipe settings can save trmnlp_id', function (): void {
|
|
||||||
$user = User::factory()->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();
|
|
||||||
});
|
|
||||||
|
|
@ -83,34 +83,19 @@ it('throws exception for invalid zip file', function (): void {
|
||||||
->toThrow(Exception::class, 'Could not open the ZIP file.');
|
->toThrow(Exception::class, 'Could not open the ZIP file.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws exception for missing settings.yml', function (): void {
|
it('throws exception for missing required files', 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 {
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$zipContent = createMockZipFile([
|
$zipContent = createMockZipFile([
|
||||||
'src/settings.yml' => getValidSettingsYaml(),
|
'src/settings.yml' => getValidSettingsYaml(),
|
||||||
// Missing all template files
|
// Missing full.liquid
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
|
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
|
||||||
|
|
||||||
$pluginImportService = new PluginImportService();
|
$pluginImportService = new PluginImportService();
|
||||||
expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
|
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 {
|
it('sets default values when settings are missing', function (): void {
|
||||||
|
|
@ -446,7 +431,7 @@ it('throws exception when multi_string default value contains a comma', function
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
// YAML with a comma in the 'default' field of a multi_string
|
// YAML with a comma in the 'default' field of a multi_string
|
||||||
$invalidYaml = <<<'YAML'
|
$invalidYaml = <<<YAML
|
||||||
name: Test Plugin
|
name: Test Plugin
|
||||||
refresh_interval: 30
|
refresh_interval: 30
|
||||||
strategy: static
|
strategy: static
|
||||||
|
|
@ -468,14 +453,14 @@ YAML;
|
||||||
$pluginImportService = new PluginImportService();
|
$pluginImportService = new PluginImportService();
|
||||||
|
|
||||||
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
||||||
->toThrow(Exception::class, 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.');
|
->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 {
|
it('throws exception when multi_string placeholder contains a comma', function (): void {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
// YAML with a comma in the 'placeholder' field
|
// YAML with a comma in the 'placeholder' field
|
||||||
$invalidYaml = <<<'YAML'
|
$invalidYaml = <<<YAML
|
||||||
name: Test Plugin
|
name: Test Plugin
|
||||||
refresh_interval: 30
|
refresh_interval: 30
|
||||||
strategy: static
|
strategy: static
|
||||||
|
|
@ -498,45 +483,7 @@ YAML;
|
||||||
$pluginImportService = new PluginImportService();
|
$pluginImportService = new PluginImportService();
|
||||||
|
|
||||||
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
||||||
->toThrow(Exception::class, 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.');
|
->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' => '<div class="shared-content">{{ data.title }}</div>',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$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('<div class="view view--{{ size }}">')
|
|
||||||
->and($plugin->render_markup)->toContain('<div class="shared-content">{{ data.title }}</div>');
|
|
||||||
});
|
|
||||||
|
|
||||||
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' => '<div class="shared-content">{{ $data["title"] }}</div>',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$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('<div class="shared-content">{{ $data["title"] }}</div>')
|
|
||||||
->and($plugin->render_markup)->not->toContain('<div class="view view--{{ size }}">');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue