diff --git a/app/Console/Commands/MashupCreateCommand.php b/app/Console/Commands/MashupCreateCommand.php new file mode 100644 index 0000000..4c1e0cc --- /dev/null +++ b/app/Console/Commands/MashupCreateCommand.php @@ -0,0 +1,172 @@ +selectDevice(); + if (! $device) { + return 1; + } + + // Select playlist + $playlist = $this->selectPlaylist($device); + if (! $playlist) { + return 1; + } + + // Select mashup layout + $layout = $this->selectLayout(); + if (! $layout) { + return 1; + } + + // Get mashup name + $name = $this->getMashupName(); + if (! $name) { + return 1; + } + + // Select plugins + $plugins = $this->selectPlugins($layout); + if ($plugins->isEmpty()) { + return 1; + } + + $maxOrder = $playlist->items()->max('order') ?? 0; + + // Create playlist item with mashup + PlaylistItem::createMashup( + playlist: $playlist, + layout: $layout, + pluginIds: $plugins->pluck('id')->toArray(), + name: $name, + order: $maxOrder + 1 + ); + + $this->info('Mashup created successfully!'); + + return 0; + } + + protected function selectDevice(): ?Device + { + $devices = Device::all(); + if ($devices->isEmpty()) { + $this->error('No devices found. Please create a device first.'); + + return null; + } + + $deviceId = select( + label: 'Select a device', + options: $devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray() + ); + + return $devices->firstWhere('id', $deviceId); + } + + protected function selectPlaylist(Device $device): ?Playlist + { + $playlists = $device->playlists; + if ($playlists->isEmpty()) { + $this->error('No playlists found for this device. Please create a playlist first.'); + + return null; + } + + $playlistId = select( + label: 'Select a playlist', + options: $playlists->mapWithKeys(fn ($playlist) => [$playlist->id => $playlist->name])->toArray() + ); + + return $playlists->firstWhere('id', $playlistId); + } + + protected function selectLayout(): ?string + { + return select( + label: 'Select a layout', + options: PlaylistItem::getAvailableLayouts() + ); + } + + protected function getMashupName(): ?string + { + return text( + label: 'Enter a name for this mashup', + required: true, + default: 'Mashup', + validate: fn (string $value) => match (true) { + strlen($value) < 1 => 'The name must be at least 2 characters.', + strlen($value) > 50 => 'The name must not exceed 50 characters.', + default => null, + } + ); + } + + protected function selectPlugins(string $layout): Collection + { + $requiredCount = PlaylistItem::getRequiredPluginCountForLayout($layout); + + $plugins = Plugin::all(); + if ($plugins->isEmpty()) { + $this->error('No plugins found. Please create some plugins first.'); + + return collect(); + } + + $selectedPlugins = collect(); + $availablePlugins = $plugins->mapWithKeys(fn ($plugin) => [$plugin->id => $plugin->name])->toArray(); + + for ($i = 0; $i < $requiredCount; $i++) { + $position = match ($i) { + 0 => 'first', + 1 => 'second', + 2 => 'third', + 3 => 'fourth', + default => ($i + 1).'th' + }; + + $pluginId = select( + label: "Select the $position plugin", + options: $availablePlugins + ); + + $selectedPlugins->push($plugins->firstWhere('id', $pluginId)); + unset($availablePlugins[$pluginId]); + } + + return $selectedPlugins; + } +} diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php index 4eba877..d8913fc 100644 --- a/app/Models/PlaylistItem.php +++ b/app/Models/PlaylistItem.php @@ -15,6 +15,7 @@ class PlaylistItem extends Model protected $casts = [ 'is_active' => 'boolean', 'last_displayed_at' => 'datetime', + 'mashup' => 'json', ]; public function playlist(): BelongsTo @@ -26,4 +27,179 @@ class PlaylistItem extends Model { return $this->belongsTo(Plugin::class); } + + /** + * Check if this playlist item is a mashup + */ + public function isMashup(): bool + { + return ! is_null($this->mashup); + } + + /** + * Get the mashup name if this is a mashup + */ + public function getMashupName(): ?string + { + return $this->mashup['mashup_name'] ?? null; + } + + /** + * Get the mashup layout type if this is a mashup + */ + public function getMashupLayoutType(): ?string + { + return $this->mashup['mashup_layout'] ?? null; + } + + /** + * Get all plugin IDs for this mashup + */ + public function getMashupPluginIds(): array + { + return $this->mashup['plugin_ids'] ?? []; + } + + /** + * Get the number of plugins required for the current layout + */ + public function getRequiredPluginCount(): int + { + if (! $this->isMashup()) { + return 1; + } + + return match ($this->getMashupLayoutType()) { + '1Lx1R', '1Tx1B' => 2, // Left-Right or Top-Bottom split + '1Lx2R', '2Lx1R', '2Tx1B', '1Tx2B' => 3, // Two on one side, one on other + '2x2' => 4, // Quadrant + default => 1, + }; + } + + /** + * Get the layout type (horizontal, vertical, or grid) + */ + public function getLayoutType(): string + { + if (! $this->isMashup()) { + return 'single'; + } + + return match ($this->getMashupLayoutType()) { + '1Lx1R', '1Lx2R', '2Lx1R' => 'vertical', + '1Tx1B', '2Tx1B', '1Tx2B' => 'horizontal', + '2x2' => 'grid', + default => 'single', + }; + } + + /** + * Get the layout size for a plugin based on its position + */ + public function getLayoutSize(int $position = 0): string + { + if (! $this->isMashup()) { + return 'full'; + } + + return match ($this->getMashupLayoutType()) { + '1Lx1R' => 'half_vertical', // Both sides are single plugins + '1Tx1B' => 'half_horizontal', // Both sides are single plugins + '2Lx1R' => match ($position) { + 0, 1 => 'quadrant', // Left side has 2 plugins + 2 => 'half_vertical', // Right side has 1 plugin + default => 'full' + }, + '1Lx2R' => match ($position) { + 0 => 'half_vertical', // Left side has 1 plugin + 1, 2 => 'quadrant', // Right side has 2 plugins + default => 'full' + }, + '2Tx1B' => match ($position) { + 0, 1 => 'quadrant', // Top side has 2 plugins + 2 => 'half_horizontal', // Bottom side has 1 plugin + default => 'full' + }, + '1Tx2B' => match ($position) { + 0 => 'half_horizontal', // Top side has 1 plugin + 1, 2 => 'quadrant', // Bottom side has 2 plugins + default => 'full' + }, + '2x2' => 'quadrant', // All positions are quadrants + default => 'full' + }; + } + + /** + * Render all plugins with appropriate layout + */ + public function render(): string + { + if (! $this->isMashup()) { + return view('trmnl-layouts.single', [ + 'slot' => $this->plugin->render('full', false), + ])->render(); + } + + $pluginMarkups = []; + $plugins = Plugin::whereIn('id', $this->getMashupPluginIds())->get(); + + foreach ($plugins as $index => $plugin) { + $size = $this->getLayoutSize($index); + $pluginMarkups[] = $plugin->render($size, false); + } + + return view('trmnl-layouts.mashup', [ + 'mashupLayout' => $this->getMashupLayoutType(), + 'slot' => implode('', $pluginMarkups), + ])->render(); + } + + /** + * Available mashup layouts with their descriptions + */ + public static function getAvailableLayouts(): array + { + return [ + '1Lx1R' => '1 Left - 1 Right (2 plugins)', + '1Lx2R' => '1 Left - 2 Right (3 plugins)', + '2Lx1R' => '2 Left - 1 Right (3 plugins)', + '1Tx1B' => '1 Top - 1 Bottom (2 plugins)', + '2Tx1B' => '2 Top - 1 Bottom (3 plugins)', + '1Tx2B' => '1 Top - 2 Bottom (3 plugins)', + '2x2' => 'Quadrant (4 plugins)', + ]; + } + + /** + * Get the required number of plugins for a given layout + */ + public static function getRequiredPluginCountForLayout(string $layout): int + { + return match ($layout) { + '1Lx1R', '1Tx1B' => 2, + '1Lx2R', '2Lx1R', '2Tx1B', '1Tx2B' => 3, + '2x2' => 4, + default => 1, + }; + } + + /** + * Create a new mashup with the given layout and plugins + */ + public static function createMashup(Playlist $playlist, string $layout, array $pluginIds, string $name, $order): self + { + return static::create([ + 'playlist_id' => $playlist->id, + 'plugin_id' => $pluginIds[0], // First plugin is the main plugin + 'mashup' => [ + 'mashup_layout' => $layout, + 'mashup_name' => $name, + 'plugin_ids' => $pluginIds, + ], + 'is_active' => true, + 'order' => $order, + ]); + } } diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index fa5dbd6..30c5938 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; @@ -65,4 +66,38 @@ class Plugin extends Model ]); } } + + /** + * Render the plugin's markup + */ + public function render(string $size = 'full', bool $standalone = true): string + { + if ($this->render_markup) { + if ($standalone) { + return view('trmnl-layouts.single', [ + 'slot' => Blade::render($this->render_markup, ['size' => $size, 'data' => $this->data_payload]), + ])->render(); + } + + return Blade::render($this->render_markup, ['size' => $size, 'data' => $this->data_payload]); + } + + if ($this->render_markup_view) { + if ($standalone) { + return view('trmnl-layouts.single', [ + 'slot' => view($this->render_markup_view, [ + 'size' => $size, + 'data' => $this->data_payload, + ])->render(), + ])->render(); + } else { + return view($this->render_markup_view, [ + 'size' => $size, + 'data' => $this->data_payload, + ])->render(); + } + } + + return '

