400 lines
18 KiB
HTML
400 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Player Schedule</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
body.dark-mode {
|
|
background-color: #121212;
|
|
color: #ffffff;
|
|
}
|
|
.card.dark-mode {
|
|
background-color: #1e1e1e;
|
|
color: #ffffff;
|
|
}
|
|
.dark-mode label, .dark-mode th, .dark-mode td {
|
|
color: #ffffff;
|
|
}
|
|
@media (max-width: 768px) {
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
.btn {
|
|
font-size: 0.9rem;
|
|
padding: 0.5rem 1rem;
|
|
}
|
|
.card {
|
|
margin-bottom: 1rem;
|
|
}
|
|
}
|
|
.sortable-list li {
|
|
cursor: move;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.sortable-list li.dragging {
|
|
opacity: 0.5;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.drag-handle {
|
|
cursor: grab;
|
|
color: #aaa;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.drag-over {
|
|
border-top: 2px solid #0d6efd;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="{% if theme == 'dark' %}dark-mode{% endif %}">
|
|
<div class="container py-5">
|
|
<h1 class="text-center mb-4">Player Schedule for {{ player.username }}</h1>
|
|
|
|
<div class="row">
|
|
<!-- Player Info Section -->
|
|
<div class="col-md-6">
|
|
<div class="card mb-4 {% if theme == 'dark' %}dark-mode{% endif %}">
|
|
<div class="card-header bg-info text-white">
|
|
<h2>Player Info</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
<p><strong>Player Name:</strong> {{ player.username }}</p>
|
|
<p><strong>Hostname:</strong> {{ player.hostname }}</p>
|
|
{% if current_user.role == 'admin' %}
|
|
<a href="{{ url_for('edit_player', player_id=player.id, return_url=url_for('player_page', player_id=player.id)) }}" class="btn btn-warning">Update</a>
|
|
<form action="{{ url_for('delete_player', player_id=player.id) }}" method="post" style="display:inline;">
|
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this player?');">Delete</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Player Status Section -->
|
|
<div class="col-md-6">
|
|
<div class="card mb-4 {% if theme == 'dark' %}dark-mode{% endif %}">
|
|
<div class="card-header bg-success text-white">
|
|
<h2>Player Status</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if player_feedback %}
|
|
<div class="mb-3">
|
|
<strong>Current Status:</strong>
|
|
<span class="badge bg-{{ 'success' if player_feedback[0].status in ['active', 'playing'] else 'danger' }}">
|
|
{{ player_feedback[0].status|title }}
|
|
</span>
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong>Last Activity:</strong> {{ player_feedback[0].timestamp.strftime('%Y-%m-%d %H:%M:%S') }}
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong>Latest Message:</strong> {{ player_feedback[0].message }}
|
|
</div>
|
|
|
|
<!-- Recent Activity Log -->
|
|
<details>
|
|
<summary class="fw-bold mb-2">Recent Activity (Last 5)</summary>
|
|
<div class="mt-2">
|
|
{% for feedback in player_feedback %}
|
|
<div class="border-bottom pb-2 mb-2">
|
|
<div class="d-flex justify-content-between">
|
|
<span class="badge bg-{{ 'success' if feedback.status in ['active', 'playing'] else 'danger' }}">
|
|
{{ feedback.status|title }}
|
|
</span>
|
|
<small class="text-muted">{{ feedback.timestamp.strftime('%m-%d %H:%M') }}</small>
|
|
</div>
|
|
<div class="mt-1">
|
|
<small>{{ feedback.message }}</small>
|
|
{% if feedback.playlist_version %}
|
|
<br><small class="text-muted">Playlist v{{ feedback.playlist_version }}</small>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</details>
|
|
{% else %}
|
|
<div class="text-center text-muted">
|
|
<p>No status information available</p>
|
|
<small>Player hasn't sent any feedback yet</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Group Membership Section -->
|
|
<div class="mb-4">
|
|
{% if player.groups %}
|
|
<h4 class="text-center">Member of Group(s):</h4>
|
|
<ul class="list-group">
|
|
{% for group in player.groups %}
|
|
<li class="list-group-item {% if theme == 'dark' %}dark-mode{% endif %}">{{ group.name }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-center">This player is not a member of any groups.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Media Management Section -->
|
|
{% if current_user.role == 'admin' %}
|
|
<div class="card mb-4 {% if theme == 'dark' %}dark-mode{% endif %}">
|
|
<div class="card-header bg-info text-white">
|
|
<h2>Manage Media</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if content %}
|
|
<!-- Bulk Actions Controls -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="selectAll" {% if player.groups %}disabled{% endif %}>
|
|
<label class="form-check-label" for="selectAll">
|
|
Select All
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<button type="button" class="btn btn-danger" id="bulkDeleteBtn" {% if player.groups %}disabled{% endif %} style="display:none;" onclick="confirmBulkDelete()">
|
|
<i class="bi bi-trash"></i> Delete Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Delete Form -->
|
|
<form id="bulkDeleteForm" action="{{ url_for('bulk_delete_player_content', player_id=player.id) }}" method="post" style="display:none;">
|
|
<input type="hidden" name="selected_content_ids" id="selectedContentIds">
|
|
</form>
|
|
|
|
<ul class="list-group sortable-list" id="mediaList">
|
|
{% for media in content %}
|
|
<li class="list-group-item {% if theme == 'dark' %}dark-mode{% endif %}"
|
|
draggable="true"
|
|
data-id="{{ media.id }}"
|
|
data-position="{{ loop.index0 }}">
|
|
<div class="d-flex flex-column flex-md-row align-items-md-center">
|
|
<!-- Checkbox for bulk selection -->
|
|
<div class="me-2">
|
|
<input class="form-check-input media-checkbox"
|
|
type="checkbox"
|
|
name="selected_content"
|
|
value="{{ media.id }}"
|
|
{% if player.groups %}disabled{% endif %}>
|
|
</div>
|
|
|
|
<!-- Drag handle -->
|
|
<div class="drag-handle me-2" title="Drag to reorder">
|
|
<i class="bi bi-grip-vertical"></i>
|
|
☰
|
|
</div>
|
|
|
|
<!-- Media Thumbnail and Name -->
|
|
<div class="flex-grow-1 mb-2 mb-md-0 d-flex align-items-center">
|
|
<img src="{{ url_for('static', filename='uploads/' ~ media.file_name) }}"
|
|
alt="thumbnail"
|
|
style="width: 48px; height: 48px; object-fit: cover; margin-right: 10px; border-radius: 4px;"
|
|
onerror="this.style.display='none';">
|
|
<p class="mb-0"><strong>Media Name:</strong> {{ media.file_name }}</p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="d-flex flex-wrap justify-content-start">
|
|
<form action="{{ url_for('edit_content', content_id=media.id) }}" method="post" class="d-flex align-items-center me-2 mb-2">
|
|
<div class="input-group">
|
|
<span class="input-group-text">seconds</span>
|
|
<input type="number" class="form-control {% if theme == 'dark' %}dark-mode{% endif %}" name="duration" value="{{ media.duration }}" {% if player.groups %}disabled{% endif %} required>
|
|
</div>
|
|
<button type="submit" class="btn btn-warning ms-2" {% if player.groups %}disabled{% endif %}>Edit</button>
|
|
</form>
|
|
<form action="{{ url_for('delete_content', content_id=media.id) }}" method="post" class="mb-2">
|
|
<button type="submit" class="btn btn-danger" {% if player.groups %}disabled{% endif %} onclick="return confirm('Are you sure you want to delete this media?');">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<!-- Add a save button for the reordering -->
|
|
<button id="saveOrder" class="btn btn-success mt-3" {% if player.groups %}disabled{% endif %}>Save Playlist Order</button>
|
|
{% else %}
|
|
<p class="text-center">No media uploaded for this player.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Action Buttons -->
|
|
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Back to Dashboard</a>
|
|
<a href="{{ url_for('player_fullscreen', player_id=player.id) }}" class="btn btn-primary">Full Screen</a>
|
|
<a href="{{ url_for('upload_content', target_type='player', target_id=player.id, return_url=url_for('player_page', player_id=player.id)) }}"
|
|
class="btn btn-success"
|
|
{% if player.groups %}disabled onclick="return false;"{% endif %}>
|
|
{% if player.groups %}Manage Media by Group{% else %}Upload Media{% endif %}
|
|
</a>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Only enable if the player is not in a group (if the buttons are not disabled)
|
|
if (!document.querySelector('#saveOrder').hasAttribute('disabled')) {
|
|
const mediaList = document.getElementById('mediaList');
|
|
let draggedItem = null;
|
|
|
|
// Initialize drag events for all items
|
|
const items = mediaList.querySelectorAll('li');
|
|
items.forEach(item => {
|
|
// Drag start
|
|
item.addEventListener('dragstart', function(e) {
|
|
draggedItem = item;
|
|
setTimeout(() => {
|
|
item.classList.add('dragging');
|
|
}, 0);
|
|
});
|
|
|
|
// Drag end
|
|
item.addEventListener('dragend', function() {
|
|
item.classList.remove('dragging');
|
|
draggedItem = null;
|
|
updatePositions();
|
|
});
|
|
|
|
// Drag over
|
|
item.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
if (item !== draggedItem) {
|
|
const rect = item.getBoundingClientRect();
|
|
const y = e.clientY - rect.top;
|
|
const height = rect.height;
|
|
|
|
if (y < height / 2) {
|
|
mediaList.insertBefore(draggedItem, item);
|
|
} else {
|
|
mediaList.insertBefore(draggedItem, item.nextSibling);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Save button click handler
|
|
document.getElementById('saveOrder').addEventListener('click', function() {
|
|
// Collect new order
|
|
const newOrder = [];
|
|
mediaList.querySelectorAll('li').forEach((item, index) => {
|
|
newOrder.push({
|
|
id: item.dataset.id,
|
|
position: index
|
|
});
|
|
});
|
|
|
|
// Send to server
|
|
fetch('{{ url_for("update_content_order", player_id=player.id) }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
|
|
},
|
|
body: JSON.stringify({items: newOrder})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('Playlist order updated successfully!');
|
|
console.log('Playlist version updated to:', data.new_version);
|
|
} else {
|
|
alert('Error updating playlist order: ' + (data.error || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('An error occurred while updating the playlist order.');
|
|
});
|
|
});
|
|
|
|
// Update positions in the UI
|
|
function updatePositions() {
|
|
mediaList.querySelectorAll('li').forEach((item, index) => {
|
|
item.dataset.position = index;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Bulk selection functionality
|
|
const selectAllCheckbox = document.getElementById('selectAll');
|
|
const mediaCheckboxes = document.querySelectorAll('.media-checkbox');
|
|
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
|
|
|
|
// Select all functionality
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.addEventListener('change', function() {
|
|
mediaCheckboxes.forEach(checkbox => {
|
|
if (!checkbox.disabled) {
|
|
checkbox.checked = this.checked;
|
|
}
|
|
});
|
|
updateBulkDeleteButton();
|
|
});
|
|
}
|
|
|
|
// Individual checkbox change
|
|
mediaCheckboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', function() {
|
|
updateSelectAllState();
|
|
updateBulkDeleteButton();
|
|
});
|
|
});
|
|
|
|
function updateSelectAllState() {
|
|
const enabledCheckboxes = Array.from(mediaCheckboxes).filter(cb => !cb.disabled);
|
|
const checkedBoxes = enabledCheckboxes.filter(cb => cb.checked);
|
|
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.checked = checkedBoxes.length === enabledCheckboxes.length && enabledCheckboxes.length > 0;
|
|
selectAllCheckbox.indeterminate = checkedBoxes.length > 0 && checkedBoxes.length < enabledCheckboxes.length;
|
|
}
|
|
}
|
|
|
|
function updateBulkDeleteButton() {
|
|
const checkedBoxes = Array.from(mediaCheckboxes).filter(cb => cb.checked);
|
|
if (bulkDeleteBtn) {
|
|
bulkDeleteBtn.style.display = checkedBoxes.length > 0 ? 'inline-block' : 'none';
|
|
}
|
|
}
|
|
});
|
|
|
|
function confirmBulkDelete() {
|
|
const checkedBoxes = Array.from(document.querySelectorAll('.media-checkbox:checked'));
|
|
if (checkedBoxes.length === 0) {
|
|
alert('No media files selected.');
|
|
return;
|
|
}
|
|
|
|
const count = checkedBoxes.length;
|
|
const message = `Are you sure you want to delete ${count} selected media file${count > 1 ? 's' : ''}? This action cannot be undone.`;
|
|
|
|
if (confirm(message)) {
|
|
// Create a form with selected IDs
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '{{ url_for("bulk_delete_player_content", player_id=player.id) }}';
|
|
|
|
checkedBoxes.forEach(checkbox => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'selected_content';
|
|
input.value = checkbox.value;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |