Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Content Library - DigiServer v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h1>Content Library</h1>
|
||||
<a href="{{ url_for('content.upload_content') }}" class="btn btn-success">+ Upload Content</a>
|
||||
</div>
|
||||
|
||||
{% if content_list %}
|
||||
<div class="card">
|
||||
<div style="margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 5px;">
|
||||
<strong>Total Files:</strong> {{ content_list|length }} |
|
||||
<strong>Total Assignments:</strong> {% set total = namespace(count=0) %}{% for item in content_list %}{% set total.count = total.count + item.player_count %}{% endfor %}{{ total.count }}
|
||||
</div>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background: #f8f9fa; text-align: left;">
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">File Name</th>
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Type</th>
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Duration</th>
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Size</th>
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Assigned To</th>
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Uploaded</th>
|
||||
<th style="padding: 12px; border-bottom: 2px solid #dee2e6;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in content_list %}
|
||||
<tr style="border-bottom: 1px solid #dee2e6;">
|
||||
<td style="padding: 12px;">
|
||||
<strong>{{ item.filename }}</strong>
|
||||
</td>
|
||||
<td style="padding: 12px;">
|
||||
{% if item.content_type == 'image' %}
|
||||
<span style="background: #28a745; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px;">📷 Image</span>
|
||||
{% elif item.content_type == 'video' %}
|
||||
<span style="background: #007bff; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px;">🎬 Video</span>
|
||||
{% elif item.content_type == 'pdf' %}
|
||||
<span style="background: #dc3545; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px;">📄 PDF</span>
|
||||
{% elif item.content_type == 'presentation' %}
|
||||
<span style="background: #ffc107; color: black; padding: 3px 8px; border-radius: 3px; font-size: 12px;">📊 PPT</span>
|
||||
{% else %}
|
||||
<span style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 3px; font-size: 12px;">📁 Other</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding: 12px;">
|
||||
{{ item.duration }}s
|
||||
</td>
|
||||
<td style="padding: 12px;">
|
||||
{{ item.file_size }} MB
|
||||
</td>
|
||||
<td style="padding: 12px;">
|
||||
{% if item.player_count == 0 %}
|
||||
<span style="color: #6c757d; font-style: italic;">Not assigned</span>
|
||||
{% else %}
|
||||
<div style="max-height: 100px; overflow-y: auto;">
|
||||
{% for player in item.players %}
|
||||
<div style="margin-bottom: 5px;">
|
||||
<strong>{{ player.name }}</strong>
|
||||
{% if player.group %}
|
||||
<span style="color: #6c757d; font-size: 12px;">({{ player.group }})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="margin-top: 5px;">
|
||||
<span style="background: #007bff; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">
|
||||
{{ item.player_count }} player{% if item.player_count != 1 %}s{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding: 12px;">
|
||||
<small style="color: #6c757d;">{{ item.uploaded_at | localtime }}</small>
|
||||
</td>
|
||||
<td style="padding: 12px;">
|
||||
{% if item.player_count > 0 %}
|
||||
{% set first_player = item.players[0] %}
|
||||
<a href="{{ url_for('players.player_page', player_id=first_player.id) }}"
|
||||
class="btn btn-primary btn-sm"
|
||||
title="Manage Playlist for {{ first_player.name }}"
|
||||
style="margin-bottom: 5px;">
|
||||
📝 Manage Playlist
|
||||
</a>
|
||||
{% if item.player_count > 1 %}
|
||||
<button onclick="showAllPlayers('{{ item.filename|replace("'", "\\'") }}', {{ item.players|tojson }})"
|
||||
class="btn btn-info btn-sm"
|
||||
title="View all players with this content">
|
||||
👥 View All ({{ item.player_count }})
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button onclick="deleteContent('{{ item.filename|replace("'", "\\'") }}')"
|
||||
class="btn btn-danger btn-sm"
|
||||
title="Delete this content from all playlists"
|
||||
style="margin-top: 5px;">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; padding: 15px; border-radius: 5px;">
|
||||
ℹ️ No content uploaded yet. <a href="{{ url_for('content.upload_content') }}" style="color: #0c5460; text-decoration: underline;">Upload your first content</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Modal for viewing all players -->
|
||||
<div id="playersModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="max-width: 600px; margin: 100px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<h2 id="modalTitle" style="margin-bottom: 20px; color: #2c3e50;">Players with this content</h2>
|
||||
<div id="playersList" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
<div style="text-align: center; margin-top: 20px;">
|
||||
<button type="button" class="btn" onclick="closePlayersModal()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function showAllPlayers(filename, players) {
|
||||
document.getElementById('modalTitle').textContent = 'Players with: ' + filename;
|
||||
|
||||
const playersList = document.getElementById('playersList');
|
||||
playersList.innerHTML = '<table style="width: 100%; border-collapse: collapse;">';
|
||||
playersList.innerHTML += '<thead><tr style="background: #f8f9fa;"><th style="padding: 10px; text-align: left;">Player Name</th><th style="padding: 10px; text-align: left;">Group</th><th style="padding: 10px; text-align: left;">Action</th></tr></thead><tbody>';
|
||||
|
||||
players.forEach(player => {
|
||||
playersList.innerHTML += `
|
||||
<tr style="border-bottom: 1px solid #dee2e6;">
|
||||
<td style="padding: 10px;"><strong>${player.name}</strong></td>
|
||||
<td style="padding: 10px;">${player.group || '-'}</td>
|
||||
<td style="padding: 10px;">
|
||||
<a href="/players/${player.id}" class="btn btn-sm" style="background: #007bff; color: white; padding: 5px 10px; text-decoration: none; border-radius: 3px;">
|
||||
Manage Playlist
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
playersList.innerHTML += '</tbody></table>';
|
||||
|
||||
document.getElementById('playersModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closePlayersModal() {
|
||||
document.getElementById('playersModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function deleteContent(filename) {
|
||||
if (confirm(`Are you sure you want to delete "${filename}"?\n\nThis will remove it from ALL player playlists!`)) {
|
||||
fetch('/content/delete-by-filename', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: filename
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Successfully deleted "${filename}" from ${data.deleted_count} playlist(s)`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error deleting content: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error deleting content: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('playersModal');
|
||||
if (event.target == modal) {
|
||||
closePlayersModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,550 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Content{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h2>Edit Content</h2>
|
||||
<p>Edit content functionality - placeholder</p>
|
||||
<a href="{{ url_for('content.list') }}" class="btn btn-secondary">Back to Content</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,719 @@
|
||||
{% 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;
|
||||
}
|
||||
|
||||
/* Audio toggle styles */
|
||||
.audio-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Duration spinner control */
|
||||
.duration-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.duration-display {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
padding: 6px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.duration-spinner button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.duration-spinner button:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.duration-spinner button:active {
|
||||
background: #e0e0e0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.duration-spinner button.btn-increase {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.duration-spinner button.btn-decrease {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.audio-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.audio-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.audio-label {
|
||||
font-size: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.audio-checkbox + .audio-label .audio-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.audio-checkbox + .audio-label .audio-off {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.audio-checkbox:checked + .audio-label .audio-on {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.audio-checkbox:checked + .audio-label .audio-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.audio-label:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
body.dark-mode .playlist-table th {
|
||||
background: #1a202c;
|
||||
color: #cbd5e0;
|
||||
border-bottom-color: #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .playlist-table td {
|
||||
border-bottom-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .draggable-row:hover {
|
||||
background: #1a202c;
|
||||
}
|
||||
|
||||
body.dark-mode .drag-handle {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
body.dark-mode .content-item {
|
||||
background: #1a202c;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .available-content {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Dark mode for duration spinner */
|
||||
body.dark-mode .duration-display {
|
||||
background: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .duration-spinner button {
|
||||
background: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .duration-spinner button:hover {
|
||||
background: #4a5568;
|
||||
border-color: #718096;
|
||||
}
|
||||
|
||||
body.dark-mode .duration-spinner button:active {
|
||||
background: #5a6a78;
|
||||
}
|
||||
|
||||
body.dark-mode .duration-spinner button.btn-increase {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
body.dark-mode .duration-spinner button.btn-decrease {
|
||||
color: #f56565;
|
||||
}
|
||||
</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">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 style="margin: 0;">📋 Playlist Content (Drag to Reorder)</h2>
|
||||
<button id="bulk-delete-btn" class="btn btn-danger" style="display: none;" onclick="bulkDeleteSelected()">
|
||||
🗑️ Delete Selected (<span id="selected-count">0</span>)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if playlist_content %}
|
||||
<table class="playlist-table" id="playlist-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)" title="Select all">
|
||||
</th>
|
||||
<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: 80px;">Audio</th>
|
||||
<th style="width: 80px;">Edit</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>
|
||||
<input type="checkbox" class="content-checkbox" data-content-id="{{ content.id }}" onchange="updateBulkDeleteButton()">
|
||||
</td>
|
||||
<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>
|
||||
<div class="duration-spinner">
|
||||
<button type="button"
|
||||
class="btn-decrease"
|
||||
onclick="event.stopPropagation(); changeDuration({{ content.id }}, -1)"
|
||||
onmousedown="event.stopPropagation()"
|
||||
title="Decrease duration by 1 second">
|
||||
⬇️
|
||||
</button>
|
||||
<div class="duration-display" id="duration-display-{{ content.id }}">
|
||||
{{ content._playlist_duration or content.duration }}s
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn-increase"
|
||||
onclick="event.stopPropagation(); changeDuration({{ content.id }}, 1)"
|
||||
onmousedown="event.stopPropagation()"
|
||||
title="Increase duration by 1 second">
|
||||
⬆️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if content.content_type == 'video' %}
|
||||
<label class="audio-toggle">
|
||||
<input type="checkbox"
|
||||
class="audio-checkbox"
|
||||
data-content-id="{{ content.id }}"
|
||||
{{ 'checked' if not content._playlist_muted else '' }}
|
||||
onchange="toggleAudio({{ content.id }}, this.checked)">
|
||||
<span class="audio-label">
|
||||
<span class="audio-on">🔊</span>
|
||||
<span class="audio-off">🔇</span>
|
||||
</span>
|
||||
</label>
|
||||
{% else %}
|
||||
<span style="color: #999;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if content.content_type in ['image', 'pdf'] %}
|
||||
<label class="audio-toggle">
|
||||
<input type="checkbox"
|
||||
class="edit-checkbox"
|
||||
data-content-id="{{ content.id }}"
|
||||
{{ 'checked' if content._playlist_edit_on_player_enabled else '' }}
|
||||
onchange="toggleEdit({{ content.id }}, this.checked)">
|
||||
<span class="audio-label">
|
||||
<span class="audio-on">✏️</span>
|
||||
<span class="audio-off">🔒</span>
|
||||
</span>
|
||||
</label>
|
||||
{% else %}
|
||||
<span style="color: #999;">—</span>
|
||||
{% endif %}
|
||||
</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);
|
||||
});
|
||||
}
|
||||
|
||||
// Change duration with spinner buttons
|
||||
function changeDuration(contentId, change) {
|
||||
const displayElement = document.getElementById(`duration-display-${contentId}`);
|
||||
const currentText = displayElement.textContent;
|
||||
const currentDuration = parseInt(currentText);
|
||||
const newDuration = currentDuration + change;
|
||||
|
||||
// Validate duration (minimum 1 second)
|
||||
if (newDuration < 1) {
|
||||
alert('Duration must be at least 1 second');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update display immediately for visual feedback
|
||||
displayElement.style.opacity = '0.7';
|
||||
displayElement.textContent = newDuration + 's';
|
||||
|
||||
// Save to server
|
||||
const playlistId = {{ playlist.id }};
|
||||
const url = `/content/playlist/${playlistId}/update-duration/${contentId}`;
|
||||
const formData = new FormData();
|
||||
formData.append('duration', newDuration);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Duration updated successfully');
|
||||
displayElement.style.opacity = '1';
|
||||
displayElement.style.color = '#28a745';
|
||||
setTimeout(() => {
|
||||
displayElement.style.color = '';
|
||||
}, 1000);
|
||||
} else {
|
||||
// Revert on error
|
||||
displayElement.textContent = currentDuration + 's';
|
||||
displayElement.style.opacity = '1';
|
||||
alert('Error updating duration: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Revert on error
|
||||
displayElement.textContent = currentDuration + 's';
|
||||
displayElement.style.opacity = '1';
|
||||
console.error('Error:', error);
|
||||
alert('Error updating duration');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAudio(contentId, enabled) {
|
||||
const muted = !enabled; // Checkbox is "enabled audio", but backend stores "muted"
|
||||
const playlistId = {{ playlist.id }};
|
||||
const url = `/content/playlist/${playlistId}/update-muted/${contentId}`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('muted', muted ? 'true' : 'false');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Audio setting updated:', enabled ? 'Enabled' : 'Muted');
|
||||
} else {
|
||||
alert('Error updating audio setting: ' + data.message);
|
||||
// Revert checkbox on error
|
||||
const checkbox = document.querySelector(`.audio-checkbox[data-content-id="${contentId}"]`);
|
||||
if (checkbox) checkbox.checked = !enabled;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error updating audio setting');
|
||||
// Revert checkbox on error
|
||||
const checkbox = document.querySelector(`.audio-checkbox[data-content-id="${contentId}"]`);
|
||||
if (checkbox) checkbox.checked = !enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEdit(contentId, enabled) {
|
||||
const playlistId = {{ playlist.id }};
|
||||
const url = `/content/playlist/${playlistId}/update-edit-enabled/${contentId}`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('edit_enabled', enabled ? 'true' : 'false');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log('Edit setting updated:', enabled ? 'Enabled' : 'Disabled');
|
||||
} else {
|
||||
alert('Error updating edit setting: ' + data.message);
|
||||
// Revert checkbox on error
|
||||
const checkbox = document.querySelector(`.edit-checkbox[data-content-id="${contentId}"]`);
|
||||
if (checkbox) checkbox.checked = !enabled;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error updating edit setting');
|
||||
// Revert checkbox on error
|
||||
const checkbox = document.querySelector(`.edit-checkbox[data-content-id="${contentId}"]`);
|
||||
if (checkbox) checkbox.checked = !enabled;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSelectAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('.content-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = checkbox.checked;
|
||||
});
|
||||
updateBulkDeleteButton();
|
||||
}
|
||||
|
||||
function updateBulkDeleteButton() {
|
||||
const checkboxes = document.querySelectorAll('.content-checkbox:checked');
|
||||
const count = checkboxes.length;
|
||||
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
|
||||
const selectedCount = document.getElementById('selected-count');
|
||||
|
||||
if (count > 0) {
|
||||
bulkDeleteBtn.style.display = 'block';
|
||||
selectedCount.textContent = count;
|
||||
} else {
|
||||
bulkDeleteBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update select-all checkbox state
|
||||
const allCheckboxes = document.querySelectorAll('.content-checkbox');
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.checked = allCheckboxes.length > 0 && count === allCheckboxes.length;
|
||||
selectAllCheckbox.indeterminate = count > 0 && count < allCheckboxes.length;
|
||||
}
|
||||
}
|
||||
|
||||
function bulkDeleteSelected() {
|
||||
const checkboxes = document.querySelectorAll('.content-checkbox:checked');
|
||||
const contentIds = Array.from(checkboxes).map(cb => parseInt(cb.dataset.contentId));
|
||||
|
||||
if (contentIds.length === 0) {
|
||||
alert('No items selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMsg = `Are you sure you want to remove ${contentIds.length} item(s) from this playlist?`;
|
||||
if (!confirm(confirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
|
||||
const originalText = bulkDeleteBtn.innerHTML;
|
||||
bulkDeleteBtn.disabled = true;
|
||||
bulkDeleteBtn.innerHTML = '⏳ Removing...';
|
||||
|
||||
fetch('{{ url_for("content.bulk_remove_from_playlist", 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) {
|
||||
// Reload page to show updated playlist
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error removing items: ' + data.message);
|
||||
bulkDeleteBtn.disabled = false;
|
||||
bulkDeleteBtn.innerHTML = originalText;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error removing items from playlist');
|
||||
bulkDeleteBtn.disabled = false;
|
||||
bulkDeleteBtn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,806 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Media Library - DigiServer v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.media-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.media-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
body.dark-mode .media-card {
|
||||
background: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.media-thumbnail {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
body.dark-mode .media-thumbnail {
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.media-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.media-icon {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.media-info {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
body.dark-mode .media-info {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.media-filename {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.dark-mode .media-filename {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.media-card:hover .delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #a02834;
|
||||
}
|
||||
|
||||
.playlist-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.playlist-badge.in-use {
|
||||
background: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.playlist-badge.unused {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.dark-mode .playlist-badge.in-use {
|
||||
background: #856404;
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
body.dark-mode .upload-section {
|
||||
background: #2d3748 !important;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .playlist-badge.unused {
|
||||
background: #1a4d2e;
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-badge.image { background: #d4edda; color: #155724; }
|
||||
.type-badge.video { background: #cce5ff; color: #004085; }
|
||||
.type-badge.pdf { background: #fff3cd; color: #856404; }
|
||||
.type-badge.pptx { background: #f8d7da; color: #721c24; }
|
||||
|
||||
body.dark-mode .type-badge.image { background: #1a4d2e; color: #86efac; }
|
||||
body.dark-mode .type-badge.video { background: #1e3a5f; color: #93c5fd; }
|
||||
body.dark-mode .type-badge.pdf { background: #4a3800; color: #fbbf24; }
|
||||
body.dark-mode .type-badge.pptx { background: #4a1a1a; color: #fca5a5; }
|
||||
|
||||
.stats-box {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.dark-mode .stat-item {
|
||||
background: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
body.dark-mode .stat-value {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
body.dark-mode .stat-label {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 30px 0 15px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #7c3aed;
|
||||
}
|
||||
|
||||
body.dark-mode .section-header {
|
||||
border-bottom-color: #a78bfa;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
body.dark-mode .section-header h2 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h1 style="display: inline-block; margin-right: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 32px; height: 32px;">
|
||||
📚 Media Library
|
||||
</h1>
|
||||
<a href="{{ url_for('content.content_list') }}" class="btn" style="float: right; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||
Back to Playlists
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-box">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ media_files|length }}</div>
|
||||
<div class="stat-label">Total Files</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ images|length }}</div>
|
||||
<div class="stat-label">Images</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ videos|length }}</div>
|
||||
<div class="stat-label">Videos</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ pdfs|length }}</div>
|
||||
<div class="stat-label">PDFs</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ presentations|length }}</div>
|
||||
<div class="stat-label">Presentations</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<div class="upload-section" style="text-align: center; padding: 20px; background: #f8f9fa; border-radius: 8px; margin-bottom: 30px;">
|
||||
<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: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||
Upload New Media
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Images Section -->
|
||||
{% if images %}
|
||||
<div class="section-header">
|
||||
<span style="font-size: 32px;">📷</span>
|
||||
<h2>Images ({{ images|length }})</h2>
|
||||
</div>
|
||||
<div class="media-grid">
|
||||
{% for media in images %}
|
||||
<div class="media-card">
|
||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||
<div class="media-thumbnail">
|
||||
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
|
||||
alt="{{ media.filename }}"
|
||||
onerror="this.style.display='none'; this.parentElement.innerHTML='<span class=\'media-icon\'>📷</span>'">
|
||||
</div>
|
||||
<div class="media-filename" title="{{ media.filename }}">{{ media.filename }}</div>
|
||||
<div class="media-info">
|
||||
<span class="type-badge image">Image</span>
|
||||
<div style="margin-top: 5px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
|
||||
<div style="font-size: 10px; margin-top: 3px;">{{ media.uploaded_at | localtime('%Y-%m-%d') }}</div>
|
||||
{% if media.playlists.count() > 0 %}
|
||||
<div class="playlist-badge in-use">📋 In {{ media.playlists.count() }} playlist(s)</div>
|
||||
{% else %}
|
||||
<div class="playlist-badge unused">✓ Not in use</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Videos Section -->
|
||||
{% if videos %}
|
||||
<div class="section-header">
|
||||
<span style="font-size: 32px;">🎥</span>
|
||||
<h2>Videos ({{ videos|length }})</h2>
|
||||
</div>
|
||||
<div class="media-grid">
|
||||
{% for media in videos %}
|
||||
<div class="media-card">
|
||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||
<div class="media-thumbnail">
|
||||
<span class="media-icon">🎥</span>
|
||||
</div>
|
||||
<div class="media-filename" title="{{ media.filename }}">{{ media.filename }}</div>
|
||||
<div class="media-info">
|
||||
<span class="type-badge video">Video</span>
|
||||
<div style="margin-top: 5px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
|
||||
<div style="font-size: 10px; margin-top: 3px;">{{ media.uploaded_at | localtime('%Y-%m-%d') }}</div>
|
||||
{% if media.playlists.count() > 0 %}
|
||||
<div class="playlist-badge in-use">📋 In {{ media.playlists.count() }} playlist(s)</div>
|
||||
{% else %}
|
||||
<div class="playlist-badge unused">✓ Not in use</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- PDFs Section -->
|
||||
{% if pdfs %}
|
||||
<div class="section-header">
|
||||
<span style="font-size: 32px;">📄</span>
|
||||
<h2>PDFs ({{ pdfs|length }})</h2>
|
||||
</div>
|
||||
<div class="media-grid">
|
||||
{% for media in pdfs %}
|
||||
<div class="media-card">
|
||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||
<div class="media-thumbnail">
|
||||
<span class="media-icon">📄</span>
|
||||
</div>
|
||||
<div class="media-filename" title="{{ media.filename }}">{{ media.filename }}</div>
|
||||
<div class="media-info">
|
||||
<span class="type-badge pdf">PDF</span>
|
||||
<div style="margin-top: 5px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
|
||||
<div style="font-size: 10px; margin-top: 3px;">{{ media.uploaded_at | localtime('%Y-%m-%d') }}</div>
|
||||
{% if media.playlists.count() > 0 %}
|
||||
<div class="playlist-badge in-use">📋 In {{ media.playlists.count() }} playlist(s)</div>
|
||||
{% else %}
|
||||
<div class="playlist-badge unused">✓ Not in use</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Presentations Section -->
|
||||
{% if presentations %}
|
||||
<div class="section-header">
|
||||
<span style="font-size: 32px;">📊</span>
|
||||
<h2>Presentations ({{ presentations|length }})</h2>
|
||||
</div>
|
||||
<div class="media-grid">
|
||||
{% for media in presentations %}
|
||||
<div class="media-card">
|
||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||
<div class="media-thumbnail">
|
||||
<span class="media-icon">📊</span>
|
||||
</div>
|
||||
<div class="media-filename" title="{{ media.filename }}">{{ media.filename }}</div>
|
||||
<div class="media-info">
|
||||
<span class="type-badge pptx">PPTX</span>
|
||||
<div style="margin-top: 5px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
|
||||
<div style="font-size: 10px; margin-top: 3px;">{{ media.uploaded_at | localtime('%Y-%m-%d') }}</div>
|
||||
{% if media.playlists.count() > 0 %}
|
||||
<div class="playlist-badge in-use">📋 In {{ media.playlists.count() }} playlist(s)</div>
|
||||
{% else %}
|
||||
<div class="playlist-badge unused">✓ Not in use</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Others Section -->
|
||||
{% if others %}
|
||||
<div class="section-header">
|
||||
<span style="font-size: 32px;">📁</span>
|
||||
<h2>Other Files ({{ others|length }})</h2>
|
||||
</div>
|
||||
<div class="media-grid">
|
||||
{% for media in others %}
|
||||
<div class="media-card">
|
||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||
<div class="media-thumbnail">
|
||||
<span class="media-icon">📁</span>
|
||||
</div>
|
||||
<div class="media-filename" title="{{ media.filename }}">{{ media.filename }}</div>
|
||||
<div class="media-info">
|
||||
<span class="type-badge">{{ media.content_type }}</span>
|
||||
<div style="margin-top: 5px;">{{ "%.1f"|format(media.file_size_mb) }} MB</div>
|
||||
<div style="font-size: 10px; margin-top: 3px;">{{ media.uploaded_at | localtime('%Y-%m-%d') }}</div>
|
||||
{% if media.playlists.count() > 0 %}
|
||||
<div class="playlist-badge in-use">📋 In {{ media.playlists.count() }} playlist(s)</div>
|
||||
{% else %}
|
||||
<div class="playlist-badge unused">✓ Not in use</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not media_files %}
|
||||
<div style="text-align: center; padding: 80px 20px;">
|
||||
<div style="font-size: 96px; margin-bottom: 20px;">📭</div>
|
||||
<h2 style="color: #6c757d;">No Media Files Yet</h2>
|
||||
<p style="color: #999; margin-bottom: 30px;">Start by uploading your first media file!</p>
|
||||
<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: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||
Upload Media
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="deleteModal" class="delete-modal">
|
||||
<div class="delete-modal-content">
|
||||
<div class="delete-modal-header">
|
||||
<span class="delete-icon">⚠️</span>
|
||||
<h2>Confirm Delete</h2>
|
||||
</div>
|
||||
|
||||
<div class="delete-modal-body">
|
||||
<p class="delete-question">
|
||||
Are you sure you want to delete <strong id="deleteFilename" class="delete-filename"></strong>?
|
||||
</p>
|
||||
|
||||
<div id="playlistWarning" class="warning-box warning-playlist">
|
||||
<div class="warning-header">
|
||||
<span>⚠️</span>
|
||||
<strong>Playlist Warning</strong>
|
||||
</div>
|
||||
<p>This file is used in <strong id="playlistCount"></strong> playlist(s). Deleting it will remove it from all playlists and increment their version numbers.</p>
|
||||
</div>
|
||||
|
||||
<div id="editWarning" class="warning-box warning-edit">
|
||||
<div class="warning-header">
|
||||
<span>✏️</span>
|
||||
<strong>Edited Versions</strong>
|
||||
</div>
|
||||
<p>This file has <strong id="editCount"></strong> edited version(s) from player devices. All edited versions and their metadata will also be permanently deleted.</p>
|
||||
</div>
|
||||
|
||||
<div class="delete-final-warning">
|
||||
<strong>⚠️ This action cannot be undone!</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="delete-modal-footer">
|
||||
<button onclick="closeDeleteModal()" class="btn btn-cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<form id="deleteForm" method="POST">
|
||||
<button type="submit" class="btn btn-delete">
|
||||
Yes, Delete File
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Delete Modal - Light Mode Styles */
|
||||
.delete-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.delete-modal-content {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
margin: 8% auto;
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 550px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-modal-header {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
padding: 24px 30px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
font-size: 2rem;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.delete-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.delete-modal-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.delete-question {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 20px 0;
|
||||
color: #2d3748;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.delete-filename {
|
||||
color: #dc3545;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
display: none;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.warning-box:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.warning-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.warning-header span {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.warning-header strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.warning-box p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.warning-playlist {
|
||||
background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.warning-playlist .warning-header,
|
||||
.warning-playlist p {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.warning-edit {
|
||||
background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%);
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.warning-edit .warning-header,
|
||||
.warning-edit p {
|
||||
color: #5b21b6;
|
||||
}
|
||||
|
||||
.delete-final-warning {
|
||||
background: linear-gradient(135deg, #fee 0%, #fdd 100%);
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 20px 0 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-final-warning strong {
|
||||
color: #dc3545;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-modal-footer {
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 0 0 16px 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.delete-modal-footer form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: linear-gradient(135deg, #5a6268 0%, #4e555b 100%);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
|
||||
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Delete Modal - Dark Mode Styles */
|
||||
body.dark-mode .delete-modal {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
body.dark-mode .delete-modal-content {
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||
border: 1px solid #4a5568;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
body.dark-mode .delete-modal-header {
|
||||
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||
box-shadow: 0 4px 12px rgba(185, 28, 28, 0.4);
|
||||
}
|
||||
|
||||
body.dark-mode .delete-question {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .delete-filename {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
body.dark-mode .warning-playlist {
|
||||
background: linear-gradient(135deg, #422006 0%, #713f12 100%);
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
body.dark-mode .warning-playlist .warning-header,
|
||||
body.dark-mode .warning-playlist p {
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
body.dark-mode .warning-edit {
|
||||
background: linear-gradient(135deg, #3b0764 0%, #581c87 100%);
|
||||
border-color: #a78bfa;
|
||||
}
|
||||
|
||||
body.dark-mode .warning-edit .warning-header,
|
||||
body.dark-mode .warning-edit p {
|
||||
color: #ddd6fe;
|
||||
}
|
||||
|
||||
body.dark-mode .delete-final-warning {
|
||||
background: linear-gradient(135deg, #450a0a 0%, #7f1d1d 100%);
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
body.dark-mode .delete-final-warning strong {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
body.dark-mode .delete-modal-footer {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-top: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-cancel {
|
||||
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
body.dark-mode .btn-cancel:hover {
|
||||
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body.dark-mode .btn-delete {
|
||||
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||
box-shadow: 0 2px 6px rgba(185, 28, 28, 0.4);
|
||||
}
|
||||
|
||||
body.dark-mode .btn-delete:hover {
|
||||
background: linear-gradient(135deg, #991b1b 0%, #7f1d1d 100%);
|
||||
box-shadow: 0 4px 12px rgba(185, 28, 28, 0.6);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let deleteMediaId = null;
|
||||
|
||||
function confirmDelete(mediaId, filename, playlistCount, editCount) {
|
||||
deleteMediaId = mediaId;
|
||||
document.getElementById('deleteFilename').textContent = filename;
|
||||
document.getElementById('deleteForm').action = "{{ url_for('content.delete_media', media_id=0) }}".replace('/0', '/' + mediaId);
|
||||
|
||||
// Show playlist warning if file is in use
|
||||
if (playlistCount > 0) {
|
||||
document.getElementById('playlistWarning').style.display = 'block';
|
||||
document.getElementById('playlistCount').textContent = playlistCount;
|
||||
} else {
|
||||
document.getElementById('playlistWarning').style.display = 'none';
|
||||
}
|
||||
|
||||
// Show edit versions warning if file has been edited
|
||||
if (editCount > 0) {
|
||||
document.getElementById('editWarning').style.display = 'block';
|
||||
document.getElementById('editCount').textContent = editCount;
|
||||
} else {
|
||||
document.getElementById('editWarning').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('deleteModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').style.display = 'none';
|
||||
deleteMediaId = null;
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('deleteModal');
|
||||
if (event.target == modal) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal with ESC key
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,278 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upload Content - DigiServer v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container" style="max-width: 1200px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h1>Upload Content</h1>
|
||||
</div>
|
||||
|
||||
<form id="upload-form" method="POST" enctype="multipart/form-data" onsubmit="handleFormSubmit(event)">
|
||||
<input type="hidden" name="return_url" value="{{ return_url or url_for('content.content_list') }}">
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3 style="margin-bottom: 15px;">Select Player</h3>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Player:</label>
|
||||
<select name="player_id" id="player_id" class="form-control" required>
|
||||
<option value="" disabled {% if not selected_player_id %}selected{% endif %}>Select a Player</option>
|
||||
{% for player in players %}
|
||||
<option value="{{ player.id }}" {% if selected_player_id == player.id %}selected{% endif %}>
|
||||
{{ player.name }} - {{ player.location or 'No location' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3 style="margin-bottom: 15px;">Media Details</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Media Type:</label>
|
||||
<select name="media_type" id="media_type" class="form-control" required onchange="handleMediaTypeChange()">
|
||||
<option value="image">Image (JPG, PNG, GIF)</option>
|
||||
<option value="video">Video (MP4, AVI, MOV)</option>
|
||||
<option value="pdf">PDF Document</option>
|
||||
<option value="ppt">PowerPoint (PPT/PPTX)</option>
|
||||
</select>
|
||||
<small style="color: #6c757d; display: block; margin-top: 5px;" id="media-type-hint">
|
||||
Images will be displayed as-is
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Duration (seconds):</label>
|
||||
<input type="number" name="duration" id="duration" class="form-control" required min="1" value="10">
|
||||
<small style="color: #6c757d; display: block; margin-top: 5px;">
|
||||
How long to display each image/slide (videos use actual length)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Files:</label>
|
||||
<input type="file" name="files" id="files" class="form-control" multiple required
|
||||
accept="image/*,video/*,.pdf,.ppt,.pptx" onchange="handleFileChange()">
|
||||
<small style="color: #6c757d; display: block; margin-top: 5px;">
|
||||
Select multiple files. Supported: JPG, PNG, GIF, MP4, PDF, PPT, PPTX
|
||||
</small>
|
||||
<div id="file-list" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<button type="submit" id="submit-button" class="btn btn-success" style="padding: 10px 30px; font-size: 16px;">
|
||||
📤 Upload Files
|
||||
</button>
|
||||
<a href="{{ return_url or url_for('content.content_list') }}" class="btn" style="padding: 10px 30px; font-size: 16px;">
|
||||
← Back
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Status Updates -->
|
||||
<div id="statusModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="max-width: 800px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<h2 style="margin-bottom: 20px; color: #2c3e50;">Processing Files</h2>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p id="status-message" style="font-size: 16px; color: #555;">Uploading and processing your files. Please wait...</p>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<label style="display: block; margin-bottom: 10px; font-weight: bold;">File Processing Progress</label>
|
||||
<div style="width: 100%; height: 30px; background: #e9ecef; border-radius: 5px; overflow: hidden;">
|
||||
<div id="progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #007bff, #0056b3); transition: width 0.3s ease; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 14px;">
|
||||
0%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px;">
|
||||
<button type="button" class="btn" onclick="closeModal()" disabled id="close-modal-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let progressInterval = null;
|
||||
let sessionId = null;
|
||||
let returnUrl = '{{ return_url or url_for("content.content_list") }}';
|
||||
|
||||
function generateSessionId() {
|
||||
return 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
sessionId = generateSessionId();
|
||||
const form = document.getElementById('upload-form');
|
||||
let sessionInput = document.getElementById('session_id_input');
|
||||
if (!sessionInput) {
|
||||
sessionInput = document.createElement('input');
|
||||
sessionInput.type = 'hidden';
|
||||
sessionInput.name = 'session_id';
|
||||
sessionInput.id = 'session_id_input';
|
||||
form.appendChild(sessionInput);
|
||||
}
|
||||
sessionInput.value = sessionId;
|
||||
|
||||
showStatusModal();
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
console.log('Form submitted successfully');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Form submission error:', error);
|
||||
document.getElementById('status-message').textContent = 'Upload failed: ' + error.message;
|
||||
document.getElementById('progress-bar').style.background = '#dc3545';
|
||||
document.getElementById('close-modal-btn').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showStatusModal() {
|
||||
const modal = document.getElementById('statusModal');
|
||||
modal.style.display = 'block';
|
||||
|
||||
const mediaType = document.getElementById('media_type').value;
|
||||
const statusMessage = document.getElementById('status-message');
|
||||
|
||||
switch(mediaType) {
|
||||
case 'image':
|
||||
statusMessage.textContent = 'Uploading images...';
|
||||
break;
|
||||
case 'video':
|
||||
statusMessage.textContent = 'Uploading and converting video. This may take several minutes...';
|
||||
break;
|
||||
case 'pdf':
|
||||
statusMessage.textContent = 'Uploading and converting PDF to images...';
|
||||
break;
|
||||
case 'ppt':
|
||||
statusMessage.textContent = 'Uploading and converting PowerPoint to images...';
|
||||
break;
|
||||
default:
|
||||
statusMessage.textContent = 'Uploading and processing your files. Please wait...';
|
||||
}
|
||||
|
||||
pollUploadProgress();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('statusModal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
window.location.href = returnUrl;
|
||||
}
|
||||
|
||||
function pollUploadProgress() {
|
||||
progressInterval = setInterval(() => {
|
||||
fetch(`/api/upload-progress/${sessionId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
progressBar.style.width = `${data.progress}%`;
|
||||
progressBar.textContent = `${data.progress}%`;
|
||||
|
||||
document.getElementById('status-message').textContent = data.message;
|
||||
|
||||
if (data.status === 'complete' || data.status === 'error') {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
|
||||
const closeBtn = document.getElementById('close-modal-btn');
|
||||
closeBtn.disabled = false;
|
||||
|
||||
if (data.status === 'complete') {
|
||||
progressBar.style.background = '#28a745';
|
||||
setTimeout(() => closeModal(), 2000);
|
||||
} else if (data.status === 'error') {
|
||||
progressBar.style.background = '#dc3545';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error fetching progress:', error));
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleMediaTypeChange() {
|
||||
const mediaType = document.getElementById('media_type').value;
|
||||
const hint = document.getElementById('media-type-hint');
|
||||
|
||||
switch(mediaType) {
|
||||
case 'image':
|
||||
hint.textContent = 'Images will be displayed as-is';
|
||||
break;
|
||||
case 'video':
|
||||
hint.textContent = 'Videos will be converted to optimized format';
|
||||
break;
|
||||
case 'pdf':
|
||||
hint.textContent = 'PDF will be converted to images (one per page)';
|
||||
break;
|
||||
case 'ppt':
|
||||
hint.textContent = 'PowerPoint will be converted to images (one per slide)';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileChange() {
|
||||
const filesInput = document.getElementById('files');
|
||||
const fileList = document.getElementById('file-list');
|
||||
const mediaType = document.getElementById('media_type').value;
|
||||
const durationInput = document.getElementById('duration');
|
||||
|
||||
fileList.innerHTML = '';
|
||||
if (filesInput.files.length > 0) {
|
||||
fileList.innerHTML = '<strong>Selected files:</strong><ul style="margin: 5px 0; padding-left: 20px;">';
|
||||
for (let i = 0; i < filesInput.files.length; i++) {
|
||||
const file = filesInput.files[i];
|
||||
const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
|
||||
fileList.innerHTML += `<li>${file.name} (${sizeMB} MB)</li>`;
|
||||
}
|
||||
fileList.innerHTML += '</ul>';
|
||||
}
|
||||
|
||||
if (mediaType === 'video' && filesInput.files.length > 0) {
|
||||
const file = filesInput.files[0];
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.onloadedmetadata = function() {
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
const duration = Math.round(video.duration);
|
||||
durationInput.value = duration;
|
||||
};
|
||||
video.src = URL.createObjectURL(file);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,514 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upload Media - DigiServer v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.upload-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
border: 2px dashed #ced4da;
|
||||
border-radius: 8px;
|
||||
padding: 25px 20px;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.dark-mode .upload-zone {
|
||||
background: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.upload-zone:hover {
|
||||
border-color: #667eea;
|
||||
background: #f0f2ff;
|
||||
}
|
||||
|
||||
body.dark-mode .upload-zone:hover {
|
||||
border-color: #7c3aed;
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.upload-zone.dragover {
|
||||
border-color: #667eea;
|
||||
background: #e8ebff;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
body.dark-mode .upload-zone.dragover {
|
||||
border-color: #7c3aed;
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 15px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
body.dark-mode .file-item {
|
||||
background: #2d3748;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.remove-file {
|
||||
cursor: pointer;
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.remove-file:hover {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.dark-mode .form-group label {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body.dark-mode .form-control {
|
||||
background: #1a202c;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.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:focus {
|
||||
border-color: #7c3aed;
|
||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.btn-upload {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-upload:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-upload:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Dark mode text colors */
|
||||
body.dark-mode h1,
|
||||
body.dark-mode h2,
|
||||
body.dark-mode h3 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode p,
|
||||
body.dark-mode small {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
body.dark-mode .file-info > div > div {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .file-info > div > div:last-child {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.playlist-selector {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
body.dark-mode .playlist-selector {
|
||||
background: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
.playlist-selector.selected {
|
||||
border-color: #667eea;
|
||||
background: #f0f2ff;
|
||||
}
|
||||
|
||||
body.dark-mode .playlist-selector.selected {
|
||||
border-color: #7c3aed;
|
||||
background: #2d3748;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="upload-container">
|
||||
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1 style="display: flex; align-items: center; gap: 0.5rem; font-size: 24px; margin: 0;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 28px; height: 28px;">
|
||||
Upload Media
|
||||
</h1>
|
||||
<a href="{{ url_for('content.content_list') }}" class="btn" style="display: flex; align-items: center; gap: 0.5rem; padding: 8px 16px;">
|
||||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
|
||||
Back to Playlists
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form id="upload-form" method="POST" action="{{ url_for('content.upload_media') }}" enctype="multipart/form-data">
|
||||
|
||||
<!-- Compact Two-Column Layout -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||
|
||||
<!-- Left Column: Upload Zone -->
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom: 15px; font-size: 18px; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 20px; height: 20px;">
|
||||
Select Files
|
||||
</h2>
|
||||
|
||||
<div class="upload-zone" id="upload-zone">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 48px; height: 48px; opacity: 0.3; margin-bottom: 10px;">
|
||||
<h3 style="margin-bottom: 8px; font-size: 16px;">Drag & Drop</h3>
|
||||
<p style="color: #6c757d; margin: 8px 0; font-size: 13px;">or</p>
|
||||
<div class="file-input-wrapper">
|
||||
<label for="file-input" class="btn btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem; padding: 8px 16px; font-size: 14px;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 16px; height: 16px; filter: brightness(0) invert(1);">
|
||||
Browse
|
||||
</label>
|
||||
<input type="file" id="file-input" name="files" multiple
|
||||
accept="image/*,video/*,.pdf,.ppt,.pptx">
|
||||
</div>
|
||||
<p style="font-size: 11px; color: #999; margin-top: 12px; line-height: 1.4;">
|
||||
<strong>Supported:</strong> JPG, PNG, GIF, MP4, AVI, MOV, PDF, PPT
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="file-list" class="file-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Settings -->
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom: 15px; font-size: 18px; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 20px; height: 20px;">
|
||||
Upload Settings
|
||||
</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playlist_id">Target Playlist (Optional)</label>
|
||||
<select name="playlist_id" id="playlist_id" class="form-control">
|
||||
<option value="">-- Media Library Only --</option>
|
||||
{% for playlist in playlists %}
|
||||
<option value="{{ playlist.id }}">
|
||||
{{ playlist.name }} ({{ playlist.orientation }}) - {{ playlist.content_count }} items
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
|
||||
💡 Add to playlists later if needed
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="content_type">Media Type</label>
|
||||
<select name="content_type" id="content_type" class="form-control">
|
||||
<option value="image">Image</option>
|
||||
<option value="video">Video</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="pptx">PPTX</option>
|
||||
</select>
|
||||
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
|
||||
Auto-detected from file extension
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="duration">Default Duration (seconds)</label>
|
||||
<input type="number" name="duration" id="duration" class="form-control"
|
||||
value="10" min="1" max="300">
|
||||
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
|
||||
Display time for images and PDFs
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; cursor: pointer; user-select: none;">
|
||||
<input type="checkbox" name="edit_on_player_enabled" id="edit_on_player_enabled"
|
||||
value="1" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
|
||||
<span>Allow editing on player (PDF, Images, PPTX)</span>
|
||||
</label>
|
||||
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
|
||||
✏️ Enable local editing of this media on the player device
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<button type="submit" class="btn-upload" id="upload-btn" disabled style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 20px; padding: 12px 30px;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||
Upload Files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const uploadZone = document.getElementById('upload-zone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const fileList = document.getElementById('file-list');
|
||||
const uploadBtn = document.getElementById('upload-btn');
|
||||
const uploadForm = document.getElementById('upload-form');
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadZone.addEventListener(eventName, preventDefaults, false);
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Highlight drop zone when item is dragged over it
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadZone.addEventListener(eventName, () => {
|
||||
uploadZone.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadZone.addEventListener(eventName, () => {
|
||||
uploadZone.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle dropped files
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
handleFiles(files);
|
||||
});
|
||||
|
||||
// Handle file input
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
console.log('File input changed, files:', e.target.files.length);
|
||||
if (e.target.files.length > 0) {
|
||||
handleFiles(e.target.files);
|
||||
}
|
||||
});
|
||||
|
||||
// Click upload zone to trigger file input (but not if clicking on the label)
|
||||
uploadZone.addEventListener('click', (e) => {
|
||||
// Don't trigger if clicking on the label or button
|
||||
if (e.target.tagName === 'LABEL' || e.target.closest('label') || e.target.tagName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
console.log('handleFiles called with', files.length, 'file(s)');
|
||||
selectedFiles = Array.from(files);
|
||||
displayFiles();
|
||||
uploadBtn.disabled = selectedFiles.length === 0;
|
||||
|
||||
// Auto-detect media type and duration from first file
|
||||
if (selectedFiles.length > 0) {
|
||||
console.log('Auto-detecting for first file:', selectedFiles[0].name);
|
||||
autoDetectMediaType(selectedFiles[0]);
|
||||
autoDetectDuration(selectedFiles[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function autoDetectMediaType(file) {
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
const contentTypeSelect = document.getElementById('content_type');
|
||||
|
||||
console.log('Auto-detecting media type for:', file.name, 'Extension:', ext);
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext)) {
|
||||
contentTypeSelect.value = 'image';
|
||||
console.log('Set type to: image');
|
||||
} else if (['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv'].includes(ext)) {
|
||||
contentTypeSelect.value = 'video';
|
||||
console.log('Set type to: video');
|
||||
} else if (ext === 'pdf') {
|
||||
contentTypeSelect.value = 'pdf';
|
||||
console.log('Set type to: pdf');
|
||||
} else if (['ppt', 'pptx'].includes(ext)) {
|
||||
contentTypeSelect.value = 'pptx';
|
||||
console.log('Set type to: pptx');
|
||||
}
|
||||
}
|
||||
|
||||
function autoDetectDuration(file) {
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
const durationInput = document.getElementById('duration');
|
||||
|
||||
console.log('Auto-detecting duration for:', file.name, 'Extension:', ext);
|
||||
|
||||
// For videos, try to get actual duration
|
||||
if (['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv'].includes(ext)) {
|
||||
console.log('Processing as video...');
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
|
||||
video.onloadedmetadata = function() {
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
const duration = Math.ceil(video.duration);
|
||||
console.log('Video duration detected:', duration, 'seconds');
|
||||
if (duration && duration > 0) {
|
||||
durationInput.value = duration;
|
||||
}
|
||||
};
|
||||
|
||||
video.onerror = function() {
|
||||
console.log('Video loading error, using default 30s');
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
durationInput.value = 30; // Default for videos if can't read duration
|
||||
};
|
||||
|
||||
video.src = URL.createObjectURL(file);
|
||||
} else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext)) {
|
||||
// Images: default 10 seconds
|
||||
console.log('Setting image duration: 10s');
|
||||
durationInput.value = 10;
|
||||
} else if (ext === 'pdf') {
|
||||
// PDFs: default 15 seconds per page (estimate)
|
||||
console.log('Setting PDF duration: 15s');
|
||||
durationInput.value = 15;
|
||||
} else if (['ppt', 'pptx'].includes(ext)) {
|
||||
// Presentations: default 20 seconds per slide (estimate)
|
||||
console.log('Setting presentation duration: 20s');
|
||||
durationInput.value = 20;
|
||||
}
|
||||
}
|
||||
|
||||
function displayFiles() {
|
||||
fileList.innerHTML = '';
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFiles.forEach((file, index) => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
let icon = '📁';
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext)) icon = '📷';
|
||||
else if (['mp4', 'avi', 'mov', 'mkv', 'webm'].includes(ext)) icon = '🎥';
|
||||
else if (ext === 'pdf') icon = '📄';
|
||||
else if (['ppt', 'pptx'].includes(ext)) icon = '📊';
|
||||
|
||||
const sizeInMB = (file.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<div class="file-info">
|
||||
<span class="file-icon">${icon}</span>
|
||||
<div>
|
||||
<div style="font-weight: 600;">${file.name}</div>
|
||||
<div style="font-size: 12px; color: #6c757d;">${sizeInMB} MB</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="remove-file" onclick="removeFile(${index})">✕</span>
|
||||
`;
|
||||
|
||||
fileList.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
|
||||
function removeFile(index) {
|
||||
selectedFiles.splice(index, 1);
|
||||
displayFiles();
|
||||
uploadBtn.disabled = selectedFiles.length === 0;
|
||||
|
||||
// Update file input
|
||||
const dt = new DataTransfer();
|
||||
selectedFiles.forEach(file => dt.items.add(file));
|
||||
fileInput.files = dt.files;
|
||||
}
|
||||
|
||||
// Form submission
|
||||
uploadForm.addEventListener('submit', (e) => {
|
||||
if (selectedFiles.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Please select files to upload');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '⏳ Uploading...';
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user