|
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
6 | 6 | <title>TinyIce Admin</title> |
7 | 7 | <script src="/assets/js/lucide.min.js"></script> |
| 8 | + <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> |
8 | 9 | <style> |
9 | 10 | :root { |
10 | 11 | --bg: #0b0f1a; --surface: #161b2c; --surface-hover: #1e253a; |
|
59 | 60 | .history-mount-btn { padding: 1rem; border: none; background: transparent; color: var(--text-dim); text-align: left; cursor: pointer; border-bottom: 1px solid var(--border); font-size: 0.9rem; width: 100%; } |
60 | 61 | .history-mount-btn:hover { background: var(--surface-hover); color: var(--text); } |
61 | 62 | .history-mount-btn.active { color: var(--primary); background: var(--primary-glow); font-weight: 700; } |
| 63 | + .playlist-item.active { border-color: var(--primary) !important; background: var(--primary-glow) !important; } |
| 64 | + .playlist-item.active span { color: var(--primary); font-weight: 600; } |
| 65 | + .sortable-ghost { opacity: 0.4; background: var(--primary-glow) !important; } |
62 | 66 | </style> |
63 | 67 | </head> |
64 | 68 | <body> |
@@ -387,7 +391,14 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;"> |
387 | 391 | Dir: <span style="font-family: monospace; font-size: 0.75rem;">{{.MusicDir}}</span> |
388 | 392 | </div> |
389 | 393 | </div> |
390 | | - <div style="display: flex; gap: 0.5rem;"> |
| 394 | + <div style="display: flex; gap: 0.5rem;"> |
| 395 | + <form action="/admin/player/shuffle" method="POST"> |
| 396 | + <input type="hidden" name="csrf" value="{{$.CSRFToken}}"> |
| 397 | + <input type="hidden" name="mount" value="{{.OutputMount}}"> |
| 398 | + <button type="submit" class="btn {{if .Shuffle}}btn-primary{{else}}btn-outline{{end}} btn-sm" title="Toggle Shuffle"> |
| 399 | + <i data-lucide="shuffle" style="width: 14px; height: 14px;"></i> |
| 400 | + </button> |
| 401 | + </form> |
391 | 402 | <a href="/player{{.OutputMount}}" target="_blank" class="btn btn-outline btn-sm"><i data-lucide="headphones" style="width: 14px; height: 14px; margin-right: 4px;"></i> MONITOR</a> |
392 | 403 | <form action="/admin/autodj/toggle" method="POST"> |
393 | 404 | <input type="hidden" name="csrf" value="{{$.CSRFToken}}"> |
@@ -425,6 +436,50 @@ <h3 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;"> |
425 | 436 | </div> |
426 | 437 | {{end}} |
427 | 438 |
|
| 439 | + <div style="margin-top: 1.5rem;"> |
| 440 | + {{if .Queue}} |
| 441 | + <div style="margin-bottom: 1rem;"> |
| 442 | + <div class="label" style="font-size: 0.65rem; color: var(--primary); margin-bottom: 0.5rem; letter-spacing: 1px;">UP NEXT (QUEUE)</div> |
| 443 | + <div style="display: flex; flex-direction: column; gap: 0.4rem;"> |
| 444 | + {{range $qidx, $qfile := .Queue}} |
| 445 | + <div style="display: flex; justify-content: space-between; align-items: center; background: rgba(56, 189, 248, 0.05); padding: 0.5rem; border-radius: 4px; border: 1px dashed var(--primary-glow);"> |
| 446 | + <span style="font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{$qfile}}</span> |
| 447 | + <form action="/admin/player/queue" method="POST"> |
| 448 | + <input type="hidden" name="csrf" value="{{$.CSRFToken}}"> |
| 449 | + <input type="hidden" name="mount" value="{{$.OutputMount}}"> |
| 450 | + <input type="hidden" name="action" value="remove"> |
| 451 | + <input type="hidden" name="index" value="{{$qidx}}"> |
| 452 | + <button type="submit" style="background:none; border:none; color:var(--danger); cursor:pointer;"><i data-lucide="x-circle" style="width:14px; height:14px;"></i></button> |
| 453 | + </form> |
| 454 | + </div> |
| 455 | + {{end}} |
| 456 | + </div> |
| 457 | + </div> |
| 458 | + {{end}} |
| 459 | + |
| 460 | + <div class="label" style="font-size: 0.65rem; color: var(--text-dim); margin-bottom: 0.5rem; letter-spacing: 1px;">PLAYLIST (DRAG TO REORDER)</div> |
| 461 | + <div class="playlist-sortable" data-mount="{{.OutputMount}}" style="max-height: 250px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px;"> |
| 462 | + {{$current := .CurrentPos}} |
| 463 | + {{range $idx, $file := .Playlist}} |
| 464 | + <div class="playlist-item {{if and (not $.Shuffle) (eq $idx $current)}}active{{end}}" data-index="{{$idx}}" style="display: flex; justify-content: space-between; align-items: center; background: var(--bg); padding: 0.6rem; border-radius: 4px; cursor: grab; border: 1px solid transparent;"> |
| 465 | + <div style="display: flex; align-items: center; gap: 0.75rem; overflow: hidden;"> |
| 466 | + <i data-lucide="grip-vertical" style="width: 14px; height: 14px; color: var(--border);"></i> |
| 467 | + <span style="font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{$file}}</span> |
| 468 | + </div> |
| 469 | + <div style="display: flex; gap: 0.5rem;"> |
| 470 | + <form action="/admin/player/queue" method="POST"> |
| 471 | + <input type="hidden" name="csrf" value="{{$.CSRFToken}}"> |
| 472 | + <input type="hidden" name="mount" value="{{$.OutputMount}}"> |
| 473 | + <input type="hidden" name="action" value="add"> |
| 474 | + <input type="hidden" name="path" value="{{$file}}"> |
| 475 | + <button type="submit" class="btn btn-outline btn-sm" style="padding: 0.2rem 0.4rem;" title="Add to Queue"><i data-lucide="list-plus" style="width: 14px; height: 14px;"></i></button> |
| 476 | + </form> |
| 477 | + </div> |
| 478 | + </div> |
| 479 | + {{end}} |
| 480 | + </div> |
| 481 | + </div> |
| 482 | + |
428 | 483 | <div style="margin-top: 1rem; display: flex; gap: 1rem;"> |
429 | 484 | <form action="/admin/player/scan" method="POST" style="flex: 1;"> |
430 | 485 | <input type="hidden" name="csrf" value="{{$.CSRFToken}}"> |
@@ -520,6 +575,35 @@ <h2>Top Connection Scanners (404s)</h2> |
520 | 575 | } |
521 | 576 | setInterval(updateAutoDJProgress, 1000); |
522 | 577 |
|
| 578 | + function initSortable() { |
| 579 | + document.querySelectorAll('.playlist-sortable').forEach(function(el) { |
| 580 | + new Sortable(el, { |
| 581 | + animation: 150, |
| 582 | + ghostClass: 'sortable-ghost', |
| 583 | + handle: '.lucide-grip-vertical', |
| 584 | + onEnd: function(evt) { |
| 585 | + const mount = el.dataset.mount; |
| 586 | + const from = evt.oldIndex; |
| 587 | + const to = evt.newIndex; |
| 588 | + if (from === to) return; |
| 589 | + |
| 590 | + const formData = new FormData(); |
| 591 | + formData.append('csrf', csrfToken); |
| 592 | + formData.append('mount', mount); |
| 593 | + formData.append('from', from); |
| 594 | + formData.append('to', to); |
| 595 | + |
| 596 | + fetch('/admin/player/reorder', { |
| 597 | + method: 'POST', |
| 598 | + body: formData |
| 599 | + }).then(r => { |
| 600 | + if (!r.ok) alert('Failed to reorder playlist'); |
| 601 | + }); |
| 602 | + } |
| 603 | + }); |
| 604 | + }); |
| 605 | + } |
| 606 | + |
523 | 607 | const csrfToken = "{{.CSRFToken}}"; |
524 | 608 | function showTab(id, updateHash) { |
525 | 609 | document.querySelectorAll('.tab-content').forEach(function(t) { t.classList.remove('active'); }); |
@@ -885,6 +969,7 @@ <h2>Top Connection Scanners (404s)</h2> |
885 | 969 |
|
886 | 970 | drawChart(); |
887 | 971 | lucide.createIcons(); |
| 972 | + initSortable(); |
888 | 973 | if (window.location.hash) { var it = window.location.hash.substring(1); if (document.getElementById(it)) { showTab(it, false); } } |
889 | 974 | })(); |
890 | 975 | </script> |
|
0 commit comments