v1.0.0: Playlist management with drag-and-drop
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
@model Device
|
||||
@{
|
||||
ViewData["Title"] = $"PURR — {Model.Name}";
|
||||
var availableSlides = ViewBag.AvailableSlides as List<Slide>;
|
||||
}
|
||||
|
||||
@section HeaderActions {
|
||||
<a href="/@Model.Slug" target="_blank" class="btn btn-success"><i class="bi bi-box-arrow-up-right me-1"></i>View Display</a>
|
||||
}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-music-note-list me-2"></i>Playlist <small class="text-muted ms-2">Drag to reorder</small></h5>
|
||||
<span class="badge bg-secondary" id="totalDuration"></span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if (Model.DeviceSlides.Any())
|
||||
{
|
||||
<div id="playlistItems" class="list-group list-group-flush">
|
||||
@foreach (var ds in Model.DeviceSlides)
|
||||
{
|
||||
<div class="list-group-item playlist-item d-flex align-items-center gap-3" data-id="@ds.Id" data-duration="@ds.DurationSeconds">
|
||||
<div class="drag-handle" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<strong>@ds.Slide.Name</strong>
|
||||
@switch (ds.Slide.SlideType) {
|
||||
case SlideType.Content: <span class="badge bg-info badge-sm">Content</span> break;
|
||||
case SlideType.Embed: <span class="badge bg-warning text-dark badge-sm">Embed</span> break;
|
||||
case SlideType.IcsCalendar: <span class="badge bg-success badge-sm">Calendar</span> break;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="form-check form-switch"><input class="form-check-input toggle-enabled" type="checkbox" data-id="@ds.Id" @(ds.Enabled ? "checked" : "") /></div>
|
||||
<div class="input-group input-group-sm duration-input" style="width:120px;"><input type="number" class="form-control text-center duration-value" value="@ds.DurationSeconds" min="5" max="3600" data-id="@ds.Id" /><span class="input-group-text">sec</span></div>
|
||||
<form asp-action="RemoveFromPlaylist" method="post" class="d-inline" onsubmit="return confirm('Remove this meow?');">
|
||||
<input type="hidden" name="id" value="@Model.Id" /><input type="hidden" name="deviceSlideId" value="@ds.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-x-lg"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="p-4 text-center text-muted"><i class="bi bi-music-note-list" style="font-size:2em;"></i><p class="mt-2">No meows in this PURR yet.</p></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4"><div class="card-header"><h6 class="mb-0">Kitten Info</h6></div><div class="card-body"><p class="mb-1"><strong>@Model.Name</strong></p><p class="mb-1 text-muted"><code>/@Model.Slug</code></p><p class="mb-0 text-muted">@(Model.ResolutionWidth)x@(Model.ResolutionHeight) · @Model.Transition</p></div></div>
|
||||
<div class="card"><div class="card-header"><h6 class="mb-0"><i class="bi bi-plus-circle me-1"></i>Add Meow</h6></div><div class="card-body">
|
||||
@if (availableSlides != null && availableSlides.Any())
|
||||
{
|
||||
<form asp-action="AddToPlaylist" method="post"><input type="hidden" name="id" value="@Model.Id" />
|
||||
<div class="mb-3"><label class="form-label">Select Slide</label><select name="slideId" class="form-select" required><option value="">— Choose a meow —</option>@foreach (var slide in availableSlides){<option value="@slide.Id">@slide.Name (@slide.SlideType)</option>}</select></div>
|
||||
<div class="mb-3"><label class="form-label">Duration (seconds)</label><input type="number" name="durationSeconds" class="form-control" value="30" min="5" max="3600" /></div>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-plus-lg me-1"></i>Add to PURR</button>
|
||||
</form>
|
||||
}
|
||||
else { <p class="text-muted mb-2">All meows assigned.</p><a href="/admin/slides/create" class="btn btn-outline-primary btn-sm"><i class="bi bi-plus-lg me-1"></i>Create New Meow</a> }
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.3/Sortable.min.js"></script>
|
||||
<script>
|
||||
function updateTotalDuration(){var t=0;document.querySelectorAll('.duration-value').forEach(function(e){t+=parseInt(e.value)||0;});var m=Math.floor(t/60),s=t%60;document.getElementById('totalDuration').textContent='Total: '+(m>0?m+'m ':'')+s+'s loop';}
|
||||
updateTotalDuration();
|
||||
var list=document.getElementById('playlistItems');
|
||||
if(list){Sortable.create(list,{handle:'.drag-handle',animation:200,onEnd:function(){var ids=[];list.querySelectorAll('.playlist-item').forEach(function(e){ids.push(parseInt(e.dataset.id));});fetch('/admin/devices/reorderplaylist',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({itemIds:ids})});}});}
|
||||
document.querySelectorAll('.duration-value').forEach(function(e){e.addEventListener('change',function(){fetch('/admin/devices/updateduration',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceSlideId:parseInt(this.dataset.id),durationSeconds:parseInt(this.value)})});updateTotalDuration();});});
|
||||
document.querySelectorAll('.toggle-enabled').forEach(function(e){e.addEventListener('change',function(){fetch('/admin/devices/toggleenabled',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceSlideId:parseInt(this.dataset.id),enabled:this.checked})});});});
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user