Files
digiserver-v2/app/templates/content/content_list_new.html
DigiServer Developer 2db0033bc0 Add Docker support and improve PDF conversion
Features:
- Dockerfile for containerized deployment
- Docker Compose configuration with health checks
- Automated database initialization via entrypoint script
- Quick start script for easy Docker deployment
- Comprehensive Docker deployment documentation (DOCKER.md)
- Complete README with installation and usage instructions
- .dockerignore for optimized image builds

Improvements:
- PDF conversion now preserves orientation (portrait/landscape)
- PDF rendering at 300 DPI for sharp quality
- Maintains aspect ratio during conversion
- Compact media library view with image thumbnails
- Better media preview with scrollable gallery

Docker Features:
- Multi-stage build for smaller images
- Non-root user for security
- Health checks for container monitoring
- Volume mounts for persistent data
- Production-ready Gunicorn configuration
- Support for Redis caching (optional)
2025-11-17 21:05:49 +02:00

529 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Playlist Management - DigiServer v2{% endblock %}
{% block content %}
<style>
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.full-width {
grid-column: 1 / -1;
}
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px 8px 0 0;
margin: -1.5rem -1.5rem 1.5rem -1.5rem;
}
.card-header h2 {
margin: 0;
color: white;
}
.playlist-list {
max-height: 400px;
overflow-y: auto;
}
.playlist-item {
background: #f8f9fa;
padding: 15px;
margin-bottom: 10px;
border-radius: 6px;
border-left: 4px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
}
.playlist-item:hover {
background: #e9ecef;
transform: translateX(5px);
}
body.dark-mode .playlist-item {
background: #1a202c;
border-left-color: #7c3aed;
}
body.dark-mode .playlist-item:hover {
background: #2d3748;
}
.playlist-info h3 {
margin: 0 0 5px 0;
font-size: 18px;
}
.playlist-stats {
font-size: 14px;
color: #6c757d;
}
body.dark-mode .playlist-stats {
color: #a0aec0;
}
.playlist-actions {
display: flex;
gap: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
body.dark-mode .form-group label {
color: #e2e8f0;
}
body.dark-mode small {
color: #a0aec0;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
background: white;
color: #2d3748;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
body.dark-mode .form-control {
background: #1a202c;
border-color: #4a5568;
color: #e2e8f0;
}
body.dark-mode .form-control:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
}
textarea.form-control {
resize: vertical;
min-height: 80px;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-sm {
padding: 5px 10px;
font-size: 13px;
}
.upload-zone {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 30px;
text-align: center;
background: #f8f9fa;
transition: all 0.3s;
}
.upload-zone:hover {
border-color: #667eea;
background: #f0f2ff;
}
.upload-zone.drag-over {
border-color: #667eea;
background: #e7e9ff;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.file-input-wrapper input[type=file] {
position: absolute;
left: -9999px;
}
.media-library {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.media-item {
background: white;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 10px;
text-align: center;
transition: all 0.2s;
cursor: pointer;
}
.media-item:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
body.dark-mode .media-item {
background: #2d3748;
border-color: #4a5568;
}
body.dark-mode .media-item:hover {
border-color: #7c3aed;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.media-thumbnail {
background: #f0f0f0;
}
body.dark-mode .media-thumbnail {
background: #1a202c;
}
.media-icon {
font-size: 48px;
margin-bottom: 10px;
}
.media-name {
font-size: 12px;
word-break: break-word;
}
body.dark-mode .media-name {
color: #e2e8f0;
}
/* Table styling for dark mode */
body.dark-mode table thead tr {
background: #1a202c !important;
}
body.dark-mode table th {
color: #e2e8f0;
border-bottom-color: #4a5568 !important;
}
body.dark-mode table tbody tr {
border-bottom-color: #4a5568 !important;
}
body.dark-mode table td {
color: #e2e8f0;
}
body.dark-mode code {
background: #1a202c !important;
color: #e2e8f0;
}
</style>
<div class="container" style="max-width: 1400px;">
<h1 style="margin-bottom: 25px; display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 32px; height: 32px;">
Playlist Management
</h1>
<div class="main-grid">
<!-- Create/Manage Playlists Card -->
<div class="card">
<div class="card-header">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
Playlists
</h2>
</div>
<!-- Create New Playlist Form -->
<form method="POST" action="{{ url_for('content.create_playlist') }}" style="margin-bottom: 25px;">
<h3 style="margin-bottom: 15px;">Create New Playlist</h3>
<div class="form-group">
<label for="playlist_name">Playlist Name *</label>
<input type="text" name="name" id="playlist_name" class="form-control" required
placeholder="e.g., Main Lobby Display">
</div>
<div class="form-group">
<label for="playlist_orientation">Content Orientation *</label>
<select name="orientation" id="playlist_orientation" class="form-control" required>
<option value="Landscape">Landscape (Horizontal)</option>
<option value="Portrait">Portrait (Vertical)</option>
</select>
<small style="color: #6c757d; font-size: 12px; display: block; margin-top: 5px;">
Select the orientation that matches your display screens
</small>
</div>
<div class="form-group">
<label for="playlist_description">Description (Optional)</label>
<textarea name="description" id="playlist_description" class="form-control"
placeholder="Describe the purpose of this playlist..."></textarea>
</div>
<button type="submit" class="btn btn-primary">
Create Playlist
</button>
</form>
<hr style="margin: 25px 0;">
<!-- Existing Playlists -->
<h3 style="margin-bottom: 15px;">Existing Playlists</h3>
<div class="playlist-list">
{% if playlists %}
{% for playlist in playlists %}
<div class="playlist-item">
<div class="playlist-info">
<h3>{{ playlist.name }}</h3>
<div class="playlist-stats">
📊 {{ playlist.content_count }} items |
👥 {{ playlist.player_count }} players |
🔄 v{{ playlist.version }}
</div>
</div>
<div class="playlist-actions">
<a href="{{ url_for('content.manage_playlist_content', playlist_id=playlist.id) }}"
class="btn btn-primary btn-sm">
✏️ Manage
</a>
<form method="POST"
action="{{ url_for('content.delete_playlist', playlist_id=playlist.id) }}"
style="display: inline;"
onsubmit="return confirm('Delete playlist {{ playlist.name }}?');">
<button type="submit" class="btn btn-danger btn-sm" style="display: flex; align-items: center; gap: 0.3rem;">
<img src="{{ url_for('static', filename='icons/trash.svg') }}" alt="" style="width: 14px; height: 14px; filter: brightness(0) invert(1);">
Delete
</button>
</form>
</div>
</div>
{% endfor %}
{% else %}
<div style="text-align: center; padding: 40px; color: #999;">
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 64px; height: 64px; opacity: 0.3; margin-bottom: 10px;">
<p>No playlists yet. Create your first playlist above!</p>
</div>
{% endif %}
</div>
</div>
<!-- Upload Media Card -->
<div class="card">
<div class="card-header">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
Media Library
</h2>
</div>
<!-- Compact Upload Section -->
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; margin-bottom: 20px;">
<a href="{{ url_for('content.upload_media_page') }}" class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
Upload New Media
</a>
</div>
<!-- Media Library with Thumbnails -->
<h3 style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
<span>📚 Available Media ({{ media_files|length }})</span>
</h3>
<div class="media-library" style="max-height: 500px; overflow-y: auto;">
{% if media_files %}
{% for media in media_files %}
<div class="media-item" title="{{ media.filename }}">
{% if media.content_type == 'image' %}
<div class="media-thumbnail" style="width: 100%; height: 100px; overflow: hidden; border-radius: 6px; margin-bottom: 8px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
alt="{{ media.filename }}"
style="max-width: 100%; max-height: 100%; object-fit: cover;"
onerror="this.style.display='none'; this.parentElement.innerHTML='<span style=\'font-size: 48px;\'>📷</span>'">
</div>
{% elif media.content_type == 'video' %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
🎥
</div>
{% elif media.content_type == 'pdf' %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
📄
</div>
{% else %}
<div class="media-icon" style="height: 100px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 6px; margin-bottom: 8px;">
📁
</div>
{% endif %}
<div class="media-name" style="font-size: 11px; line-height: 1.3;">{{ media.filename[:25] }}{% if media.filename|length > 25 %}...{% endif %}</div>
<div style="font-size: 10px; color: #999; margin-top: 4px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
</div>
{% endfor %}
{% else %}
<div style="text-align: center; padding: 40px; color: #999; grid-column: 1 / -1;">
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
<p>No media files yet. Upload your first file!</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Assign Players to Playlists Card -->
<div class="card full-width">
<div class="card-header">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
Player Assignments
</h2>
</div>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f8f9fa; text-align: left;">
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Player Name</th>
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Hostname</th>
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Location</th>
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Assigned Playlist</th>
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Status</th>
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Actions</th>
</tr>
</thead>
<tbody>
{% for player in players %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 12px;"><strong>{{ player.name }}</strong></td>
<td style="padding: 12px;">
<code style="background: #f8f9fa; padding: 2px 6px; border-radius: 3px;">
{{ player.hostname }}
</code>
</td>
<td style="padding: 12px;">{{ player.location or '-' }}</td>
<td style="padding: 12px;">
<form method="POST" action="{{ url_for('content.assign_player_to_playlist', player_id=player.id) }}"
style="display: inline;">
<select name="playlist_id" class="form-control" style="width: auto; display: inline-block;"
onchange="this.form.submit()">
<option value="">No Playlist</option>
{% for playlist in playlists %}
<option value="{{ playlist.id }}"
{% if player.playlist_id == playlist.id %}selected{% endif %}>
{{ playlist.name }}
</option>
{% endfor %}
</select>
</form>
</td>
<td style="padding: 12px;">
{% if player.is_online %}
<span style="background: #28a745; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px;">
🟢 Online
</span>
{% else %}
<span style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px;">
⚫ Offline
</span>
{% endif %}
</td>
<td style="padding: 12px;">
<div style="display: flex; gap: 0.5rem;">
<a href="{{ url_for('players.player_page', player_id=player.id) }}"
class="btn btn-primary btn-sm">
⚙️ Manage
</a>
{% if player.playlist_id %}
<a href="{{ url_for('players.player_fullscreen', player_id=player.id) }}"
class="btn btn-success btn-sm" target="_blank"
title="View live content preview">
🖥️ Live
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
// File upload handling
const fileInput = document.getElementById('file-input');
const uploadZone = document.getElementById('upload-zone');
const fileList = document.getElementById('file-list');
const uploadBtn = document.getElementById('upload-btn');
fileInput.addEventListener('change', handleFiles);
// Drag and drop
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('drag-over');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('drag-over');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('drag-over');
fileInput.files = e.dataTransfer.files;
handleFiles();
});
function handleFiles() {
const files = fileInput.files;
fileList.innerHTML = '';
if (files.length > 0) {
uploadBtn.disabled = false;
const ul = document.createElement('ul');
ul.style.cssText = 'list-style: none; padding: 0;';
for (let file of files) {
const li = document.createElement('li');
li.style.cssText = 'padding: 8px; background: #f8f9fa; margin-bottom: 5px; border-radius: 4px;';
li.textContent = `📎 ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
ul.appendChild(li);
}
fileList.appendChild(ul);
} else {
uploadBtn.disabled = true;
}
}
</script>
{% endblock %}