No render markup yet defined for this plugin.

'; + } } diff --git a/composer.json b/composer.json index f890ca4..a995526 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "require": { "php": "^8.2", "ext-imagick": "*", - "bnussbau/laravel-trmnl-blade": "1.0.*", + "bnussbau/laravel-trmnl-blade": "1.1.*", "intervention/image": "^3.11", "keepsuit/laravel-liquid": "^0.5.2", "laravel/framework": "^12.1", diff --git a/composer.lock b/composer.lock index 5661d09..c2d89f0 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": "abffc30d88cd79c676be64cb30b8cfb1", + "content-hash": "63075a49e79059d81034658d332d9dd8", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.343.24", + "version": "3.344.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5308e15ca92655906d04d5945613ab9046b4f79f" + "reference": "3a6aaaea75f4605f89aa57ad63b9a077bf01e1e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5308e15ca92655906d04d5945613ab9046b4f79f", - "reference": "5308e15ca92655906d04d5945613ab9046b4f79f", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3a6aaaea75f4605f89aa57ad63b9a077bf01e1e5", + "reference": "3a6aaaea75f4605f89aa57ad63b9a077bf01e1e5", "shasum": "" }, "require": { @@ -153,22 +153,22 @@ "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.343.24" + "source": "https://github.com/aws/aws-sdk-php/tree/3.344.2" }, - "time": "2025-06-03T18:04:18+00:00" + "time": "2025-06-06T18:14:42+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", - "version": "1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "9e23a83c2dbb33286d0940c5282eaa8b142cd218" + "reference": "f57a7e2f855d882364e9ce2bb1a239b9387cdd75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/9e23a83c2dbb33286d0940c5282eaa8b142cd218", - "reference": "9e23a83c2dbb33286d0940c5282eaa8b142cd218", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/f57a7e2f855d882364e9ce2bb1a239b9387cdd75", + "reference": "f57a7e2f855d882364e9ce2bb1a239b9387cdd75", "shasum": "" }, "require": { @@ -223,7 +223,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/1.0.0" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/1.1.0" }, "funding": [ { @@ -235,7 +235,7 @@ "type": "custom" } ], - "time": "2025-06-04T15:52:22+00:00" + "time": "2025-06-10T13:05:01+00:00" }, { "name": "brick/math", @@ -7571,16 +7571,16 @@ }, { "name": "laravel/pail", - "version": "v1.2.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", "shasum": "" }, "require": { @@ -7600,7 +7600,7 @@ "orchestra/testbench-core": "^8.13|^9.0|^10.0", "pestphp/pest": "^2.20|^3.0", "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, "type": "library", @@ -7636,6 +7636,7 @@ "description": "Easily delve into your Laravel application's log files directly from the command line.", "homepage": "https://github.com/laravel/pail", "keywords": [ + "dev", "laravel", "logs", "php", @@ -7645,7 +7646,7 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-01-28T15:15:15+00:00" + "time": "2025-06-05T13:55:57+00:00" }, { "name": "laravel/pint", diff --git a/database/factories/PlaylistItemFactory.php b/database/factories/PlaylistItemFactory.php index 9045e58..a7a1d97 100644 --- a/database/factories/PlaylistItemFactory.php +++ b/database/factories/PlaylistItemFactory.php @@ -17,6 +17,7 @@ class PlaylistItemFactory extends Factory return [ 'playlist_id' => Playlist::factory(), 'plugin_id' => Plugin::factory(), + 'mashup' => null, 'order' => $this->faker->numberBetween(0, 100), 'is_active' => $this->faker->boolean(80), // 80% chance of being active 'last_displayed_at' => null, diff --git a/database/migrations/2025_06_10_211056_add_mashup_to_playlist_items_table.php b/database/migrations/2025_06_10_211056_add_mashup_to_playlist_items_table.php new file mode 100644 index 0000000..a8a61d5 --- /dev/null +++ b/database/migrations/2025_06_10_211056_add_mashup_to_playlist_items_table.php @@ -0,0 +1,22 @@ +json('mashup')->nullable(); + }); + } + + public function down(): void + { + Schema::table('playlist_items', function (Blueprint $table) { + $table->dropColumn('mashup'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 66b5d5f..be688db 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -34,6 +34,7 @@ class DatabaseSeeder extends Seeder $this->call([ ExampleRecipesSeeder::class, + MashupPocSeeder::class, ]); } } diff --git a/database/seeders/MashupPocSeeder.php b/database/seeders/MashupPocSeeder.php new file mode 100644 index 0000000..35060f8 --- /dev/null +++ b/database/seeders/MashupPocSeeder.php @@ -0,0 +1,50 @@ + 1, + 'name' => 'Mashup Test Playlist', + 'is_active' => true, + ]); + + // Create a playlist item with 1Tx1B layout using the new JSON structure + PlaylistItem::createMashup( + playlist: $playlist, + layout: '1Tx1B', + pluginIds: [2, 3], // Top and bottom plugins + name: 'Mashup 1Tx1B', + order: 1 + ); + + // Create another playlist item with 2x2 layout + PlaylistItem::createMashup( + playlist: $playlist, + layout: '1Lx1R', + pluginIds: [2, 6], // All four quadrants + name: 'Mashup Quadrant', + order: 2 + ); + + // Create a single plugin item (no mashup) + PlaylistItem::create([ + 'playlist_id' => $playlist->id, + 'plugin_id' => 1, + 'mashup' => null, + 'is_active' => true, + 'order' => 3, + ]); + } +} diff --git a/resources/views/flux/icon/mashup-1Lx1R.blade.php b/resources/views/flux/icon/mashup-1Lx1R.blade.php new file mode 100644 index 0000000..75d1a3d --- /dev/null +++ b/resources/views/flux/icon/mashup-1Lx1R.blade.php @@ -0,0 +1,39 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + diff --git a/resources/views/flux/icon/mashup-1Lx2R.blade.php b/resources/views/flux/icon/mashup-1Lx2R.blade.php new file mode 100644 index 0000000..5794416 --- /dev/null +++ b/resources/views/flux/icon/mashup-1Lx2R.blade.php @@ -0,0 +1,40 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/flux/icon/mashup-1Tx1B.blade.php b/resources/views/flux/icon/mashup-1Tx1B.blade.php new file mode 100644 index 0000000..c392742 --- /dev/null +++ b/resources/views/flux/icon/mashup-1Tx1B.blade.php @@ -0,0 +1,39 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + diff --git a/resources/views/flux/icon/mashup-1x1.blade.php b/resources/views/flux/icon/mashup-1x1.blade.php new file mode 100644 index 0000000..398b3cf --- /dev/null +++ b/resources/views/flux/icon/mashup-1x1.blade.php @@ -0,0 +1,38 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + diff --git a/resources/views/flux/icon/mashup-2Lx1R.blade.php b/resources/views/flux/icon/mashup-2Lx1R.blade.php new file mode 100644 index 0000000..9f3a630 --- /dev/null +++ b/resources/views/flux/icon/mashup-2Lx1R.blade.php @@ -0,0 +1,40 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/flux/icon/mashup-2x2.blade.php b/resources/views/flux/icon/mashup-2x2.blade.php new file mode 100644 index 0000000..71077ca --- /dev/null +++ b/resources/views/flux/icon/mashup-2x2.blade.php @@ -0,0 +1,41 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + diff --git a/resources/views/livewire/playlists/index.blade.php b/resources/views/livewire/playlists/index.blade.php index 5f5e641..b96814b 100644 --- a/resources/views/livewire/playlists/index.blade.php +++ b/resources/views/livewire/playlists/index.blade.php @@ -195,7 +195,19 @@ new class extends Component { @foreach($playlist->items->sortBy('order') as $item) - {{ $item->plugin->name }} + @if($item->isMashup()) +
+
+
{{ $item->getMashupName() }}
+
+ + {{ collect($item->getMashupPluginIds())->map(fn($id) => App\Models\Plugin::find($id)->name)->join(' | ') }} +
+
+
+ @else +
{{ $item->plugin->name }}
+ @endif
- Delete {{ $item->plugin->name }}? -

