add features

* feat: autojoin toggle
* feat: auto add devices
* feat: proxy feature
* feat: support puppeteer in docker
* feat: toggle to activate cloud proxy
* feat: relay device information
* feat: relay logs to cloud
* feat: migrate on start
* feat: calculate battery state, wifi signal
* feat: eye candy for configure view
* feat: update via api
This commit is contained in:
Benjamin Nussbaum 2025-02-26 09:33:54 +01:00
parent d4eb832186
commit 715e6a2562
53 changed files with 1459 additions and 460 deletions

View file

@ -20,15 +20,17 @@
:current="request()->routeIs('devices')">
Devices
</flux:navbar.item>
<flux:navbar.item icon="puzzle-piece" href="{{ route('plugins.index') }}" wire:navigate
:current="request()->routeIs(['plugins.index', 'plugins.markup'])">
Plugins
</flux:navbar.item>
</flux:navbar>
<flux:spacer/>
{{-- <flux:navbar class="mr-1.5 space-x-0.5 py-0!">--}}
{{-- <flux:tooltip content="Add devices automatically that try to connect to this server" position="bottom">--}}
{{-- <flux:switch --}}{{-- wire:model.live="device-autojoin" --}}{{-- label="Permit Auto-Join"/>--}}
{{-- </flux:tooltip>--}}
{{-- </flux:navbar>--}}
<flux:navbar class="mr-1.5 space-x-0.5 py-0! max-lg:hidden">
<livewire:actions.device-auto-join/>
</flux:navbar>
<!-- Desktop User Menu -->
<flux:dropdown position="top" align="end">
@ -87,22 +89,24 @@
<flux:navlist variant="outline">
<flux:navlist.group heading="Platform">
<flux:navlist.item icon="layout-grid" href="{{ route('dashboard') }}" wire:navigate
:current="request()->routeIs('dashboard')">
:current="request()->routeIs('dashboard')" class="m-2">
Dashboard
</flux:navlist.item>
<flux:navbar.item icon="square-chart-gantt" href="{{ route('devices') }}" wire:navigate
:current="request()->routeIs('devices')">
:current="request()->routeIs('devices')" class="m-2">
Devices
</flux:navbar.item>
<flux:navbar.item icon="puzzle-piece" href="{{ route('plugins.index') }}" wire:navigate
:current="request()->routeIs('plugins.index')" class="m-2">
Plugins
</flux:navbar.item>
</flux:navlist.group>
</flux:navlist>
<flux:spacer/>
<flux:navlist variant="outline">
{{-- <flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">--}}
{{-- Repository--}}
{{-- </flux:navlist.item>--}}
<livewire:actions.device-auto-join/>
</flux:navlist>
</flux:sidebar>

View file

@ -0,0 +1,10 @@
@props(['percent'])
<flux:tooltip content="Battery Percent: {{ $percent }}%" position="bottom">
@if ($percent > 60)
<flux:icon.battery-full class="dark:text-zinc-200"/>
@elseif ($percent < 20)
<flux:icon.battery-low class="dark:text-zinc-200"/>
@else
<flux:icon.battery-medium class="dark:text-zinc-200"/>
@endif
</flux:tooltip>

View file

@ -0,0 +1,12 @@
@props(['strength', 'rssi'])
<flux:tooltip content="Wi-Fi RSSI Level: {{ $rssi }} db" position="bottom">
@if ($strength === 3)
<flux:icon.wifi class="dark:text-zinc-200"/>
@elseif ($strength === 2)
<flux:icon.wifi-high class="dark:text-zinc-200"/>
@elseif ($strength === 1)
<flux:icon.wifi-low class="dark:text-zinc-200"/>
@else
<flux:icon.wifi-zero class="dark:text-zinc-200"/>
@endif
</flux:tooltip>

View file

@ -1,5 +0,0 @@
<x-layouts.app>
<div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
<livewire:device-dashboard/>
</div>
</x-layouts.app>

View file

@ -1,3 +0,0 @@
<x-layouts.app>
<livewire:device-manager />
</x-layouts.app>

View file

@ -1,63 +0,0 @@
<x-layouts.app>
<div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex flex-col gap-6">
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">
@php
$current_image_uuid =$device->current_screen_image;
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
@endphp
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>
<p class="text-sm dark:text-zinc-400">{{$device->mac_address}}</p>
<p class="text-sm dark:text-zinc-400">Friendly Id: {{$device->friendly_id}}</p>
<p class="text-sm dark:text-zinc-400">Refresh Interval: {{$device->default_refresh_interval}}</p>
<p class="text-sm dark:text-zinc-400">Battery Voltage: {{$device->last_battery_voltage}}</p>
<p class="text-sm dark:text-zinc-400">Wifi RSSI Level: {{$device->last_rssi_level}}</p>
<p class="text-sm dark:text-zinc-400">Firmware Version: {{$device->last_firmware_version}}</p>
<flux:input.group class="mt-4 mb-2">
<flux:input.group.prefix>API Key</flux:input.group.prefix>
<flux:input icon="key" value="{{ $device->api_key }}" type="password" viewable class="max-w-xs"/>
</flux:input.group>
@if($current_image_uuid)
<flux:separator class="mt-6 mb-6" text="Current Screen" />
<img src="{{ asset($current_image_path) }}" alt="Current Image"/>
@endif
</div>
</div>
</div>
</div>
</x-layouts.app>
{{--<x-layouts.app>--}}
{{-- <x-slot name="header">--}}
{{-- <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">--}}
{{-- {{ __('Device Details: ') }} {{ $device->name }}--}}
{{-- </h2>--}}
{{-- </x-slot>--}}
{{-- <div class="py-12">--}}
{{-- <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">--}}
{{-- <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-6">--}}
{{-- <div class="mb-4">--}}
{{-- <p class="dark:text-gray-100"><strong>Name</strong> {{ $device->name }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Friendly ID</strong> {{ $device->friendly_id }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Mac Address</strong> {{ $device->mac_address }}</p>--}}
{{-- <p><strong>API Key</strong> <flux:input value="{{ $device->api_key }}" type="password" viewable></flux:input></p>--}}
{{-- <p class="dark:text-gray-100"><strong>Refresh--}}
{{-- Interval</strong> {{ $device->default_refresh_interval }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Battery Voltage</strong> {{ $device->last_battery_voltage }}--}}
{{-- </p>--}}
{{-- <p class="dark:text-gray-100"><strong>Wifi RSSI Level</strong> {{ $device->last_rssi_level }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Firmware Version</strong> {{ $device->last_firmware_version }}--}}
{{-- </p>--}}
{{-- </div>--}}
{{-- @if($image)--}}
{{-- <img src="{{$image}}"/>--}}
{{-- @endif--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- </div>--}}
{{--</x-layouts.app>--}}

View file

@ -0,0 +1,41 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$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,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z" />
</svg>

View file

@ -0,0 +1,7 @@
<div>
@if($isFirstUser)
<flux:tooltip content="Add devices automatically that try to connect to this server" position="bottom">
<flux:switch wire:model.live="deviceAutojoin" label="Permit Auto-Join"/>
</flux:tooltip>
@endif
</div>

View file

@ -28,7 +28,7 @@ new #[Layout('components.layouts.auth')] class extends Component {
$this->ensureIsNotRateLimited();
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
@ -47,7 +47,7 @@ new #[Layout('components.layouts.auth')] class extends Component {
*/
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
@ -68,19 +68,20 @@ new #[Layout('components.layouts.auth')] class extends Component {
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
return Str::transliterate(Str::lower($this->email) . '|' . request()->ip());
}
}; ?>
<div class="flex flex-col gap-6">
<x-auth-header title="Log in to your account" description="Enter your email and password below to log in" />
<x-auth-header title="Log in to your account" description="Enter your email and password below to log in"/>
<!-- Session Status -->
<x-auth-session-status class="text-center" :status="session('status')" />
<x-auth-session-status class="text-center" :status="session('status')"/>
<form wire:submit="login" class="flex flex-col gap-6">
<!-- Email Address -->
<flux:input wire:model="email" label="{{ __('Email address') }}" type="email" name="email" required autofocus autocomplete="email" placeholder="email@example.com" />
<flux:input wire:model="email" label="{{ __('Email address') }}" type="email" name="email" required autofocus
autocomplete="email" placeholder="email@example.com"/>
<!-- Password -->
<div class="relative">
@ -102,15 +103,19 @@ new #[Layout('components.layouts.auth')] class extends Component {
</div>
<!-- Remember Me -->
<flux:checkbox wire:model="remember" label="{{ __('Remember me') }}" />
<flux:checkbox wire:model="remember" label="{{ __('Remember me') }}"/>
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Log in') }}</flux:button>
</div>
</form>
<div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400">
Don't have an account?
<x-text-link href="{{ route('register') }}">Sign up</x-text-link>
</div>
@if (Route::has('register'))
<div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400">
Don't have an account?
<x-text-link href="{{ route('register') }}">Sign up</x-text-link>
</div>
@endif
</div>

View file

@ -1,3 +1,15 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public function mount()
{
return view('livewire.device-dashboard', ['devices' => auth()->user()->devices()->paginate(10)]);
}
}
?>
<div>
<div class="flex w-full max-w-3xl flex-col gap-6">
@if($devices->isEmpty())
@ -21,7 +33,8 @@
<div class="px-10 py-8">
@php
$current_image_uuid =$device->current_screen_image;
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp';
$current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension;
@endphp
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>

View file

@ -1,124 +0,0 @@
<div class="py-12">
{{--@dump($devices)--}}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
<flux:modal.trigger name="create-device">
<flux:button icon="plus" variant="primary">Add Device</flux:button>
</flux:modal.trigger>
</div>
@if (session()->has('message'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
{{ session('message') }}
</div>
@endif
<flux:modal name="create-device" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Add Device</flux:heading>
</div>
<form wire:submit="createDevice">
<div class="mb-4">
<flux:input label="Name" wire:model="name" id="name" class="block mt-1 w-full" type="text"
name="name"
autofocus/>
</div>
<div class="mb-4">
<flux:input label="Mac Address" wire:model="mac_address" id="mac_address"
class="block mt-1 w-full"
type="text" name="mac_address" autofocus/>
</div>
<div class="mb-4">
<flux:input label="API Key" wire:model="api_key" id="api_key" class="block mt-1 w-full"
type="text"
name="api_key" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Friendly Id" wire:model="friendly_id" id="friendly_id"
class="block mt-1 w-full"
type="text" name="friendly_id" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Refresh Rate (seconds)" wire:model="default_refresh_interval"
id="default_refresh_interval"
class="block mt-1 w-full" type="text" name="default_refresh_interval"
autofocus/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">Create Device</flux:button>
</div>
</form>
</div>
</flux:modal>
<table
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
data-flux-table="">
<thead data-flux-columns="">
<tr>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Name</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Friendly ID</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Mac Address</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Refresh</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Actions</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows="">
@foreach ($devices as $device)
<tr data-flux-row="">
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->name }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->friendly_id }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
<div type="button" data-flux-badge="data-flux-badge"
class="inline-flex items-center font-medium whitespace-nowrap -mt-1 -mb-1 text-xs py-1 [&_[data-flux-badge-icon]]:size-3 [&_[data-flux-badge-icon]]:mr-1 rounded-md px-2 text-zinc-700 [&_button]:!text-zinc-700 dark:text-zinc-200 [&_button]:dark:!text-zinc-200 bg-zinc-400/15 dark:bg-zinc-400/40 [&:is(button)]:hover:bg-zinc-400/25 [&:is(button)]:hover:dark:bg-zinc-400/50">
{{ $device->mac_address }}
</div>
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->default_refresh_interval }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap font-medium text-zinc-800 dark:text-white"
>
<flux:button href="{{ route('devices.configure', $device) }}" wire:navigate icon="eye">
</flux:button>
</td>
</tr>
@endforeach
<!--[if ENDBLOCK]><![endif]-->
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,120 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public $device;
public function mount(\App\Models\Device $device)
{
abort_unless(auth()->user()->devices->contains($device), 403);
$current_image_uuid = $device->current_screen_image;
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
return view('livewire.devices.configure', compact('device'), [
'image' => ($current_image_uuid) ? url($current_image_path) : null,
]);
}
public function deleteDevice(\App\Models\Device $device)
{
abort_unless(auth()->user()->devices->contains($device), 403);
$device->delete();
redirect()->route('devices');
}
}
?>
<div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex flex-col gap-6">
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8 min-w-lg">
@php
$current_image_uuid =$device->current_screen_image;
file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp';
$current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension;
@endphp
<div class="flex items-center justify-between">
<flux:tooltip content="Friendly ID: {{$device->friendly_id}}" position="bottom">
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>
</flux:tooltip>
<div>
<flux:modal.trigger name="edit-device">
<flux:button icon="key" variant="subtle"/>
</flux:modal.trigger>
<flux:modal.trigger name="delete-device">
<flux:button icon="trash" variant="danger"/>
</flux:modal.trigger>
</div>
<div class="flex gap-2">
<flux:tooltip content="Last update" position="bottom">
<span class="dark:text-zinc-200">{{$device->updated_at->diffForHumans()}}</span>
</flux:tooltip>
<flux:separator vertical/>
<flux:tooltip content="MAC Address" position="bottom">
<span class="dark:text-zinc-200">{{$device->mac_address}}</span>
</flux:tooltip>
<flux:separator vertical/>
<flux:tooltip content="Firmware Version" position="bottom">
<span class="dark:text-zinc-200">{{$device->last_firmware_version}}</span>
</flux:tooltip>
<flux:separator vertical/>
<x-responsive-icons.wifi :strength="$device->wifiStrengh" :rssi="$device->last_rssi_level"
class="dark:text-zinc-200"/>
<flux:separator vertical/>
<x-responsive-icons.battery :percent="$device->batteryPercent"/>
</div>
</div>
<flux:modal name="edit-device" class="md:w-96">
<div class="space-y-6">
<div>
{{-- <flux:heading size="lg">Edit TRMNL</flux:heading>--}}
{{-- <flux:subheading></flux:subheading>--}}
</div>
{{-- <flux:input label="Name" value="{{ $device->name }}"/>--}}
<flux:input label="API Key" icon="key" value="{{ $device->api_key }}" type="password"
viewable class="max-w-xs"/>
<div class="flex">
<flux:spacer/>
{{-- <flux:button type="submit" variant="primary">Save changes</flux:button>--}}
</div>
</div>
</flux:modal>
<flux:modal name="delete-device" class="min-w-[22rem] space-y-6">
<div>
<flux:heading size="lg">Delete {{$device->name}}?</flux:heading>
</div>
<div class="flex gap-2">
<flux:spacer/>
<flux:modal.close>
<flux:button variant="ghost">Cancel</flux:button>
</flux:modal.close>
<flux:button wire:click="deleteDevice({{ $device->id }})" variant="danger">Delete device</flux:button>
</div>
</flux:modal>
@if($current_image_uuid)
<flux:separator class="mt-6 mb-6" text="Next Screen"/>
<img src="{{ asset($current_image_path) }}" alt="Next Image"/>
@endif
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,205 @@
<?php
use App\Models\Device;
use Livewire\Volt\Component;
new class extends Component {
public $devices;
public $showDeviceForm = false;
public $name;
public $mac_address;
public $api_key;
public $default_refresh_interval = 900;
public $friendly_id;
protected $rules = [
'mac_address' => 'required',
'api_key' => 'required',
'default_refresh_interval' => 'required|integer',
];
public function mount()
{
$this->devices = auth()->user()->devices;
return view('livewire.devices.manage');
}
public function createDevice(): void
{
$this->validate();
Device::create([
'name' => $this->name,
'mac_address' => $this->mac_address,
'api_key' => $this->api_key,
'default_refresh_interval' => $this->default_refresh_interval,
'friendly_id' => $this->friendly_id,
'user_id' => auth()->id(),
]);
$this->reset();
\Flux::modal('create-device')->close();
$this->devices = auth()->user()->devices;
session()->flash('message', 'Device created successfully.');
}
public function toggleProxyCloud(Device $device): void
{
abort_unless(auth()->user()->devices->contains($device), 403);
$device->update([
'proxy_cloud' => !$device->proxy_cloud,
]);
// if ($device->proxy_cloud) {
// \App\Jobs\FetchProxyCloudResponses::dispatch();
// }
}
}
?>
<div>
<div class="py-12">
{{--@dump($devices)--}}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
<flux:modal.trigger name="create-device">
<flux:button icon="plus" variant="primary">Add Device</flux:button>
</flux:modal.trigger>
</div>
@if (session()->has('message'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
{{ session('message') }}
</div>
@endif
<flux:modal name="create-device" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Add Device</flux:heading>
</div>
<form wire:submit="createDevice">
<div class="mb-4">
<flux:input label="Name" wire:model="name" id="name" class="block mt-1 w-full" type="text"
name="name"
autofocus/>
</div>
<div class="mb-4">
<flux:input label="Mac Address" wire:model="mac_address" id="mac_address"
class="block mt-1 w-full"
type="text" name="mac_address" autofocus/>
</div>
<div class="mb-4">
<flux:input label="API Key" wire:model="api_key" id="api_key" class="block mt-1 w-full"
type="text"
name="api_key" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Friendly Id" wire:model="friendly_id" id="friendly_id"
class="block mt-1 w-full"
type="text" name="friendly_id" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Refresh Rate (seconds)" wire:model="default_refresh_interval"
id="default_refresh_interval"
class="block mt-1 w-full" type="text" name="default_refresh_interval"
autofocus/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">Create Device</flux:button>
</div>
</form>
</div>
</flux:modal>
<table
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
data-flux-table="">
<thead data-flux-columns="">
<tr>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Name</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Friendly ID</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Mac Address</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Refresh</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Actions</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows="">
@foreach ($devices as $device)
<tr data-flux-row="">
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->name }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->friendly_id }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
<div type="button" data-flux-badge="data-flux-badge"
class="inline-flex items-center font-medium whitespace-nowrap -mt-1 -mb-1 text-xs py-1 [&_[data-flux-badge-icon]]:size-3 [&_[data-flux-badge-icon]]:mr-1 rounded-md px-2 text-zinc-700 [&_button]:!text-zinc-700 dark:text-zinc-200 [&_button]:dark:!text-zinc-200 bg-zinc-400/15 dark:bg-zinc-400/40 [&:is(button)]:hover:bg-zinc-400/25 [&:is(button)]:hover:dark:bg-zinc-400/50">
{{ $device->mac_address }}
</div>
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->default_refresh_interval }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap font-medium text-zinc-800 dark:text-white"
>
<div class="flex items-center gap-4">
<flux:button href="{{ route('devices.configure', $device) }}" wire:navigate icon="eye">
</flux:button>
<flux:tooltip
content="Proxies images from the TRMNL Cloud service when no image is set (available in TRMNL DEV Edition only)."
position="bottom">
<flux:switch wire:model.live="proxy_cloud"
wire:click="toggleProxyCloud({{ $device->id }})"
:checked="$device->proxy_cloud" label="☁️ Proxy"/>
</flux:tooltip>
</div>
</td>
</tr>
@endforeach
<!--[if ENDBLOCK]><![endif]-->
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,56 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public $token;
public function mount(): void
{
$token = Auth::user()?->tokens()?->first();
if ($token === null) {
$token = Auth::user()->createToken('api-token', ['update-screen']);
}
$this->token = $token->plainTextToken;
}
public function regenerateToken()
{
Auth::user()->tokens()?->first()?->delete();
$token = Auth::user()->createToken('api-token', ['update-screen']);
$this->token = $token->plainTextToken;
}
};
?>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">API</h2>
</div>
<div>
<p>
<flux:badge>POST</flux:badge>
<span class="ml-2 font-mono">{{route('display.update')}}</span>
</p>
<div class="mt-4">
<h3 class="text-lg">Headers</h3>
<div>Authorization <span class="ml-2 font-mono">Bearer {{$token ?? '**********'}}</span>
<flux:button variant="subtle" size="xs" class="mt-2" wire:click="regenerateToken()">
Regenerate Token
</flux:button>
</div>
</div>
<div class="mt-4">
<h3 class="text-lg">Body</h3>
<div class="font-mono">
<pre>
{&#x22;markup&#x22;:&#x22;&#x3C;h1&#x3E;Hello World&#x3C;/h1&#x3E;&#x22;}
</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,36 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public $plugins = [
'markup' =>
['name' => 'Markup', 'icon' => 'code-backet', 'route' => 'plugins.markup'],
'api' =>
['name' => 'API', 'icon' => 'code-backet', 'route' => 'plugins.api'],
];
};
?>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Plugins</h2>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
@foreach($plugins as $plugin)
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<a href="{{ route($plugin['route']) }}" class="block">
<div class="flex items-center space-x-4 px-10 py-8">
<flux:icon name="code-bracket" class="text-4xl text-accent"/>
<h3 class="text-xl font-medium dark:text-zinc-200">{{$plugin['name']}}</h3>
</div>
</a>
</div>
@endforeach
</div>
</div>
</div>

View file

@ -0,0 +1,171 @@
<?php
use App\Jobs\GenerateScreenJob;
use Livewire\Volt\Component;
new class extends Component {
public string $blade_code = '';
public bool $isLoading = false;
public function submit()
{
$this->isLoading = true;
$this->validate([
'blade_code' => 'required|string'
]);
try {
$rendered = Blade::render($this->blade_code);
// if (config('app.puppeteer_docker')) {
// GenerateScreenJob::dispatch(auth()->user()->devices()->first()->id, $rendered);
// } else {
GenerateScreenJob::dispatchSync(auth()->user()->devices()->first()->id, $rendered);
// }
} catch (\Exception $e) {
$this->addError('error', $e->getMessage());
}
$this->isLoading = false;
}
public function renderExample(string $example)
{
switch ($example) {
case 'quote':
$markup = $this->renderQuote();
break;
case 'trainMonitor':
$markup = $this->renderTrainMonitor();
break;
case 'homeAssistant':
$markup = $this->renderHomeAssistant();
break;
default:
$markup = '<h1>Hello World!</h1>';
break;
}
$this->blade_code = $markup;
}
public function renderQuote(): string
{
return <<<HTML
<x-trmnl::view>
<x-trmnl::layout>
<x-trmnl::markdown gapSize="large">
<x-trmnl::title>Motivational Quote</x-trmnl::title>
<x-trmnl::content>“I love inside jokes. I hope to be a part of one someday.</x-trmnl::content>
<x-trmnl::label variant="underline">Michael Scott</x-trmnl::label>
</x-trmnl::markdown>
</x-trmnl::layout>
<x-trmnl::title-bar/>
</x-trmnl::view>
HTML;
}
public function renderTrainMonitor()
{
return <<<HTML
<x-trmnl::view>
<x-trmnl::layout>
<x-trmnl::table>
<thead>
<tr>
<th><x-trmnl::title>Abfahrt</x-trmnl::title></th>
<th><x-trmnl::title>Aktuell</x-trmnl::title></th>
<th><x-trmnl::title>Zug</x-trmnl::title></th>
<th><x-trmnl::title>Ziel</x-trmnl::title></th>
<th><x-trmnl::title>Steig</x-trmnl::title></th>
</tr>
</thead>
<tbody>
<tr>
<td><x-trmnl::label>08:51</x-trmnl::label></td>
<td><x-trmnl::label>08:52</x-trmnl::label></td>
<td><x-trmnl::label>REX 1</x-trmnl::label></td>
<td><x-trmnl::label>Vienna Main Station</x-trmnl::label></td>
<td><x-trmnl::label>3</x-trmnl::label></td>
</tr>
</tbody>
</x-trmnl::table>
</x-trmnl::layout>
<x-trmnl::title-bar title="Train Monitor"/>
</x-trmnl::view>
HTML;
}
public function renderHomeAssistant()
{
return <<<HTML
<x-trmnl::view>
<x-trmnl::layout class="layout--col gap--space-between">
<x-trmnl::grid cols="4">
<x-trmnl::col position="center">
<x-trmnl::item>
<x-trmnl::meta/>
<x-trmnl::content>
<x-trmnl::value size="large">23.3°</x-trmnl::value>
<x-trmnl::label class="w--full flex">
<flux:icon icon="droplet"/>
47.52 %
</x-trmnl::label>
<x-trmnl::label class="w--full flex">Sensor 1</x-trmnl::label>
</x-trmnl::content>
</x-trmnl::item>
</x-trmnl::col>
</x-trmnl::grid>
</x-trmnl::layout>
<x-trmnl::title-bar title="Home Assistant"/>
</x-trmnl::view>
HTML;
}
};
?>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<h2 class="text-2xl font-semibold dark:text-gray-100">Markup</h2>
{{-- <div class="flex justify-between items-center mb-6">--}}
<div class="mt-5 mb-5 ">
<span>Examples</span>
<div class="text-accent">
<a href="#" wire:click="renderExample('quote')" class="text-xl">Quote</a> |
<a href="#" wire:click="renderExample('trainMonitor')" class="text-xl">Train Monitor</a> |
<a href="#" wire:click="renderExample('homeAssistant')" class="text-xl">Temperature Sensors</a>
</div>
</div>
<form wire:submit="submit">
<div class="mb-4">
<flux:textarea
label="Blade Code"
class="font-mono"
wire:model="blade_code"
id="blade_code"
name="blade_code"
rows="15"
placeholder="Enter your blade code here..."
/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">
Generate Screen
</flux:button>
</div>
</form>
{{-- </div>--}}
</div>
</div>