user()->plugins->contains($this->plugin), 403); $this->blade_code = $this->plugin->render_markup; $this->configuration_template = $this->plugin->configuration_template ?? []; $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : []; if ($this->plugin->render_markup_view) { try { $basePath = resource_path('views/' . str_replace('.', '/', $this->plugin->render_markup_view)); $paths = [ $basePath . '.blade.php', $basePath . '.liquid', ]; $this->view_content = null; foreach ($paths as $path) { if (file_exists($path)) { $this->view_content = file_get_contents($path); break; } } } catch (\Exception $e) { $this->view_content = null; } } else { $this->markup_code = $this->plugin->render_markup; $this->markup_language = $this->plugin->markup_language ?? 'blade'; } $this->fillformFields(); $this->data_payload_updated_at = $this->plugin->data_payload_updated_at; } public function fillFormFields(): void { $this->name = $this->plugin->name; $this->data_stale_minutes = $this->plugin->data_stale_minutes; $this->data_strategy = $this->plugin->data_strategy; $this->polling_url = $this->plugin->polling_url; $this->polling_verb = $this->plugin->polling_verb; $this->polling_header = $this->plugin->polling_header; $this->polling_body = $this->plugin->polling_body; $this->data_payload = json_encode($this->plugin->data_payload, JSON_PRETTY_PRINT); } public function saveMarkup(): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); $this->validate(); $this->plugin->update([ 'render_markup' => $this->markup_code ?? null, 'markup_language' => $this->markup_language ?? null ]); } protected array $rules = [ 'name' => 'required|string|max:255', 'data_stale_minutes' => 'required|integer|min:1', 'data_strategy' => 'required|string|in:polling,webhook,static', 'polling_url' => 'required_if:data_strategy,polling|nullable', 'polling_verb' => 'required|string|in:get,post', 'polling_header' => 'nullable|string|max:255', 'polling_body' => 'nullable|string', 'data_payload' => 'required_if:data_strategy,static|nullable|json', 'markup_code' => 'nullable|string', 'markup_language' => 'nullable|string|in:blade,liquid', 'checked_devices' => 'array', 'playlist_name' => 'required_if:selected_playlist,new|string|max:255', 'selected_weekdays' => 'nullable|array', 'active_from' => 'nullable|date_format:H:i', 'active_until' => 'nullable|date_format:H:i', 'selected_playlist' => 'nullable|string', ]; public function editSettings() { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); // Custom validation for polling_url with Liquid variable resolution $this->validatePollingUrl(); $validated = $this->validate(); $validated['data_payload'] = json_decode(Arr::get($validated,'data_payload'), true); $this->plugin->update($validated); } protected function validatePollingUrl(): void { if ($this->data_strategy === 'polling' && !empty($this->polling_url)) { try { $resolvedUrl = $this->plugin->resolveLiquidVariables($this->polling_url); if (!filter_var($resolvedUrl, FILTER_VALIDATE_URL)) { $this->addError('polling_url', 'The polling URL must be a valid URL after resolving configuration variables.'); } } catch (\Exception $e) { $this->addError('polling_url', 'Error resolving Liquid variables: ' . $e->getMessage() . $e->getPrevious()?->getMessage()); } } } public function updateData(): void { if ($this->plugin->data_strategy === 'polling') { try { $this->plugin->updateDataPayload(); $this->data_payload = json_encode($this->plugin->data_payload, JSON_PRETTY_PRINT); $this->data_payload_updated_at = $this->plugin->data_payload_updated_at; } catch (\Exception $e) { $this->dispatch('data-update-error', message: $e->getMessage() . $e->getPrevious()?->getMessage()); } } } public function getAvailablePlugins() { return auth()->user()->plugins()->where('id', '!=', $this->plugin->id)->get(); } public function getRequiredPluginCount(): int { if ($this->mashup_layout === 'full') { return 1; } return match ($this->mashup_layout) { '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, }; } public function addToPlaylist() { $this->validate([ 'checked_devices' => 'required|array|min:1', 'mashup_layout' => 'required|string', 'mashup_plugins' => 'required_if:mashup_layout,1Lx1R,1Lx2R,2Lx1R,1Tx1B,2Tx1B,1Tx2B,2x2|array', ]); // Validate that each checked device has a playlist selected foreach ($this->checked_devices as $deviceId) { if (!isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) { $this->addError('device_playlists.' . $deviceId, 'Please select a playlist for each device.'); return; } // If creating new playlist, validate required fields if ($this->device_playlists[$deviceId] === 'new') { if (!isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) { $this->addError('device_playlist_names.' . $deviceId, 'Playlist name is required when creating a new playlist.'); return; } } } foreach ($this->checked_devices as $deviceId) { $playlist = null; if ($this->device_playlists[$deviceId] === 'new') { // Create new playlist $playlist = \App\Models\Playlist::create([ 'device_id' => $deviceId, 'name' => $this->device_playlist_names[$deviceId], 'weekdays' => !empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null, 'active_from' => $this->device_active_from[$deviceId] ?? null, 'active_until' => $this->device_active_until[$deviceId] ?? null, ]); } else { $playlist = \App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); } // Add plugin to playlist $maxOrder = $playlist->items()->max('order') ?? 0; if ($this->mashup_layout === 'full') { $playlist->items()->create([ 'plugin_id' => $this->plugin->id, 'order' => $maxOrder + 1, ]); } else { // Create mashup $pluginIds = array_merge([$this->plugin->id], array_map('intval', $this->mashup_plugins)); \App\Models\PlaylistItem::createMashup( $playlist, $this->mashup_layout, $pluginIds, $this->plugin->name . ' Mashup', $maxOrder + 1 ); } } $this->reset([ 'checked_devices', 'device_playlists', 'device_playlist_names', 'device_weekdays', 'device_active_from', 'device_active_until', 'mashup_layout', 'mashup_plugins' ]); Flux::modal('add-to-playlist')->close(); } public function saveConfiguration() { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); $configurationValues = []; if (isset($this->configuration_template['custom_fields'])) { foreach ($this->configuration_template['custom_fields'] as $field) { $fieldKey = $field['keyname']; if (isset($this->configuration[$fieldKey])) { $configurationValues[$fieldKey] = $this->configuration[$fieldKey]; } } } $this->plugin->update([ 'configuration' => $configurationValues ]); Flux::modal('configuration-modal')->close(); } public function getDevicePlaylists($deviceId) { return \App\Models\Playlist::where('device_id', $deviceId)->get(); } public function hasAnyPlaylistSelected(): bool { foreach ($this->checked_devices as $deviceId) { if (isset($this->device_playlists[$deviceId]) && !empty($this->device_playlists[$deviceId])) { return true; } } return false; } public function getConfigurationValue($key, $default = null) { return $this->configuration[$key] ?? $default; } public function renderExample(string $example) { switch ($example) { case 'layoutTitle': $markup = $this->renderLayoutWithTitleBar(); break; case 'layout': $markup = $this->renderLayoutBlank(); break; default: $markup = '