This will remove this item from the playlist.

+ + @if($item->isMashup()) + Delete {{ $item->getMashupName() }}? + @else + Delete {{ $item->plugin->name }}? + @endif + +

+ @if($item->isMashup()) + This will remove this mashup from the playlist. + @else + This will remove this item from the playlist. + @endif +

diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index 08ee441..94df0c0 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -150,6 +150,7 @@ new class extends Component { 'plugin_id' => $this->plugin->id, 'order' => $maxOrder + 1, ]); + } $this->reset(['checked_devices', 'playlist_name', 'selected_weekdays', 'active_from', 'active_until', 'selected_playlist']); @@ -200,16 +201,12 @@ HTML; HTML; } - public function renderPreview(): void + public function renderPreview($size = 'full'): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); try { - if ($this->plugin->render_markup_view) { - $previewMarkup = view($this->plugin->render_markup_view, ['data' => $this->plugin->data_payload])->render(); - } else { - $previewMarkup = Blade::render($this->plugin->render_markup, ['data' => $this->plugin->data_payload]); - } + $previewMarkup = $this->plugin->render($size); $this->dispatch('preview-updated', preview: $previewMarkup); } catch (\Exception $e) { $this->dispatch('preview-error', message: $e->getMessage()); @@ -237,6 +234,27 @@ HTML; Preview + + + + + Half-Horizontal + + + + + Half-Vertical + + + + + Quadrant + + + + + + Add to Playlist @@ -475,7 +493,7 @@ HTML; @script diff --git a/resources/views/recipes/day-in-history.liquid b/resources/views/recipes/day-in-history.liquid index fda23b2..c79f462 100644 --- a/resources/views/recipes/day-in-history.liquid +++ b/resources/views/recipes/day-in-history.liquid @@ -1,79 +1,57 @@ - - - - - - - - - plugin - - - -
-
- - -
-
-
-
Events
- {% for event in data.metadata.events %} -
-
- -
-
- {{ data.events[event].year }} - {{ data.events[event].text }} -
+
+
+
+
+
Events
+ {% for event in data.metadata.events %} +
+
+
- {% endfor %} -
-
-
Births
- {% for birth in data.metadata.births %} -
-
- -
-
- {{ data.births[birth].year }} - {{ data.births[birth].text }} -
+
+ {{ data.events[event].year }} + {{ data.events[event].text }}
- {% endfor %} -
Deaths
- {% for death in data.metadata.deaths %} -
-
- -
-
- {{ data.deaths[death].year }} - {{ data.deaths[death].text }} -
+
+ {% endfor %} +
+
+
Births
+ {% for birth in data.metadata.births %} +
+
+
- {% endfor %} -
+
+ {{ data.births[birth].year }} + {{ data.births[birth].text }} +
+
+ {% endfor %} +
Deaths
+ {% for death in data.metadata.deaths %} +
+
+ +
+
+ {{ data.deaths[death].year }} + {{ data.deaths[death].text }} +
+
+ {% endfor %}
-
- - This Day in History (Wikipedia) +
+
+ + This Day in History (Wikipedia) - {{ data.metadata.current_date }} -
+ {{ data.metadata.current_date }}
- - diff --git a/resources/views/recipes/home-assistant.blade.php b/resources/views/recipes/home-assistant.blade.php index 26ad095..6322c07 100644 --- a/resources/views/recipes/home-assistant.blade.php +++ b/resources/views/recipes/home-assistant.blade.php @@ -4,7 +4,8 @@ }); @endphp - +@props(['size' => 'full']) + @if($weatherEntity) diff --git a/resources/views/recipes/pollen-forecast-eu.liquid b/resources/views/recipes/pollen-forecast-eu.liquid index fc96494..f34414a 100644 --- a/resources/views/recipes/pollen-forecast-eu.liquid +++ b/resources/views/recipes/pollen-forecast-eu.liquid @@ -1,182 +1,167 @@ - - - - - - - - - - - plugin - - - -
-
-
- -
-
-
-
- {{ data.current.birch_pollen }} - grains/m³ - Birch -
-
-
-
-
- {{ data.current.grass_pollen }} - grains/m³ - Grass -
-
-
-
-
- {{ data.current.alder_pollen }} - grains/m³ - Alder -
-
-
-
-
- {{ data.current.mugwort_pollen }} - grains/m³ - Mugwort -
-
-
-
-
- {{ data.current.ragweed_pollen }} - grains/m³ - Ragweed -
+ + + +
+
+ +
+
+
+
+ {{ data.current.birch_pollen }} + grains/m³ + Birch +
+
+
+
+
+ {{ data.current.grass_pollen }} + grains/m³ + Grass +
+
+
+
+
+ {{ data.current.alder_pollen }} + grains/m³ + Alder +
+
+
+
+
+ {{ data.current.mugwort_pollen }} + grains/m³ + Mugwort +
+
+
+
+
+ {{ data.current.ragweed_pollen }} + grains/m³ + Ragweed
- - -
-
- - Pollen Forecast Vienna - Data provided by: Open-Meteo.com -
+ +
- +
+ + + + Pollen Forecast Vienna + Data provided by: Open-Meteo.com
- - + + +
diff --git a/resources/views/recipes/sunrise-sunset.liquid b/resources/views/recipes/sunrise-sunset.liquid index 3622f17..3ae8eef 100644 --- a/resources/views/recipes/sunrise-sunset.liquid +++ b/resources/views/recipes/sunrise-sunset.liquid @@ -1,44 +1,27 @@ - - - - - - - - - plugin - - - -
-
-
- -
- -
-
- Sunrise - - {{ data.today.sunrise }} - -
-
- -
-
- Sunset - - {{ data.today.sunset }} - -
+
+
+ +
+ +
+
+ Sunrise + + {{ data.today.sunrise }} +
- + +
+
+ Sunset + + {{ data.today.sunset }} + +
+
+
+ {% if size == 'full' or size == 'half_vertical' %}
@@ -58,12 +41,10 @@
-
- -
- Sunrise & Sunset -
+ {% endif %} +
+ +
+ Sunrise & Sunset
- - diff --git a/resources/views/recipes/train-monitor.blade.php b/resources/views/recipes/train-monitor.blade.php index e8ff38d..72fdeaf 100644 --- a/resources/views/recipes/train-monitor.blade.php +++ b/resources/views/recipes/train-monitor.blade.php @@ -1,4 +1,5 @@ - +@props(['size' => 'full']) + diff --git a/resources/views/recipes/train.blade.php b/resources/views/recipes/train.blade.php index d6f8dab..6521c89 100644 --- a/resources/views/recipes/train.blade.php +++ b/resources/views/recipes/train.blade.php @@ -1,4 +1,5 @@ - +@props(['size' => 'full']) + @@ -28,7 +29,7 @@ @if($journey['isCancelled']) - Cancelled + Ausfall @else diff --git a/resources/views/recipes/weather.blade.php b/resources/views/recipes/weather.blade.php index 65198d5..0d8045f 100644 --- a/resources/views/recipes/weather.blade.php +++ b/resources/views/recipes/weather.blade.php @@ -1,15 +1,18 @@ {{--@dump($data)--}} - +@props(['size' => 'full']) +
- +
-
-
+
+
- {{Arr::get($data, 'properties.timeseries.0.data.instant.details.air_temperature', 'N/A')}} + {{Arr::get($data, 'properties.timeseries.0.data.instant.details.air_temperature', 'N/A')}} Temperature
@@ -18,10 +21,11 @@
-{{-- --}} + {{-- --}}
- {{Arr::get($data, 'properties.timeseries.0.data.instant.details.wind_speed', 'N/A')}} + {{Arr::get($data, 'properties.timeseries.0.data.instant.details.wind_speed', 'N/A')}} Wind Speed (km/h)
@@ -29,7 +33,7 @@
-{{-- --}} + {{-- --}}
{{Arr::get($data, 'properties.timeseries.0.data.instant.details.relative_humidity', 'N/A')}}% @@ -40,10 +44,11 @@
-{{-- --}} + {{-- --}}
- {{Str::title(Arr::get($data, 'properties.timeseries.0.data.next_1_hours.summary.symbol_code', 'N/A'))}} + {{Str::title(Arr::get($data, 'properties.timeseries.0.data.next_1_hours.summary.symbol_code', 'N/A'))}} Right Now
diff --git a/resources/views/recipes/zen.blade.php b/resources/views/recipes/zen.blade.php index 3ea6909..5e01eac 100644 --- a/resources/views/recipes/zen.blade.php +++ b/resources/views/recipes/zen.blade.php @@ -1,9 +1,10 @@ {{--@dump($data)--}} - +@props(['size' => 'full']) +
{{$data[0]['a']}}
- @if (strlen($data[0]['q']) < 300) + @if (strlen($data[0]['q']) < 300 && $size != 'quadrant')

