mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: Playlist UI on device details page
This commit is contained in:
parent
86b8632bc3
commit
539a98a356
2 changed files with 319 additions and 2 deletions
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistItem;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
|
|
@ -12,6 +14,13 @@ new class extends Component {
|
|||
public $mac_address;
|
||||
public $default_refresh_interval;
|
||||
|
||||
// Playlist properties
|
||||
public $playlists;
|
||||
public $playlist_name;
|
||||
public $selected_weekdays = [];
|
||||
public $active_from;
|
||||
public $active_until;
|
||||
|
||||
public function mount(\App\Models\Device $device)
|
||||
{
|
||||
abort_unless(auth()->user()->devices->contains($device), 403);
|
||||
|
|
@ -19,11 +28,13 @@ new class extends Component {
|
|||
$current_image_uuid = $device->current_screen_image;
|
||||
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
|
||||
|
||||
$this->device = $device;
|
||||
$this->name = $device->name;
|
||||
$this->api_key = $device->api_key;
|
||||
$this->friendly_id = $device->friendly_id;
|
||||
$this->mac_address = $device->mac_address;
|
||||
$this->default_refresh_interval = $device->default_refresh_interval;
|
||||
$this->playlists = $device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
|
||||
return view('livewire.devices.configure', [
|
||||
'image' => ($current_image_uuid) ? url($current_image_path) : null,
|
||||
|
|
@ -58,6 +69,115 @@ new class extends Component {
|
|||
|
||||
Flux::modal('edit-device')->close();
|
||||
}
|
||||
|
||||
public function createPlaylist()
|
||||
{
|
||||
$this->validate([
|
||||
'playlist_name' => 'required|string|max:255',
|
||||
'selected_weekdays' => 'array',
|
||||
'active_from' => 'nullable|date_format:H:i',
|
||||
'active_until' => 'nullable|date_format:H:i',
|
||||
]);
|
||||
|
||||
$this->device->playlists()->create([
|
||||
'name' => $this->playlist_name,
|
||||
'weekdays' => $this->selected_weekdays,
|
||||
'active_from' => $this->active_from,
|
||||
'active_until' => $this->active_until,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
$this->reset(['playlist_name', 'selected_weekdays', 'active_from', 'active_until']);
|
||||
Flux::modal('create-playlist')->close();
|
||||
}
|
||||
|
||||
public function togglePlaylistActive(Playlist $playlist)
|
||||
{
|
||||
$playlist->update(['is_active' => !$playlist->is_active]);
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
}
|
||||
|
||||
public function movePlaylistItemUp(PlaylistItem $item)
|
||||
{
|
||||
$previousItem = $item->playlist->items()
|
||||
->where('order', '<', $item->order)
|
||||
->orderBy('order', 'desc')
|
||||
->first();
|
||||
|
||||
if ($previousItem) {
|
||||
$tempOrder = $previousItem->order;
|
||||
$previousItem->update(['order' => $item->order]);
|
||||
$item->update(['order' => $tempOrder]);
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function movePlaylistItemDown(PlaylistItem $item)
|
||||
{
|
||||
$nextItem = $item->playlist->items()
|
||||
->where('order', '>', $item->order)
|
||||
->orderBy('order')
|
||||
->first();
|
||||
|
||||
if ($nextItem) {
|
||||
$tempOrder = $nextItem->order;
|
||||
$nextItem->update(['order' => $item->order]);
|
||||
$item->update(['order' => $tempOrder]);
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function togglePlaylistItemActive(PlaylistItem $item)
|
||||
{
|
||||
$item->update(['is_active' => !$item->is_active]);
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
}
|
||||
|
||||
public function deletePlaylist(Playlist $playlist)
|
||||
{
|
||||
abort_unless(auth()->user()->devices->contains($playlist->device), 403);
|
||||
$playlist->delete();
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
Flux::modal('delete-playlist-' . $playlist->id)->close();
|
||||
}
|
||||
|
||||
public function deletePlaylistItem(PlaylistItem $item)
|
||||
{
|
||||
abort_unless(auth()->user()->devices->contains($item->playlist->device), 403);
|
||||
$item->delete();
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
Flux::modal('delete-playlist-item-' . $item->id)->close();
|
||||
}
|
||||
|
||||
public function editPlaylist(Playlist $playlist)
|
||||
{
|
||||
$this->validate([
|
||||
'playlist_name' => 'required|string|max:255',
|
||||
'selected_weekdays' => 'array',
|
||||
'active_from' => 'nullable|date_format:H:i',
|
||||
'active_until' => 'nullable|date_format:H:i',
|
||||
]);
|
||||
|
||||
$playlist->update([
|
||||
'name' => $this->playlist_name,
|
||||
'weekdays' => $this->selected_weekdays,
|
||||
'active_from' => $this->active_from,
|
||||
'active_until' => $this->active_until,
|
||||
]);
|
||||
|
||||
$this->playlists = $this->device->playlists()->with('items.plugin')->orderBy('created_at')->get();
|
||||
$this->reset(['playlist_name', 'selected_weekdays', 'active_from', 'active_until']);
|
||||
Flux::modal('edit-playlist-' . $playlist->id)->close();
|
||||
}
|
||||
|
||||
public function preparePlaylistEdit(Playlist $playlist)
|
||||
{
|
||||
$this->playlist_name = $playlist->name;
|
||||
$this->selected_weekdays = $playlist->weekdays ?? [];
|
||||
$this->active_from = optional($playlist->active_from)->format('H:i');
|
||||
$this->active_until = optional($playlist->active_until)->format('H:i');
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
|
@ -129,7 +249,8 @@ new class extends Component {
|
|||
|
||||
<flux:input label="Friendly ID" wire:model="friendly_id"/>
|
||||
<flux:input label="MAC Address" wire:model="mac_address"/>
|
||||
<flux:input label="Default Refresh Interval (seconds)" wire:model="default_refresh_interval" type="number"/>
|
||||
<flux:input label="Default Refresh Interval (seconds)" wire:model="default_refresh_interval"
|
||||
type="number"/>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
|
|
@ -161,6 +282,203 @@ new class extends Component {
|
|||
<flux:separator class="mt-6 mb-6" text="Next Screen"/>
|
||||
<img src="{{ asset($current_image_path) }}" alt="Next Image"/>
|
||||
@endif
|
||||
|
||||
<flux:separator class="mt-6 mb-6" text="Playlists"/>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium dark:text-zinc-200">Device Playlists</h3>
|
||||
<flux:modal.trigger name="create-playlist">
|
||||
<flux:button icon="plus" variant="primary">Create Playlist</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
|
||||
<flux:modal name="create-playlist" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Create Playlist</flux:heading>
|
||||
</div>
|
||||
|
||||
<form wire:submit="createPlaylist">
|
||||
<div class="mb-4">
|
||||
<flux:input label="Playlist Name" wire:model="playlist_name" required/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:checkbox.group wire:model="selected_weekdays" label="Active Days">
|
||||
<flux:checkbox label="Monday" value="1"/>
|
||||
<flux:checkbox label="Tuesday" value="2"/>
|
||||
<flux:checkbox label="Wednesday" value="3"/>
|
||||
<flux:checkbox label="Thursday" value="4"/>
|
||||
<flux:checkbox label="Friday" value="5"/>
|
||||
<flux:checkbox label="Saturday" value="6"/>
|
||||
<flux:checkbox label="Sunday" value="0"/>
|
||||
</flux:checkbox.group>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input type="time" label="Active From" wire:model="active_from"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input type="time" label="Active Until" wire:model="active_until"/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="submit" variant="primary">Create Playlist</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
@foreach($playlists as $playlist)
|
||||
<div class="mb-6 rounded-lg border dark:border-zinc-700 p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<h4 class="text-lg font-medium dark:text-zinc-200">{{ $playlist->name }}</h4>
|
||||
<flux:switch wire:model.live="playlist.is_active"
|
||||
wire:click="togglePlaylistActive({{ $playlist->id }})"
|
||||
:checked="$playlist->is_active"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
@if($playlist->weekdays)
|
||||
<span>{{ implode(', ', collect($playlist->weekdays)->map(fn($day) => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][$day])->toArray()) }}</span>
|
||||
@endif
|
||||
@if($playlist->active_from && $playlist->active_until)
|
||||
<flux:separator vertical/>
|
||||
<span>{{ $playlist->active_from->format('H:i') }} - {{ $playlist->active_until->format('H:i') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:modal.trigger name="edit-playlist-{{ $playlist->id }}">
|
||||
<flux:button icon="pencil-square" variant="subtle" size="sm" wire:click="preparePlaylistEdit({{ $playlist->id }})"/>
|
||||
</flux:modal.trigger>
|
||||
<flux:modal.trigger name="delete-playlist-{{ $playlist->id }}">
|
||||
<flux:button icon="trash" size="sm"/>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:modal name="edit-playlist-{{ $playlist->id }}" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Edit Playlist</flux:heading>
|
||||
</div>
|
||||
|
||||
<form wire:submit="editPlaylist({{ $playlist->id }})">
|
||||
<div class="mb-4">
|
||||
<flux:input label="Playlist Name" wire:model="playlist_name" required/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:checkbox.group wire:model="selected_weekdays" label="Active Days">
|
||||
<flux:checkbox label="Monday" value="1"/>
|
||||
<flux:checkbox label="Tuesday" value="2"/>
|
||||
<flux:checkbox label="Wednesday" value="3"/>
|
||||
<flux:checkbox label="Thursday" value="4"/>
|
||||
<flux:checkbox label="Friday" value="5"/>
|
||||
<flux:checkbox label="Saturday" value="6"/>
|
||||
<flux:checkbox label="Sunday" value="0"/>
|
||||
</flux:checkbox.group>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input type="time" label="Active From" wire:model="active_from"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input type="time" label="Active Until" wire:model="active_until"/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="submit" variant="primary">Save Changes</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<flux:modal name="delete-playlist-{{ $playlist->id }}" class="min-w-[22rem] space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Delete {{ $playlist->name }}?</flux:heading>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">This will permanently delete this playlist and all its items.</p>
|
||||
</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="deletePlaylist({{ $playlist->id }})" variant="danger">Delete playlist</flux:button>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<table class="w-full" 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">Plugin</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">Status</div>
|
||||
</th>
|
||||
<th class="py-3 px-3 first:pl-0 last:pr-0 text-right text-sm font-medium text-zinc-800 dark:text-white"
|
||||
data-flux-column>
|
||||
<div class="whitespace-nowrap flex justify-end">Actions</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows>
|
||||
@foreach($playlist->items->sortBy('order') as $item)
|
||||
<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">
|
||||
{{ $item->plugin->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">
|
||||
<flux:switch wire:model.live="item.is_active"
|
||||
wire:click="togglePlaylistItemActive({{ $item->id }})"
|
||||
:checked="$item->is_active"/>
|
||||
</td>
|
||||
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
@if(!$loop->first)
|
||||
<flux:button wire:click="movePlaylistItemUp({{ $item->id }})"
|
||||
icon="arrow-up" variant="subtle" size="sm"/>
|
||||
@endif
|
||||
@if(!$loop->last)
|
||||
<flux:button wire:click="movePlaylistItemDown({{ $item->id }})"
|
||||
icon="arrow-down" variant="subtle" size="sm"/>
|
||||
@endif
|
||||
<flux:modal.trigger name="delete-playlist-item-{{ $item->id }}">
|
||||
<flux:button icon="trash" size="sm"/>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
|
||||
<flux:modal name="delete-playlist-item-{{ $item->id }}" class="min-w-[22rem] space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Delete {{ $item->plugin->name }}?</flux:heading>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">This will remove this item from the playlist.</p>
|
||||
</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="deletePlaylistItem({{ $item->id }})" variant="danger">Delete item</flux:button>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue