551 lines
20 KiB
HTML
551 lines
20 KiB
HTML
{% 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;
|
||
}
|
||
|
||
/* Dark mode for upload section */
|
||
body.dark-mode .card > div[style*="background: #f8f9fa"] {
|
||
background: #2d3748 !important;
|
||
border: 1px solid #4a5568;
|
||
}
|
||
</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 Playlist 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);">
|
||
Create New Playlist
|
||
</h2>
|
||
</div>
|
||
|
||
<!-- Create New Playlist Form -->
|
||
<form method="POST" action="{{ url_for('content.create_playlist') }}">
|
||
<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>
|
||
</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>📚 Last 3 Added Media</span>
|
||
<small style="color: #6c757d; font-size: 0.85rem;">Total: {{ total_media_count }}</small>
|
||
</h3>
|
||
<div class="media-library" style="max-height: 350px; 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>
|
||
|
||
<!-- View All Media Button -->
|
||
{% if total_media_count > 3 %}
|
||
<div style="text-align: center; padding: 15px; border-top: 1px solid #dee2e6; margin-top: 10px;">
|
||
<a href="{{ url_for('content.media_library') }}" class="btn btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem;">
|
||
<span>📚</span>
|
||
View All Media ({{ total_media_count }} files)
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Existing 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/playlist.svg') }}" alt="" style="width: 24px; height: 24px; filter: brightness(0) invert(1);">
|
||
Existing Playlists
|
||
</h2>
|
||
</div>
|
||
|
||
<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>
|
||
|
||
<!-- 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 %}
|