355 lines
16 KiB
HTML
355 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ player.username }} - Player View{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Page Header -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h1><i class="bi bi-display"></i> {{ player.username }}</h1>
|
|
<p class="text-muted">Player details and content management</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<a href="{{ url_for('dashboard.index') }}" class="btn btn-secondary">
|
|
<i class="bi bi-arrow-left"></i> Back to Dashboard
|
|
</a>
|
|
<a href="{{ url_for('player.edit', player_id=player.id) }}" class="btn btn-primary">
|
|
<i class="bi bi-pencil"></i> Edit Player
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Player Information -->
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5><i class="bi bi-info-circle"></i> Player Information</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-2">
|
|
<div class="col-sm-5"><strong>Username:</strong></div>
|
|
<div class="col-sm-7">{{ player.username }}</div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-sm-5"><strong>Hostname:</strong></div>
|
|
<div class="col-sm-7"><code>{{ player.hostname }}</code></div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-sm-5"><strong>Status:</strong></div>
|
|
<div class="col-sm-7">
|
|
{% if player.is_active %}
|
|
<span class="badge bg-success">Active</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-sm-5"><strong>Created:</strong></div>
|
|
<div class="col-sm-7">{{ player.created_at.strftime('%Y-%m-%d %H:%M') if player.created_at else 'N/A' }}</div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-sm-5"><strong>Last Seen:</strong></div>
|
|
<div class="col-sm-7">
|
|
{% if player.last_seen %}
|
|
{{ player.last_seen.strftime('%Y-%m-%d %H:%M') }}
|
|
{% else %}
|
|
<span class="text-muted">Never</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-sm-5"><strong>Groups:</strong></div>
|
|
<div class="col-sm-7">
|
|
{% if player.groups %}
|
|
{% for group in player.groups %}
|
|
<span class="badge bg-info me-1">{{ group.name }}</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span class="text-muted">No groups</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h6><i class="bi bi-lightning"></i> Quick Actions</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<a href="{{ url_for('player.fullscreen', player_id=player.id) }}"
|
|
class="btn btn-info" target="_blank">
|
|
<i class="bi bi-fullscreen"></i> Open Display
|
|
</a>
|
|
<button type="button" class="btn btn-success" onclick="refreshPlayer()">
|
|
<i class="bi bi-arrow-clockwise"></i> Refresh Content
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Management -->
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5><i class="bi bi-collection-play"></i> Player Content</h5>
|
|
<a href="{{ url_for('content.upload') }}" class="btn btn-sm btn-primary">
|
|
<i class="bi bi-plus"></i> Add Content
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if content %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Position</th>
|
|
<th>Filename</th>
|
|
<th>Type</th>
|
|
<th>Duration</th>
|
|
<th>Uploaded</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in content %}
|
|
<tr>
|
|
<td>
|
|
<span class="badge bg-primary">{{ item.position }}</span>
|
|
</td>
|
|
<td>
|
|
<strong>{{ item.file_name }}</strong>
|
|
{% if item.original_name %}
|
|
<br><small class="text-muted">{{ item.original_name }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if item.content_type == 'image' %}
|
|
<span class="badge bg-success"><i class="bi bi-image"></i> Image</span>
|
|
{% elif item.content_type == 'video' %}
|
|
<span class="badge bg-info"><i class="bi bi-play-circle"></i> Video</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ item.content_type }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ item.duration }}s</td>
|
|
<td>{{ item.uploaded_at.strftime('%m/%d %H:%M') if item.uploaded_at else 'N/A' }}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary"
|
|
onclick="previewContent('{{ item.id }}', '{{ item.file_name }}', '{{ item.content_type }}')">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning"
|
|
onclick="editContent('{{ item.id }}', '{{ item.file_name }}', {{ item.duration }})">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger"
|
|
onclick="removeContent('{{ item.id }}', '{{ item.file_name }}')">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<i class="bi bi-collection-play text-muted" style="font-size: 3rem;"></i>
|
|
<h5 class="text-muted mt-2">No Content Available</h5>
|
|
<p class="text-muted">This player doesn't have any content assigned yet.</p>
|
|
<a href="{{ url_for('content.upload') }}" class="btn btn-primary">
|
|
<i class="bi bi-plus"></i> Upload First Content
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Preview Modal -->
|
|
<div class="modal fade" id="previewModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Content Preview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center" id="previewContent">
|
|
<!-- Content will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remove Content Modal -->
|
|
<div class="modal fade" id="removeModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Remove Content</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to remove <strong id="removeFilename"></strong> from this player?</p>
|
|
<p class="text-warning small">This will only remove the content from this player, not delete the file.</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" onclick="confirmRemoveContent()">Remove Content</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Content Modal -->
|
|
<div class="modal fade" id="editModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Content Duration</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Edit display duration for <strong id="editFilename"></strong></p>
|
|
<div class="mb-3">
|
|
<label for="editDuration" class="form-label">Duration (seconds)</label>
|
|
<input type="number" class="form-control" id="editDuration" min="1" max="300" required>
|
|
<div class="form-text">How long should this content be displayed? (1-300 seconds)</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="confirmEditContent()">Update Duration</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentContentId = null;
|
|
|
|
function refreshPlayer() {
|
|
// Placeholder for content refresh functionality
|
|
alert('Content refresh signal sent to player!');
|
|
}
|
|
|
|
function previewContent(contentId, filename, contentType) {
|
|
console.log('Preview function called:', {contentId, filename, contentType});
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
const previewDiv = document.getElementById('previewContent');
|
|
|
|
if (contentType === 'image') {
|
|
const imgSrc = `/static/uploads/${filename}`;
|
|
console.log('Loading image from:', imgSrc);
|
|
previewDiv.innerHTML = `<img src="${imgSrc}" class="img-fluid" alt="${filename}"
|
|
onload="console.log('Image loaded successfully')"
|
|
onerror="console.error('Failed to load image:', this.src)">`;
|
|
} else if (contentType === 'video') {
|
|
const videoSrc = `/static/uploads/${filename}`;
|
|
console.log('Loading video from:', videoSrc);
|
|
previewDiv.innerHTML = `<video controls class="w-100" style="max-height: 400px;">
|
|
<source src="${videoSrc}">
|
|
Your browser does not support the video tag.
|
|
</video>`;
|
|
} else {
|
|
console.log('Unsupported content type:', contentType);
|
|
previewDiv.innerHTML = `<p>Preview not available for this content type: ${contentType}</p>`;
|
|
}
|
|
|
|
modal.show();
|
|
}
|
|
|
|
function removeContent(contentId, filename) {
|
|
currentContentId = contentId;
|
|
document.getElementById('removeFilename').textContent = filename;
|
|
const modal = new bootstrap.Modal(document.getElementById('removeModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function editContent(contentId, filename, currentDuration) {
|
|
currentContentId = contentId;
|
|
document.getElementById('editFilename').textContent = filename;
|
|
document.getElementById('editDuration').value = currentDuration;
|
|
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function confirmEditContent() {
|
|
if (currentContentId) {
|
|
const newDuration = parseInt(document.getElementById('editDuration').value);
|
|
|
|
if (!newDuration || newDuration < 1 || newDuration > 300) {
|
|
alert('Please enter a valid duration between 1 and 300 seconds.');
|
|
return;
|
|
}
|
|
|
|
// Make API call to update content duration
|
|
fetch(`/api/content/${currentContentId}/edit`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
duration: newDuration
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error updating content: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error updating content');
|
|
});
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
|
|
modal.hide();
|
|
}
|
|
}
|
|
|
|
function confirmRemoveContent() {
|
|
if (currentContentId) {
|
|
// Make API call to remove content from player
|
|
fetch(`/api/content/${currentContentId}/remove-from-player`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
player_id: {{ player.id|tojson }}
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error removing content: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error removing content');
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|