Files
digiserver-v2/app/templates/players/manage_player.html
DigiServer Developer 8d52c0338f Add player media editing feature with versioning
- Added PlayerEdit model to track edited media history
- Created /api/player-edit-media endpoint for receiving edited files from players
- Implemented versioned storage: edited_media/<content_id>/<filename_vN.ext>
- Automatic playlist update when edited media is received
- Updated content.filename to reference versioned file in playlist
- Added 'Edited Media on the Player' card to player management page
- UI shows version history grouped by original file
- Each edit preserves previous versions in archive folder
- Includes dark mode support for new UI elements
- Documentation: PLAYER_EDIT_MEDIA_API.md
2025-12-06 00:06:11 +02:00

757 lines
32 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Manage Player - {{ player.name }}{% endblock %}
{% block content %}
<style>
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
body.dark-mode .form-group label {
color: #e2e8f0;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
body.dark-mode .form-control {
background: #1a202c;
border-color: #4a5568;
color: #e2e8f0;
}
body.dark-mode .form-control:focus {
border-color: #7c3aed;
outline: none;
}
.info-box {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.info-box.neutral {
background: #f8f9fa;
}
body.dark-mode .info-box.neutral {
background: #1a202c;
border: 1px solid #4a5568;
}
.info-box.success {
background: #d4edda;
color: #155724;
}
body.dark-mode .info-box.success {
background: #1a4d2e;
color: #86efac;
border: 1px solid #48bb78;
}
.info-box.warning {
background: #fff3cd;
color: #856404;
}
body.dark-mode .info-box.warning {
background: #4a3800;
color: #fbbf24;
border: 1px solid #ecc94b;
}
.log-item {
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 4px;
background: #f8f9fa;
border-left: 4px solid;
}
body.dark-mode .log-item {
background: #2d3748;
}
.log-item pre {
background: #fff;
border: 1px solid #ddd;
}
body.dark-mode .log-item pre {
background: #1a202c;
border-color: #4a5568;
color: #e2e8f0;
}
body.dark-mode h1,
body.dark-mode h2,
body.dark-mode h3,
body.dark-mode h4 {
color: #e2e8f0;
}
body.dark-mode p {
color: #a0aec0;
}
body.dark-mode strong {
color: #e2e8f0;
}
body.dark-mode code {
background: #1a202c;
color: #e2e8f0;
padding: 2px 6px;
border-radius: 3px;
}
body.dark-mode small {
color: #718096;
}
.credential-item {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #e2e8f0;
}
body.dark-mode .credential-item {
border-bottom-color: #4a5568;
}
.credential-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.credential-label {
font-weight: 600;
display: block;
margin-bottom: 0.25rem;
font-size: 0.85rem;
color: #495057;
}
body.dark-mode .credential-label {
color: #a0aec0;
}
.credential-value {
font-family: 'Courier New', monospace;
background: #f8f9fa;
padding: 0.5rem;
border-radius: 4px;
word-break: break-all;
font-size: 0.85rem;
border: 1px solid #dee2e6;
}
body.dark-mode .credential-value {
background: #0d1117;
border-color: #4a5568;
color: #e2e8f0;
}
.status-card {
margin-bottom: 2rem;
}
.status-card.online {
background: #d4edda;
}
body.dark-mode .status-card.online {
background: #1a4d2e;
border: 1px solid #48bb78;
}
.status-card.offline {
background: #f8d7da;
}
body.dark-mode .status-card.offline {
background: #4a1a1a;
border: 1px solid #dc3545;
}
.status-card.other {
background: #fff3cd;
}
body.dark-mode .status-card.other {
background: #4a3800;
border: 1px solid #ecc94b;
}
.playlist-stats {
padding: 1rem;
background: #f8f9fa;
border-radius: 4px;
}
body.dark-mode .playlist-stats {
background: #1a202c;
border: 1px solid #4a5568;
}
body.dark-mode .playlist-stats > div > div:first-child {
color: #a0aec0;
}
/* Player Logs Dark Mode */
body.dark-mode .card p[style*="color: #6c757d"] {
color: #a0aec0 !important;
}
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] {
background: #2d3748 !important;
}
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] p {
color: #e2e8f0 !important;
}
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] p[style*="color: #6c757d"] {
color: #a0aec0 !important;
}
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] small {
color: #a0aec0 !important;
}
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] details summary {
color: #f87171 !important;
}
body.dark-mode .card > div[style*="max-height: 500px"] > div[style*="padding: 0.75rem"] pre {
background: #1a202c !important;
border-color: #4a5568 !important;
color: #e2e8f0 !important;
}
body.dark-mode .card > div[style*="text-align: center"] {
color: #a0aec0 !important;
}
/* Edited Media Cards Dark Mode */
body.dark-mode .card[style*="border: 2px solid #7c3aed"] {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%) !important;
border-color: #8b5cf6 !important;
}
body.dark-mode .card[style*="border: 2px solid #7c3aed"] h3 {
color: #a78bfa !important;
}
body.dark-mode .card[style*="border: 2px solid #7c3aed"] div[style*="background: white"] {
background: #374151 !important;
}
body.dark-mode .card[style*="border: 2px solid #7c3aed"] strong {
color: #a78bfa !important;
}
body.dark-mode .card[style*="border: 2px solid #7c3aed"] p[style*="color: #475569"],
body.dark-mode .card[style*="border: 2px solid #7c3aed"] p[style*="color: #64748b"] {
color: #cbd5e1 !important;
}
body.dark-mode div[style*="background: #f8f9fa; border-radius: 8px"] {
background: #2d3748 !important;
}
body.dark-mode .card > div[style*="text-align: center"] p {
color: #a0aec0 !important;
}
</style>
<div style="margin-bottom: 2rem; display: flex; justify-content: space-between; align-items: flex-start;">
<h1 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 32px; height: 32px;">
Manage Player: {{ player.name }}
</h1>
<a href="{{ url_for('players.list') }}" class="btn" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Back to Players
</a>
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
<div style="background-color: #fefefe; margin: 15% auto; padding: 20px; border: 1px solid #888; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<h2 style="margin-top: 0; color: #dc3545; display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 1.5rem;">⚠️</span>
Confirm Delete
</h2>
<p style="font-size: 1.1rem; margin: 1.5rem 0;">
Are you sure you want to delete player <strong>"{{ player.name }}"</strong>?
</p>
<p style="color: #dc3545; margin: 1rem 0;">
<strong>Warning:</strong> This action cannot be undone. All feedback logs for this player will also be deleted.
</p>
<div style="display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 2rem;">
<button onclick="closeDeleteModal()" class="btn" style="background: #6c757d;">
Cancel
</button>
<form method="POST" action="{{ url_for('players.delete_player', player_id=player.id) }}" style="margin: 0;">
<button type="submit" class="btn" style="background: #dc3545;">
Yes, Delete Player
</button>
</form>
</div>
</div>
</div>
<style>
body.dark-mode #deleteModal > div {
background-color: #1a202c;
border-color: #4a5568;
color: #e2e8f0;
}
body.dark-mode #deleteModal h2 {
color: #f87171;
}
body.dark-mode #deleteModal p {
color: #e2e8f0;
}
body.dark-mode #deleteModal p strong {
color: #fbbf24;
}
</style>
<script>
function confirmDelete() {
document.getElementById('deleteModal').style.display = 'block';
}
function closeDeleteModal() {
document.getElementById('deleteModal').style.display = 'none';
}
// Close modal when clicking outside of it
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>
<!-- Player Status Overview -->
<div class="card status-card {% if player.status == 'online' %}online{% elif player.status == 'offline' %}offline{% else %}other{% endif %}">
<h3 style="display: flex; align-items: center; gap: 0.5rem;">
Status:
{% if player.status == 'online' %}
<span style="color: #28a745; display: flex; align-items: center; gap: 0.3rem;">
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 20px; height: 20px; color: #28a745;">
Online
</span>
{% elif player.status == 'offline' %}
<span style="color: #dc3545; display: flex; align-items: center; gap: 0.3rem;">
<img src="{{ url_for('static', filename='icons/warning.svg') }}" alt="" style="width: 20px; height: 20px; color: #dc3545;">
Offline
</span>
{% else %}
<span style="color: #ffc107; display: flex; align-items: center; gap: 0.3rem;">
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 20px; height: 20px; color: #ffc107;">
{{ player.status|title }}
</span>
{% endif %}
</h3>
<p><strong>Hostname:</strong> {{ player.hostname }}</p>
<p><strong>Last Seen:</strong>
{% if player.last_seen %}
{{ player.last_seen | localtime('%Y-%m-%d %H:%M:%S') }}
{% else %}
Never
{% endif %}
</p>
<p><strong>Assigned Playlist:</strong>
{% if current_playlist %}
<span style="color: #28a745; font-weight: bold;">{{ current_playlist.name }} (v{{ current_playlist.version }})</span>
{% else %}
<span style="color: #dc3545;">No playlist assigned</span>
{% endif %}
</p>
</div>
<!-- Playlist Overview Card -->
{% if current_playlist %}
<div class="card" style="margin-bottom: 2rem;">
<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;">
Current Playlist: {{ current_playlist.name }}
</h2>
<div class="playlist-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-top: 1rem;">
<div>
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Total Items</div>
<div style="font-size: 1.5rem; font-weight: bold;">{{ current_playlist.contents.count() }}</div>
</div>
<div>
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Playlist Version</div>
<div style="font-size: 1.5rem; font-weight: bold;">v{{ current_playlist.version }}</div>
</div>
<div>
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Last Updated</div>
<div style="font-size: 1rem; font-weight: bold;">{{ current_playlist.updated_at | localtime }}</div>
</div>
<div>
<div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Orientation</div>
<div style="font-size: 1.5rem; font-weight: bold;">{{ current_playlist.orientation }}</div>
</div>
</div>
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
<a href="{{ url_for('content.manage_playlist_content', playlist_id=current_playlist.id) }}"
class="btn btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Edit Playlist Content
</a>
<a href="{{ url_for('players.player_fullscreen', player_id=player.id) }}"
class="btn btn-success" style="display: inline-flex; align-items: center; gap: 0.5rem;"
target="_blank">
<img src="{{ url_for('static', filename='icons/monitor.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
View Live Content
</a>
</div>
</div>
{% endif %}
<!-- Three Column Layout -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem;">
<!-- Card 1: Edit Credentials -->
<div class="card">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 24px; height: 24px;">
Edit Credentials
</h2>
<form method="POST" style="margin-top: 1rem;">
<input type="hidden" name="action" value="update_credentials">
<div class="form-group">
<label for="name">Player Name *</label>
<input type="text" id="name" name="name" value="{{ player.name }}"
required minlength="3" class="form-control">
</div>
<div class="form-group">
<label for="location">Location</label>
<input type="text" id="location" name="location" value="{{ player.location or '' }}"
placeholder="e.g., Main Lobby" class="form-control">
</div>
<div class="form-group">
<label for="orientation">Orientation</label>
<select id="orientation" name="orientation" class="form-control">
<option value="Landscape" {% if player.orientation == 'Landscape' %}selected{% endif %}>Landscape</option>
<option value="Portrait" {% if player.orientation == 'Portrait' %}selected{% endif %}>Portrait</option>
</select>
</div>
<div style="border-top: 1px solid #ddd; margin: 1.5rem 0; padding-top: 1.5rem;">
<h4 style="margin: 0 0 1rem 0; font-size: 0.95rem;">🔑 Authentication Settings</h4>
<div class="form-group">
<label for="hostname">Hostname *</label>
<input type="text" id="hostname" name="hostname" value="{{ player.hostname }}"
required minlength="3" class="form-control"
placeholder="e.g., tv-terasa">
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">
This is the unique identifier for the player
</small>
</div>
<div class="form-group">
<label for="password">New Password (leave blank to keep current)</label>
<input type="password" id="password" name="password" class="form-control"
placeholder="Enter new password">
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">
🔒 Optional: Set a new password for player authentication
</small>
</div>
<div class="form-group">
<label for="quickconnect_code">Quick Connect Code</label>
<input type="text" id="quickconnect_code" name="quickconnect_code" class="form-control"
placeholder="e.g., 8887779">
<small style="display: block; margin-top: 0.25rem; color: #6c757d;">
🔗 Enter the plain text code (e.g., 8887779) - will be hashed automatically
</small>
</div>
</div>
<button type="submit" class="btn btn-success" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Save Changes
</button>
</form>
</div>
<!-- Card 2: Assign Playlist -->
<div class="card">
<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;">
Assign Playlist
</h2>
<form method="POST" style="margin-top: 1rem;">
<input type="hidden" name="action" value="assign_playlist">
<div class="form-group">
<label for="playlist_id">Select Playlist</label>
<select id="playlist_id" name="playlist_id" class="form-control">
<option value="">-- No Playlist (Unassign) --</option>
{% for playlist in playlists %}
<option value="{{ playlist.id }}"
{% if player.playlist_id == playlist.id %}selected{% endif %}>
{{ playlist.name }} (v{{ playlist.version }}) - {{ playlist.contents.count() }} items
</option>
{% endfor %}
</select>
</div>
{% if current_playlist %}
<div class="info-box success">
<h4 style="margin: 0 0 0.5rem 0;">Currently Assigned:</h4>
<p style="margin: 0;"><strong>{{ current_playlist.name }}</strong></p>
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem;">Version: {{ current_playlist.version }}</p>
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem;">Content Items: {{ current_playlist.contents.count() }}</p>
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem; color: #6c757d;">
Updated: {{ current_playlist.updated_at | localtime }}
</p>
</div>
{% else %}
<div class="info-box warning">
<p style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/warning.svg') }}" alt="" style="width: 18px; height: 18px;">
No playlist currently assigned to this player.
</p>
</div>
{% endif %}
<button type="submit" class="btn btn-success" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Assign Playlist
</button>
</form>
<div style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid #ddd;">
<h4>Quick Actions:</h4>
<a href="{{ url_for('content.content_list') }}" class="btn" style="width: 100%; margin-top: 0.5rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Create New Playlist
</a>
{% if current_playlist %}
<a href="{{ url_for('content.manage_playlist_content', playlist_id=current_playlist.id) }}"
class="btn" style="width: 100%; margin-top: 0.5rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
Edit Current Playlist
</a>
{% endif %}
<button onclick="confirmDelete()" class="btn" style="width: 100%; margin-top: 0.5rem; background: #dc3545; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<span style="font-size: 1.2rem;">🗑️</span>
Delete Player
</button>
</div>
</div>
<!-- Card 3: Player Logs -->
<div class="card">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/info.svg') }}" alt="" style="width: 24px; height: 24px;">
Player Logs
</h2>
<p style="color: #6c757d; font-size: 0.9rem;">Recent feedback from the player device</p>
<div style="max-height: 500px; overflow-y: auto; margin-top: 1rem;">
{% if recent_logs %}
{% for log in recent_logs %}
<div style="padding: 0.75rem; margin-bottom: 0.5rem; border-left: 4px solid
{% if log.status == 'error' %}#dc3545
{% elif log.status == 'warning' %}#ffc107
{% elif log.status == 'playing' %}#28a745
{% else %}#17a2b8{% endif %};
background: #f8f9fa; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<strong style="color:
{% if log.status == 'error' %}#dc3545
{% elif log.status == 'warning' %}#ffc107
{% elif log.status == 'playing' %}#28a745
{% else %}#17a2b8{% endif %};">
{% if log.status == 'error' %}❌
{% elif log.status == 'warning' %}⚠️
{% elif log.status == 'playing' %}▶️
{% elif log.status == 'restarting' %}🔄
{% else %}{% endif %}
{{ log.status|upper }}
</strong>
<p style="margin: 0.25rem 0 0 0; font-size: 0.9rem;">{{ log.message }}</p>
{% if log.playlist_version %}
<p style="margin: 0.25rem 0 0 0; font-size: 0.85rem; color: #6c757d;">
Playlist v{{ log.playlist_version }}
</p>
{% endif %}
{% if log.error_details %}
<details style="margin-top: 0.5rem;">
<summary style="cursor: pointer; font-size: 0.85rem; color: #dc3545;">Error Details</summary>
<pre style="margin: 0.5rem 0 0 0; padding: 0.5rem; background: #fff; border: 1px solid #ddd; border-radius: 4px; font-size: 0.8rem; overflow-x: auto;">{{ log.error_details }}</pre>
</details>
{% endif %}
</div>
<small style="color: #6c757d; white-space: nowrap; margin-left: 1rem;">
{{ log.timestamp | localtime('%m/%d %H:%M') }}
</small>
</div>
</div>
{% endfor %}
{% else %}
<div style="text-align: center; padding: 2rem; color: #6c757d;">
<p>📭 No logs received yet</p>
<p style="font-size: 0.9rem;">Logs will appear here once the player starts sending feedback</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Edited Media Section - Full Width -->
<div class="card" style="margin-top: 2rem;">
<h2 style="display: flex; align-items: center; gap: 0.5rem;">
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 24px; height: 24px;">
Edited Media on the Player
</h2>
<p style="color: #6c757d; font-size: 0.9rem;">History of media files edited on this player, grouped by original file</p>
{% if edited_media %}
{% set edited_by_content = {} %}
{% for edit in edited_media %}
{% if edit.content_id not in edited_by_content %}
{% set _ = edited_by_content.update({edit.content_id: {'original_name': edit.original_name, 'content_id': edit.content_id, 'versions': []}}) %}
{% endif %}
{% set _ = edited_by_content[edit.content_id]['versions'].append(edit) %}
{% endfor %}
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;">
{% for content_id, data in edited_by_content.items() %}
<div class="card" style="background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); border: 2px solid #7c3aed; box-shadow: 0 2px 8px rgba(124, 58, 237, 0.1);">
<div style="padding: 1rem;">
<h3 style="margin: 0 0 0.75rem 0; color: #7c3aed; font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
✏️ {{ data.original_name }}
</h3>
<p style="margin: 0 0 1rem 0; font-size: 0.85rem; color: #6c757d;">
{{ data.versions|length }} version{{ 's' if data.versions|length > 1 else '' }}
</p>
<div style="max-height: 400px; overflow-y: auto;">
{% for edit in data.versions|sort(attribute='version', reverse=True) %}
<div style="padding: 0.75rem; margin-bottom: 0.75rem; background: white; border-radius: 6px; border-left: 4px solid {% if loop.first %}#7c3aed{% else %}#cbd5e1{% endif %}; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem;">
<strong style="color: {% if loop.first %}#7c3aed{% else %}#64748b{% endif %}; font-size: 0.95rem;">
Version {{ edit.version }}
{% if loop.first %}
<span style="background: #7c3aed; color: white; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; margin-left: 0.5rem;">Latest</span>
{% endif %}
</strong>
<small style="color: #6c757d; white-space: nowrap;">
{{ edit.created_at | localtime('%m/%d %H:%M') }}
</small>
</div>
<p style="margin: 0.25rem 0; font-size: 0.85rem; color: #475569;">
📄 {{ edit.new_name }}
</p>
{% if edit.user %}
<p style="margin: 0.25rem 0; font-size: 0.8rem; color: #64748b;">
👤 {{ edit.user }}
</p>
{% endif %}
{% if edit.time_of_modification %}
<p style="margin: 0.25rem 0; font-size: 0.8rem; color: #64748b;">
🕒 {{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') }}
</p>
{% endif %}
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem;">
<a href="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
target="_blank"
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.4rem 0.75rem; background: #7c3aed; color: white; text-decoration: none; border-radius: 4px; font-size: 0.8rem; transition: background 0.2s;"
onmouseover="this.style.background='#6d28d9'"
onmouseout="this.style.background='#7c3aed'">
📥 View File
</a>
<a href="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
download
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.4rem 0.75rem; background: #64748b; color: white; text-decoration: none; border-radius: 4px; font-size: 0.8rem; transition: background 0.2s;"
onmouseover="this.style.background='#475569'"
onmouseout="this.style.background='#64748b'">
💾 Download
</a>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div style="text-align: center; padding: 3rem; color: #6c757d; background: #f8f9fa; border-radius: 8px; margin-top: 1.5rem;">
<p style="font-size: 2rem; margin: 0;">📝</p>
<p style="margin: 0.5rem 0 0 0; font-size: 1.1rem; font-weight: 500;">No edited media yet</p>
<p style="font-size: 0.9rem; margin: 0.5rem 0 0 0;">Media edits will appear here once the player sends edited files</p>
</div>
{% endif %}
</div>
<!-- Additional Info Section -->
<div class="card" style="margin-top: 2rem;">
<h2> Player Information</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-top: 1rem;">
<div>
<p><strong>Player ID:</strong> {{ player.id }}</p>
<p><strong>Created:</strong> {{ (player.created_at | localtime) if player.created_at else 'N/A' }}</p>
</div>
<div>
<p><strong>Orientation:</strong> {{ player.orientation }}</p>
<p><strong>Location:</strong> {{ player.location or 'Not set' }}</p>
</div>
<div>
<p><strong>Last Heartbeat:</strong>
{% if player.last_heartbeat %}
{{ player.last_heartbeat | localtime('%Y-%m-%d %H:%M:%S') }}
{% else %}
Never
{% endif %}
</p>
</div>
</div>
</div>
{% endblock %}