Hello World!

'; break; } $this->markup_code = $markup; } public function renderLayoutWithTitleBar(): string { if ($this->markup_language === 'liquid') { return <<
TRMNL BYOS
HTML; } return << 'full']) HTML; } public function renderLayoutBlank(): string { if ($this->markup_language === 'liquid') { return <<
HTML; } return << 'full']) HTML; } public function renderPreview($size = 'full'): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); // 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 { $previewMarkup = $this->plugin->render($size); $this->dispatch('preview-updated', preview: $previewMarkup); } catch (LiquidException $e) { $this->dispatch('preview-error', message: $e->toLiquidErrorMessage()); } catch (\Exception $e) { $this->dispatch('preview-error', message: $e->getMessage()); } } public function deletePlugin(): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); $this->plugin->delete(); $this->redirect(route('plugins.index')); } 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); } } } ?>

{{$plugin->name}} Recipe

Preview Half-Horizontal Half-Vertical Quadrant Add to Playlist Delete Plugin
Add to Playlist
@foreach(auth()->user()->devices as $device) @endforeach
@if(count($checked_devices) > 0)
@foreach($checked_devices as $deviceId) @php $device = auth()->user()->devices->find($deviceId); @endphp
{{ $device->name }}
@foreach($this->getDevicePlaylists($deviceId) as $playlist) @endforeach
@if(isset($device_playlists[$deviceId]) && $device_playlists[$deviceId] === 'new')
@endif
@endforeach
@endif @if(count($checked_devices) > 0 && $this->hasAnyPlaylistSelected())
@if($mashup_layout !== 'full')
Mashup Slots
Main Plugin
@for($i = 0; $i < $this->getRequiredPluginCount() - 1; $i++)
Plugin {{ $i + 2 }}:
@foreach($this->getAvailablePlugins() as $availablePlugin) @endforeach
@endfor
@endif @endif
Add to Playlist
Delete {{ $plugin->name }}?

This will remove this plugin from your account.

