Replace emoji icons with local SVG files for consistent rendering
- 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
This commit is contained in:
304
app/templates/content/manage_playlist_content.html
Normal file
304
app/templates/content/manage_playlist_content.html
Normal file
@@ -0,0 +1,304 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Manage {{ playlist.name }} - DigiServer v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.playlist-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.playlist-header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.playlist-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.playlist-table th {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.playlist-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.draggable-row {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.draggable-row:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.available-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-item {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container" style="max-width: 1400px;">
|
||||
<div class="playlist-header">
|
||||
<h1>🎬 {{ playlist.name }}</h1>
|
||||
{% if playlist.description %}
|
||||
<p style="margin: 5px 0; opacity: 0.9;">{{ playlist.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Content Items</span>
|
||||
<span class="stat-value">{{ playlist_content|length }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total Duration</span>
|
||||
<span class="stat-value">{{ playlist.total_duration }}s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Version</span>
|
||||
<span class="stat-value">{{ playlist.version }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Players Assigned</span>
|
||||
<span class="stat-value">{{ playlist.player_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<a href="{{ url_for('content.content_list') }}" class="btn btn-secondary">
|
||||
← Back to Playlists
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="content-grid">
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom: 20px;">📋 Playlist Content (Drag to Reorder)</h2>
|
||||
|
||||
{% 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: 100px;">Duration</th>
|
||||
<th style="width: 100px;">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 %}📁 Other{% endif %}
|
||||
</td>
|
||||
<td>{{ content._playlist_duration or content.duration }}s</td>
|
||||
<td>
|
||||
<form method="POST"
|
||||
action="{{ url_for('content.remove_content_from_playlist', playlist_id=playlist.id, content_id=content.id) }}"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('Remove from playlist?');">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 40px; color: #999;">
|
||||
<div style="font-size: 48px;">📭</div>
|
||||
<p>No content in playlist yet. Add content from the right panel.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom: 20px;">➕ Add Content</h2>
|
||||
|
||||
{% if available_content %}
|
||||
<div class="available-content">
|
||||
{% for content in available_content %}
|
||||
<div class="content-item">
|
||||
<div>
|
||||
<div>
|
||||
{% if content.content_type == 'image' %}📷
|
||||
{% elif content.content_type == 'video' %}🎥
|
||||
{% elif content.content_type == 'pdf' %}📄
|
||||
{% else %}📁{% endif %}
|
||||
{{ content.filename }}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #999;">
|
||||
{{ content.file_size_mb }} MB
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST"
|
||||
action="{{ url_for('content.add_content_to_playlist', playlist_id=playlist.id) }}"
|
||||
style="display: inline;">
|
||||
<input type="hidden" name="content_id" value="{{ content.id }}">
|
||||
<input type="hidden" name="duration" value="{{ content.duration }}">
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
+ Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 40px; color: #999;">
|
||||
<p>All available content has been added to this playlist!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let draggedElement = null;
|
||||
|
||||
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.style.opacity = '0.5';
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
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.style.opacity = '1';
|
||||
}
|
||||
|
||||
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("content.reorder_playlist_content", playlist_id=playlist.id) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content_ids: contentIds })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
alert('Error reordering: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user