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:
ske087
2025-11-13 21:00:07 +02:00
parent e5a00d19a5
commit 498c03ef00
37 changed files with 4240 additions and 840 deletions

View File

@@ -0,0 +1,492 @@
{% 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 %}