2af04e1db3
- Content model: add url column + is_weblink() for web page content items - Player model: add deployment tracking fields (status, timestamps, message) - Content blueprint: add add_weblink and add_weblink_to_playlist routes; weblinks auto-deleted when removed from playlist - Players blueprint: add SSH deploy mode in add_player; weblink-aware playlist - API blueprint: weblink URL served directly in playlist response; add /api/deploy/test-ssh and /api/deploy/player endpoints - Admin blueprint: add build-player page (clone from Gitea, write base config); replace nginx status card with Caddy status on HTTPS config page - caddy_manager: rewritten to generate proper HTTPS/internal-CA Caddyfiles and reload Caddy via admin API (/load) for live config updates - ssh_deploy, background_tasks, player_build: new utils for SSH deployment - background_tasks: push Flask app context into background thread so DB updates after deployment complete correctly - ssh_deploy: robust install script detection with passwordless sudo injection (uses SSH credentials, cleaned up after install) - Dockerfile: add git, sshpass, openssh-client, rsync - docker-compose: switch nginx to Caddy on ports 80/443; add port 5000 for dev - Templates: add_player deploy mode UI, weblink form in playlist/upload pages, build_player admin page, Caddy status on HTTPS config page - Migrations: add_url_to_content, add_deployment_fields_to_player - app.py: call db.create_all() on startup for schema bootstrap - config.py: add PLAYER_CODE_DIR and PLAYER_REPO_URL settings
743 lines
25 KiB
HTML
743 lines
25 KiB
HTML
{% 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>
|
||
{% if content.content_type == 'weblink' %}
|
||
<a href="{{ content.url }}" target="_blank" rel="noopener noreferrer">{{ content.url }}</a>
|
||
{% else %}
|
||
{{ content.filename }}
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if content.content_type == 'image' %}📷 Image
|
||
{% elif content.content_type == 'video' %}🎥 Video
|
||
{% elif content.content_type == 'pdf' %}📄 PDF
|
||
{% elif content.content_type == 'weblink' %}🔗 Web Link
|
||
{% 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>
|
||
|
||
<div style="margin-bottom: 24px; padding-bottom: 24px; border-bottom: 1px solid #e0e0e0;">
|
||
<h3 style="margin-bottom: 12px; font-size: 16px;">🔗 Add Web Link</h3>
|
||
<form method="POST"
|
||
action="{{ url_for('content.add_weblink_to_playlist', playlist_id=playlist.id) }}">
|
||
<input type="url" name="url" required
|
||
placeholder="https://example.com/dashboard"
|
||
style="width: 100%; padding: 8px; margin-bottom: 8px; box-sizing: border-box;">
|
||
<div style="display: flex; gap: 8px; align-items: center;">
|
||
<label style="font-size: 13px; color: #666;">Duration (s):</label>
|
||
<input type="number" name="duration" value="30" min="1"
|
||
style="width: 80px; padding: 6px;">
|
||
<button type="submit" class="btn btn-primary btn-sm">+ Add Link</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{% 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 %}
|