- Created 10 SVG icon files in app/static/icons/ (Feather Icons style) - Updated base.html with SVG icons in navigation and dark mode toggle - Updated dashboard.html with icons in stats cards and quick actions - Updated content_list_new.html (playlist management) with SVG icons - Updated upload_media.html with upload-related icons - Updated manage_player.html with player management icons - Icons use currentColor for automatic theme adaptation - Removed emoji dependency for better Raspberry Pi compatibility - Added ICON_INTEGRATION.md documentation
493 lines
14 KiB
HTML
493 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Manage Playlist - {{ player.name }} - DigiServer v2{% endblock %}
|
||
|
||
{% block content %}
|
||
<style>
|
||
.playlist-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.player-info-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 25px;
|
||
border-radius: 12px;
|
||
margin-bottom: 25px;
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.player-info-card h1 {
|
||
margin: 0 0 10px 0;
|
||
font-size: 28px;
|
||
}
|
||
|
||
.player-info-card p {
|
||
margin: 5px 0;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.playlist-section {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 25px;
|
||
margin-bottom: 25px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #f0f0f0;
|
||
}
|
||
|
||
.section-header h2 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
color: #333;
|
||
}
|
||
|
||
.playlist-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.playlist-table thead {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.playlist-table th {
|
||
padding: 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #555;
|
||
border-bottom: 2px solid #dee2e6;
|
||
}
|
||
|
||
.playlist-table td {
|
||
padding: 12px;
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
|
||
.playlist-table tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.draggable-row {
|
||
cursor: move;
|
||
}
|
||
|
||
.draggable-row.dragging {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.drag-handle {
|
||
cursor: grab;
|
||
font-size: 18px;
|
||
color: #999;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
.drag-handle:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border-radius: 6px;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #5568d3;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #218838;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #c82333;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #5a6268;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 5px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.add-content-form {
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
font-weight: 600;
|
||
margin-bottom: 5px;
|
||
color: #333;
|
||
}
|
||
|
||
.form-control {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-control:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 15px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.stat-item {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
opacity: 0.9;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #999;
|
||
}
|
||
|
||
.empty-state-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.duration-input {
|
||
width: 80px;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
|
||
<div class="playlist-container">
|
||
<!-- Player Info Card -->
|
||
<div class="player-info-card">
|
||
<h1>🎬 {{ player.name }}</h1>
|
||
<p>📍 {{ player.location or 'No location' }}</p>
|
||
<p>🖥️ Hostname: {{ player.hostname }}</p>
|
||
<p>📊 Status: {{ '🟢 Online' if player.is_online else '🔴 Offline' }}</p>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-item">
|
||
<div class="stat-label">Playlist Items</div>
|
||
<div class="stat-value">{{ playlist_content|length }}</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-label">Playlist Version</div>
|
||
<div class="stat-value">{{ player.playlist_version }}</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-label">Total Duration</div>
|
||
<div class="stat-value">{{ playlist_content|sum(attribute='duration') }}s</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div style="margin-bottom: 20px; display: flex; gap: 10px;">
|
||
<a href="{{ url_for('players.player_page', player_id=player.id) }}" class="btn btn-secondary">
|
||
← Back to Player
|
||
</a>
|
||
<a href="{{ url_for('content.upload_content', player_id=player.id, return_url=url_for('playlist.manage_playlist', player_id=player.id)) }}"
|
||
class="btn btn-success">
|
||
➕ Upload New Content
|
||
</a>
|
||
{% if playlist_content %}
|
||
<form method="POST" action="{{ url_for('playlist.clear_playlist', player_id=player.id) }}"
|
||
style="display: inline;"
|
||
onsubmit="return confirm('Are you sure you want to clear the entire playlist?');">
|
||
<button type="submit" class="btn btn-danger">
|
||
🗑️ Clear Playlist
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Current Playlist -->
|
||
<div class="playlist-section">
|
||
<div class="section-header">
|
||
<h2>📋 Current Playlist</h2>
|
||
<span style="color: #999; font-size: 14px;">
|
||
Drag and drop to reorder
|
||
</span>
|
||
</div>
|
||
|
||
{% if playlist_content %}
|
||
<table class="playlist-table" id="playlist-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 40px;"></th>
|
||
<th style="width: 50px;">#</th>
|
||
<th>Filename</th>
|
||
<th style="width: 100px;">Type</th>
|
||
<th style="width: 120px;">Duration (s)</th>
|
||
<th style="width: 100px;">Size</th>
|
||
<th style="width: 150px;">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="playlist-tbody">
|
||
{% for content in playlist_content %}
|
||
<tr class="draggable-row" draggable="true" data-content-id="{{ content.id }}">
|
||
<td>
|
||
<span class="drag-handle">⋮⋮</span>
|
||
</td>
|
||
<td>{{ loop.index }}</td>
|
||
<td>{{ content.filename }}</td>
|
||
<td>
|
||
{% if content.content_type == 'image' %}📷 Image
|
||
{% elif content.content_type == 'video' %}🎥 Video
|
||
{% elif content.content_type == 'pdf' %}📄 PDF
|
||
{% else %}📁 {{ content.content_type }}
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<input type="number"
|
||
class="form-control duration-input"
|
||
value="{{ content.duration }}"
|
||
min="1"
|
||
onchange="updateDuration({{ content.id }}, this.value)">
|
||
</td>
|
||
<td>{{ "%.2f"|format(content.file_size_mb) }} MB</td>
|
||
<td>
|
||
<form method="POST"
|
||
action="{{ url_for('playlist.remove_from_playlist', player_id=player.id, content_id=content.id) }}"
|
||
style="display: inline;"
|
||
onsubmit="return confirm('Remove {{ content.filename }} from playlist?');">
|
||
<button type="submit" class="btn btn-danger btn-sm">
|
||
✕ Remove
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">📭</div>
|
||
<h3>No content in playlist</h3>
|
||
<p>Upload content or add existing files to get started</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Add Content Section -->
|
||
{% if available_files %}
|
||
<div class="playlist-section">
|
||
<div class="section-header">
|
||
<h2>➕ Add Existing Content</h2>
|
||
</div>
|
||
|
||
<form method="POST" action="{{ url_for('playlist.add_to_playlist', player_id=player.id) }}"
|
||
class="add-content-form">
|
||
<div class="form-group">
|
||
<label for="filename">Select File:</label>
|
||
<select name="filename" id="filename" class="form-control" required>
|
||
<option value="" disabled selected>Choose a file...</option>
|
||
{% for filename in available_files %}
|
||
<option value="{{ filename }}">{{ filename }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="duration">Display Duration (seconds):</label>
|
||
<input type="number"
|
||
name="duration"
|
||
id="duration"
|
||
class="form-control"
|
||
value="10"
|
||
min="1"
|
||
required>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-success">
|
||
➕ Add to Playlist
|
||
</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script>
|
||
let draggedElement = null;
|
||
|
||
// Initialize drag and drop
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const tbody = document.getElementById('playlist-tbody');
|
||
if (!tbody) return;
|
||
|
||
const rows = tbody.querySelectorAll('.draggable-row');
|
||
|
||
rows.forEach(row => {
|
||
row.addEventListener('dragstart', handleDragStart);
|
||
row.addEventListener('dragover', handleDragOver);
|
||
row.addEventListener('drop', handleDrop);
|
||
row.addEventListener('dragend', handleDragEnd);
|
||
});
|
||
});
|
||
|
||
function handleDragStart(e) {
|
||
draggedElement = this;
|
||
this.classList.add('dragging');
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
}
|
||
|
||
function handleDragOver(e) {
|
||
if (e.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
e.dataTransfer.dropEffect = 'move';
|
||
return false;
|
||
}
|
||
|
||
function handleDrop(e) {
|
||
if (e.stopPropagation) {
|
||
e.stopPropagation();
|
||
}
|
||
|
||
if (draggedElement !== this) {
|
||
const tbody = document.getElementById('playlist-tbody');
|
||
const allRows = [...tbody.querySelectorAll('.draggable-row')];
|
||
const draggedIndex = allRows.indexOf(draggedElement);
|
||
const targetIndex = allRows.indexOf(this);
|
||
|
||
if (draggedIndex < targetIndex) {
|
||
this.parentNode.insertBefore(draggedElement, this.nextSibling);
|
||
} else {
|
||
this.parentNode.insertBefore(draggedElement, this);
|
||
}
|
||
|
||
updateRowNumbers();
|
||
saveOrder();
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function handleDragEnd(e) {
|
||
this.classList.remove('dragging');
|
||
}
|
||
|
||
function updateRowNumbers() {
|
||
const rows = document.querySelectorAll('#playlist-tbody tr');
|
||
rows.forEach((row, index) => {
|
||
row.querySelector('td:nth-child(2)').textContent = index + 1;
|
||
});
|
||
}
|
||
|
||
function saveOrder() {
|
||
const rows = document.querySelectorAll('#playlist-tbody .draggable-row');
|
||
const contentIds = Array.from(rows).map(row => parseInt(row.dataset.contentId));
|
||
|
||
fetch('{{ url_for("playlist.reorder_playlist", player_id=player.id) }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ content_ids: contentIds })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
console.log('Playlist reordered successfully');
|
||
} else {
|
||
alert('Error reordering playlist: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Error reordering playlist');
|
||
});
|
||
}
|
||
|
||
function updateDuration(contentId, duration) {
|
||
const formData = new FormData();
|
||
formData.append('duration', duration);
|
||
|
||
fetch(`{{ url_for("playlist.update_duration", player_id=player.id, content_id=0) }}`.replace('/0', `/${contentId}`), {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
console.log('Duration updated successfully');
|
||
// Update total duration
|
||
location.reload();
|
||
} else {
|
||
alert('Error updating duration: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Error updating duration');
|
||
});
|
||
}
|
||
</script>
|
||
|
||
{% endblock %}
|