updated for beter media management
This commit is contained in:
@@ -409,7 +409,7 @@ def delete_leftover_images():
|
||||
try:
|
||||
# Find all leftover image content
|
||||
leftover_images = db.session.query(Content).filter(
|
||||
Content.media_type == 'image',
|
||||
Content.content_type == 'image',
|
||||
~Content.id.in_(
|
||||
db.session.query(playlist_content.c.content_id)
|
||||
)
|
||||
@@ -421,8 +421,8 @@ def delete_leftover_images():
|
||||
for content in leftover_images:
|
||||
try:
|
||||
# Delete physical file
|
||||
if content.file_path:
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.file_path)
|
||||
if content.filename:
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
@@ -430,7 +430,7 @@ def delete_leftover_images():
|
||||
db.session.delete(content)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Error deleting {content.file_path}: {str(e)}")
|
||||
errors.append(f"Error deleting {content.filename}: {str(e)}")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -455,7 +455,7 @@ def delete_leftover_videos():
|
||||
try:
|
||||
# Find all leftover video content
|
||||
leftover_videos = db.session.query(Content).filter(
|
||||
Content.media_type == 'video',
|
||||
Content.content_type == 'video',
|
||||
~Content.id.in_(
|
||||
db.session.query(playlist_content.c.content_id)
|
||||
)
|
||||
@@ -467,8 +467,8 @@ def delete_leftover_videos():
|
||||
for content in leftover_videos:
|
||||
try:
|
||||
# Delete physical file
|
||||
if content.file_path:
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.file_path)
|
||||
if content.filename:
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
@@ -476,7 +476,7 @@ def delete_leftover_videos():
|
||||
db.session.delete(content)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Error deleting {content.file_path}: {str(e)}")
|
||||
errors.append(f"Error deleting {content.filename}: {str(e)}")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -500,8 +500,8 @@ def delete_single_leftover(content_id):
|
||||
content = Content.query.get_or_404(content_id)
|
||||
|
||||
# Delete physical file
|
||||
if content.file_path:
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.file_path)
|
||||
if content.filename:
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
@@ -509,7 +509,7 @@ def delete_single_leftover(content_id):
|
||||
db.session.delete(content)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Successfully deleted {content.file_path}', 'success')
|
||||
flash(f'Successfully deleted {content.filename}', 'success')
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
|
||||
@@ -19,15 +19,88 @@ content_bp = Blueprint('content', __name__, url_prefix='/content')
|
||||
def content_list():
|
||||
"""Main playlist management page."""
|
||||
playlists = Playlist.query.order_by(Playlist.created_at.desc()).all()
|
||||
media_files = Content.query.order_by(Content.uploaded_at.desc()).all()
|
||||
media_files = Content.query.order_by(Content.uploaded_at.desc()).limit(3).all() # Only last 3
|
||||
total_media_count = Content.query.count() # Total count for display
|
||||
players = Player.query.order_by(Player.name).all()
|
||||
|
||||
return render_template('content/content_list_new.html',
|
||||
playlists=playlists,
|
||||
media_files=media_files,
|
||||
total_media_count=total_media_count,
|
||||
players=players)
|
||||
|
||||
|
||||
@content_bp.route('/media-library')
|
||||
@login_required
|
||||
def media_library():
|
||||
"""View all media files in the library."""
|
||||
media_files = Content.query.order_by(Content.uploaded_at.desc()).all()
|
||||
|
||||
# Group by content type
|
||||
images = [m for m in media_files if m.content_type == 'image']
|
||||
videos = [m for m in media_files if m.content_type == 'video']
|
||||
pdfs = [m for m in media_files if m.content_type == 'pdf']
|
||||
presentations = [m for m in media_files if m.content_type == 'pptx']
|
||||
others = [m for m in media_files if m.content_type not in ['image', 'video', 'pdf', 'pptx']]
|
||||
|
||||
return render_template('content/media_library.html',
|
||||
media_files=media_files,
|
||||
images=images,
|
||||
videos=videos,
|
||||
pdfs=pdfs,
|
||||
presentations=presentations,
|
||||
others=others)
|
||||
|
||||
|
||||
@content_bp.route('/media/<int:media_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_media(media_id: int):
|
||||
"""Delete a media file and remove it from all playlists."""
|
||||
try:
|
||||
media = Content.query.get_or_404(media_id)
|
||||
filename = media.filename
|
||||
|
||||
# Get all playlists containing this media
|
||||
affected_playlists = list(media.playlists.all())
|
||||
|
||||
# Delete physical file
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], media.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
log_action('info', f'Deleted physical file: {filename}')
|
||||
|
||||
# Remove from all playlists (this will cascade properly)
|
||||
db.session.delete(media)
|
||||
|
||||
# Increment version for all affected playlists
|
||||
for playlist in affected_playlists:
|
||||
playlist.version += 1
|
||||
log_action('info', f'Playlist "{playlist.name}" version updated to {playlist.version} (media removed)')
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Clear cache for affected playlists
|
||||
from app.blueprints.players import get_player_playlist
|
||||
from app.extensions import cache
|
||||
for playlist in affected_playlists:
|
||||
for player in playlist.players:
|
||||
cache.delete_memoized(get_player_playlist, player.id)
|
||||
|
||||
if affected_playlists:
|
||||
flash(f'Deleted "{filename}" and removed from {len(affected_playlists)} playlist(s). Playlist versions updated.', 'success')
|
||||
else:
|
||||
flash(f'Deleted "{filename}" successfully.', 'success')
|
||||
|
||||
log_action('info', f'Media deleted: {filename} (affected {len(affected_playlists)} playlists)')
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
log_action('error', f'Error deleting media: {str(e)}')
|
||||
flash(f'Error deleting media: {str(e)}', 'danger')
|
||||
|
||||
return redirect(url_for('content.media_library'))
|
||||
|
||||
|
||||
@content_bp.route('/playlist/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_playlist():
|
||||
|
||||
@@ -249,6 +249,12 @@
|
||||
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;">
|
||||
@@ -258,18 +264,17 @@
|
||||
</h1>
|
||||
|
||||
<div class="main-grid">
|
||||
<!-- Create/Manage Playlists Card -->
|
||||
<!-- 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);">
|
||||
Playlists
|
||||
Create New Playlist
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Create New Playlist Form -->
|
||||
<form method="POST" action="{{ url_for('content.create_playlist') }}" style="margin-bottom: 25px;">
|
||||
<h3 style="margin-bottom: 15px;">Create New Playlist</h3>
|
||||
<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
|
||||
@@ -294,47 +299,6 @@
|
||||
➕ Create Playlist
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr style="margin: 25px 0;">
|
||||
|
||||
<!-- Existing Playlists -->
|
||||
<h3 style="margin-bottom: 15px;">Existing Playlists</h3>
|
||||
<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>
|
||||
|
||||
<!-- Upload Media Card -->
|
||||
@@ -356,9 +320,10 @@
|
||||
|
||||
<!-- Media Library with Thumbnails -->
|
||||
<h3 style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between;">
|
||||
<span>📚 Available Media ({{ media_files|length }})</span>
|
||||
<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: 500px; overflow-y: auto;">
|
||||
<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 }}">
|
||||
@@ -393,6 +358,63 @@
|
||||
</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>
|
||||
|
||||
|
||||
539
app/templates/content/media_library.html
Normal file
539
app/templates/content/media_library.html
Normal file
@@ -0,0 +1,539 @@
|
||||
{% 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() }})" 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.strftime('%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() }})" 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.strftime('%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() }})" 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.strftime('%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() }})" 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.strftime('%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() }})" 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.strftime('%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" 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.5);">
|
||||
<div style="background-color: #fefefe; margin: 10% auto; padding: 30px; border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<h2 style="margin-top: 0; color: #dc3545; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 2rem;">⚠️</span>
|
||||
Confirm Delete
|
||||
</h2>
|
||||
<p style="font-size: 1.1rem; margin: 1.5rem 0;">
|
||||
Are you sure you want to delete <strong id="deleteFilename"></strong>?
|
||||
</p>
|
||||
<div id="playlistWarning" style="display: none; background: #fff3cd; border-left: 4px solid #ffc107; padding: 12px; margin: 1rem 0; border-radius: 4px;">
|
||||
<strong style="color: #856404;">⚠️ Warning:</strong>
|
||||
<p style="margin: 0.5rem 0 0 0; color: #856404;">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>
|
||||
<p style="color: #dc3545; margin: 1rem 0;">
|
||||
<strong>This action cannot be undone!</strong>
|
||||
</p>
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 2rem;">
|
||||
<button onclick="closeDeleteModal()" class="btn" style="background: #6c757d;">
|
||||
Cancel
|
||||
</button>
|
||||
<form id="deleteForm" method="POST" style="margin: 0;">
|
||||
<button type="submit" class="btn" style="background: #dc3545;">
|
||||
Yes, Delete File
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
body.dark-mode #deleteModal {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
body.dark-mode #deleteModal > div {
|
||||
background-color: #1a202c;
|
||||
border: 1px solid #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode #deleteModal h2 {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
body.dark-mode #deleteModal p {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode #deleteModal strong {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
body.dark-mode #playlistWarning {
|
||||
background: #4a3800;
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
body.dark-mode #playlistWarning strong,
|
||||
body.dark-mode #playlistWarning p {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
body.dark-mode #deleteForm button {
|
||||
background: #dc3545 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
body.dark-mode #deleteForm button:hover {
|
||||
background: #a02834 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let deleteMediaId = null;
|
||||
|
||||
function confirmDelete(mediaId, filename, playlistCount) {
|
||||
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';
|
||||
}
|
||||
|
||||
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 %}
|
||||
@@ -211,13 +211,52 @@
|
||||
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;
|
||||
}
|
||||
|
||||
body.dark-mode .card > div[style*="text-align: center"] p {
|
||||
color: #a0aec0 !important;
|
||||
}
|
||||
</style>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h1 style="display: inline-block; margin-right: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<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="float: right; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
<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>
|
||||
@@ -435,14 +474,6 @@ document.addEventListener('keydown', function(event) {
|
||||
🔗 Enter the plain text code (e.g., 8887779) - will be hashed automatically
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="info-box neutral" style="margin-top: 1rem;">
|
||||
<h4 style="margin: 0 0 0.5rem 0; font-size: 0.85rem;">📋 Current Credentials (Read-Only)</h4>
|
||||
<div style="font-size: 0.85rem;">
|
||||
<strong>Auth Code:</strong> <code style="font-size: 0.8rem;">{{ player.auth_code }}</code><br>
|
||||
<strong>Quick Connect Hash:</strong> <code style="font-size: 0.7rem;">{{ player.quickconnect_code[:50] if player.quickconnect_code else 'Not set' }}...</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
|
||||
|
||||
Reference in New Issue
Block a user