Cancel Delete plugin
Preview {{ $plugin->name }}
Configuration Configure your plugin settings
@if(isset($configuration_template['custom_fields']) && is_array($configuration_template['custom_fields'])) @foreach($configuration_template['custom_fields'] as $field) @php $fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name']; $currentValue = $configuration[$fieldKey] ?? ''; @endphp
@if($field['field_type'] === 'author_bio') @continue @endif @if($field['field_type'] === 'copyable_webhook_url') @continue @endif @if($field['field_type'] === 'string' || $field['field_type'] === 'url') @elseif($field['field_type'] === 'text') @elseif($field['field_type'] === 'code') @elseif($field['field_type'] === 'password') @elseif($field['field_type'] === 'copyable') @elseif($field['field_type'] === 'time_zone') @foreach(timezone_identifiers_list() as $timezone) @endforeach @elseif($field['field_type'] === 'number') @elseif($field['field_type'] === 'boolean') @elseif($field['field_type'] === 'date') @elseif($field['field_type'] === 'time') @elseif($field['field_type'] === 'select') @if(isset($field['multiple']) && $field['multiple'] === true) @if(isset($field['options']) && is_array($field['options'])) @foreach($field['options'] as $option) @if(is_array($option)) @foreach($option as $label => $value) @endforeach @else @php $key = mb_strtolower(str_replace(' ', '_', $option)); @endphp @endif @endforeach @endif @else @if(isset($field['options']) && is_array($field['options'])) @foreach($field['options'] as $option) @if(is_array($option)) @foreach($option as $label => $value) @endforeach @else @php $key = mb_strtolower(str_replace(' ', '_', $option)); @endphp @endif @endforeach @endif @endif @elseif($field['field_type'] === 'xhrSelect') @if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey])) @foreach($xhrSelectOptions[$fieldKey] as $option) @if(is_array($option)) @if(isset($option['id']) && isset($option['name'])) {{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}} @else {{-- xhrSelect format: { 'Braves' => 123 } --}} @foreach($option as $label => $value) @endforeach @endif @else @endif @endforeach @endif @elseif($field['field_type'] === 'xhrSelectSearch')
{{ $field['name'] }} {{ $field['description'] ?? '' }} {{ $field['help_text'] ?? '' }} @if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue)) @if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey])) @foreach($xhrSelectOptions[$fieldKey] as $option) @if(is_array($option)) @if(isset($option['id']) && isset($option['name'])) {{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}} @else {{-- xhrSelect format: { 'Braves' => 123 } --}} @foreach($option as $label => $value) @endforeach @endif @else @endif @endforeach @endif @if(!empty($currentValue) && (!isset($xhrSelectOptions[$fieldKey]) || empty($xhrSelectOptions[$fieldKey]))) {{-- Show current value even if no options are loaded --}} @endif @endif
@else Field type "{{ $field['field_type'] }}" not yet supported @endif
@endforeach @endif
Save Configuration

Settings

@php $authorField = null; if (isset($configuration_template['custom_fields'])) { foreach ($configuration_template['custom_fields'] as $field) { if ($field['field_type'] === 'author_bio') { $authorField = $field; break; } } } @endphp @if($authorField)
{{ $authorField['description'] }}
@if(isset($authorField['github_url']) || isset($authorField['learn_more_url']) || isset($authorField['email_address']))
@if(isset($authorField['github_url'])) @php $githubUrl = $authorField['github_url']; $githubUsername = null; // Extract username from various GitHub URL formats if (preg_match('/github\.com\/([^\/\?]+)/', $githubUrl, $matches)) { $githubUsername = $matches[1]; } @endphp @if($githubUsername)@endif @endif @if(isset($authorField['learn_more_url'])) Learn More @endif @if(isset($authorField['github_url'])) @endif @if(isset($authorField['email_address'])) @endif
@endif
@endif @if(isset($configuration_template['custom_fields']) && !empty($configuration_template['custom_fields'])) @if($plugin->hasMissingRequiredConfigurationFields()) @endif
Configuration
@endif
@if($data_strategy === 'polling')
Fetch data now
@if($polling_verb === 'post')
@endif
@elseif($data_strategy === 'webhook')
@elseif($data_strategy === 'static') Enter static JSON data in the Data Payload field. @endif
Save
Data Payload @isset($this->data_payload_updated_at) {{ $this->data_payload_updated_at?->diffForHumans() ?? 'Never' }} @endisset

Markup

@if($plugin->render_markup_view)
Edit view {{ $plugin->render_markup_view }} to update.
@else
Template language
Getting started Responsive Layout with Title Bar Responsive Layout
@endif
@if(!$plugin->render_markup_view)
Save
@endif
@script @endscript