{{ $data[0]['q'] }}

@else

{{ $data[0]['q'] }}

diff --git a/resources/views/trmnl-layouts/mashup.blade.php b/resources/views/trmnl-layouts/mashup.blade.php new file mode 100644 index 0000000..d2890fa --- /dev/null +++ b/resources/views/trmnl-layouts/mashup.blade.php @@ -0,0 +1,8 @@ +@props(['mashupLayout' => '1Tx1B']) + + + + {{-- The slot is used to pass the content of the mashup --}} + {!! $slot !!} + + diff --git a/resources/views/trmnl-layouts/single.blade.php b/resources/views/trmnl-layouts/single.blade.php new file mode 100644 index 0000000..ec073e5 --- /dev/null +++ b/resources/views/trmnl-layouts/single.blade.php @@ -0,0 +1,3 @@ + + {!! $slot !!} + diff --git a/routes/api.php b/routes/api.php index 2fb5e3c..36a9650 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,11 +3,13 @@ use App\Jobs\GenerateScreenJob; use App\Models\Device; use App\Models\DeviceLog; +use App\Models\Plugin; use App\Models\User; use App\Services\ImageGenerationService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; Route::get('/display', function (Request $request) { @@ -46,13 +48,14 @@ Route::get('/display', function (Request $request) { 'last_refreshed_at' => now(), ]); - // Get current screen image from mirror device or continue if not available + // Get current screen image from a mirror device or continue if not available if (! $image_uuid = $device->mirrorDevice?->current_screen_image) { $refreshTimeOverride = null; // Skip if cloud proxy is enabled for the device if (! $device->proxy_cloud || $device->getNextPlaylistItem()) { $playlistItem = $device->getNextPlaylistItem(); - if ($playlistItem) { + + if ($playlistItem && ! $playlistItem->isMashup()) { $refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time; $plugin = $playlistItem->plugin; @@ -62,12 +65,7 @@ Route::get('/display', function (Request $request) { // Check and update stale data if needed if ($plugin->isDataStale() || $plugin->current_image == null) { $plugin->updateDataPayload(); - - if ($plugin->render_markup) { - $markup = Blade::render($plugin->render_markup, ['data' => $plugin->data_payload]); - } elseif ($plugin->render_markup_view) { - $markup = view($plugin->render_markup_view, ['data' => $plugin->data_payload])->render(); - } + $markup = $plugin->render(); GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup); } @@ -78,6 +76,28 @@ Route::get('/display', function (Request $request) { $playlistItem->update(['last_displayed_at' => now()]); $device->update(['current_screen_image' => $plugin->current_image]); } + } elseif ($playlistItem) { + $refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time; + + // Get all plugins for the mashup + $plugins = Plugin::whereIn('id', $playlistItem->getMashupPluginIds())->get(); + + foreach ($plugins as $plugin) { + // Reset cache if Devices with different dimensions exist + ImageGenerationService::resetIfNotCacheable($plugin); + if ($plugin->isDataStale() || $plugin->current_image == null) { + $plugin->updateDataPayload(); + } + } + + $markup = $playlistItem->render(); + GenerateScreenJob::dispatchSync($device->id, null, $markup); + + $device->refresh(); + + if ($device->current_screen_image != null) { + $playlistItem->update(['last_displayed_at' => now()]); + } } }