Add edit_on_player_enabled feature for playlist content
- Added edit_on_player_enabled column to playlist_content table - Updated playlist model to track edit enablement per content item - Added UI checkbox on upload media page to enable/disable editing - Added toggle column on manage playlist page for existing content - Updated API endpoint to return edit_on_player_enabled flag to players - Fixed docker-entrypoint.sh to use production config - Supports PDF, Images, and PPTX content types
This commit is contained in:
@@ -401,7 +401,8 @@ def get_cached_playlist(player_id: int) -> List[Dict]:
|
||||
'duration': content._playlist_duration or content.duration or 10,
|
||||
'position': content._playlist_position or idx,
|
||||
'url': content_url, # Full URL for downloads
|
||||
'description': content.description
|
||||
'description': content.description,
|
||||
'edit_on_player_enabled': getattr(content, '_playlist_edit_on_player_enabled', False)
|
||||
})
|
||||
|
||||
return playlist_data
|
||||
|
||||
@@ -396,6 +396,47 @@ def update_playlist_content_muted(playlist_id: int, content_id: int):
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@content_bp.route('/playlist/<int:playlist_id>/update-edit-enabled/<int:content_id>', methods=['POST'])
|
||||
@login_required
|
||||
def update_playlist_content_edit_enabled(playlist_id: int, content_id: int):
|
||||
"""Update content edit_on_player_enabled setting in playlist."""
|
||||
playlist = Playlist.query.get_or_404(playlist_id)
|
||||
|
||||
try:
|
||||
content = Content.query.get_or_404(content_id)
|
||||
edit_enabled = request.form.get('edit_enabled', 'false').lower() == 'true'
|
||||
|
||||
from app.models.playlist import playlist_content
|
||||
from sqlalchemy import update
|
||||
|
||||
# Update edit_on_player_enabled in association table
|
||||
stmt = update(playlist_content).where(
|
||||
(playlist_content.c.playlist_id == playlist_id) &
|
||||
(playlist_content.c.content_id == content_id)
|
||||
).values(edit_on_player_enabled=edit_enabled)
|
||||
db.session.execute(stmt)
|
||||
|
||||
# Increment playlist version
|
||||
playlist.increment_version()
|
||||
|
||||
db.session.commit()
|
||||
cache.clear()
|
||||
|
||||
log_action('info', f'Updated edit_on_player_enabled={edit_enabled} for "{content.filename}" in playlist "{playlist.name}"')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Edit setting updated',
|
||||
'edit_enabled': edit_enabled,
|
||||
'version': playlist.version
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
log_action('error', f'Error updating edit setting: {str(e)}')
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@content_bp.route('/upload-media-page')
|
||||
@login_required
|
||||
def upload_media_page():
|
||||
@@ -527,7 +568,7 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
|
||||
|
||||
def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
||||
duration: int, playlist_id: Optional[int], task_id: str):
|
||||
duration: int, playlist_id: Optional[int], task_id: str, edit_on_player_enabled: bool = False):
|
||||
"""Process large files (PDF, PPTX, Video) in background thread."""
|
||||
with app.app_context():
|
||||
try:
|
||||
@@ -573,7 +614,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
||||
playlist_id=playlist_id,
|
||||
content_id=page_content.id,
|
||||
position=max_position,
|
||||
duration=duration
|
||||
duration=duration,
|
||||
edit_on_player_enabled=edit_on_player_enabled
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
@@ -629,7 +671,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
||||
playlist_id=playlist_id,
|
||||
content_id=slide_content.id,
|
||||
position=max_position,
|
||||
duration=duration
|
||||
duration=duration,
|
||||
edit_on_player_enabled=edit_on_player_enabled
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
@@ -677,7 +720,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
||||
playlist_id=playlist_id,
|
||||
content_id=content.id,
|
||||
position=max_position + 1,
|
||||
duration=duration
|
||||
duration=duration,
|
||||
edit_on_player_enabled=edit_on_player_enabled
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
playlist.version += 1
|
||||
@@ -949,6 +993,7 @@ def upload_media():
|
||||
content_type = request.form.get('content_type', 'image')
|
||||
duration = request.form.get('duration', type=int, default=10)
|
||||
playlist_id = request.form.get('playlist_id', type=int)
|
||||
edit_on_player_enabled = request.form.get('edit_on_player_enabled', '0') == '1'
|
||||
|
||||
if not files or files[0].filename == '':
|
||||
flash('No files provided.', 'warning')
|
||||
@@ -991,7 +1036,7 @@ def upload_media():
|
||||
|
||||
thread = threading.Thread(
|
||||
target=process_file_in_background,
|
||||
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id)
|
||||
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id, edit_on_player_enabled)
|
||||
)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
@@ -1054,7 +1099,8 @@ def upload_media():
|
||||
playlist_id=playlist_id,
|
||||
content_id=page_content.id,
|
||||
position=max_position,
|
||||
duration=duration
|
||||
duration=duration,
|
||||
edit_on_player_enabled=edit_on_player_enabled
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
@@ -1113,7 +1159,8 @@ def upload_media():
|
||||
playlist_id=playlist_id,
|
||||
content_id=slide_content.id,
|
||||
position=max_position,
|
||||
duration=duration
|
||||
duration=duration,
|
||||
edit_on_player_enabled=edit_on_player_enabled
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
@@ -1165,7 +1212,8 @@ def upload_media():
|
||||
playlist_id=playlist_id,
|
||||
content_id=content.id,
|
||||
position=max_position + 1,
|
||||
duration=duration
|
||||
duration=duration,
|
||||
edit_on_player_enabled=edit_on_player_enabled
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ playlist_content = db.Table('playlist_content',
|
||||
db.Column('content_id', db.Integer, db.ForeignKey('content.id', ondelete='CASCADE'), primary_key=True),
|
||||
db.Column('position', db.Integer, default=0),
|
||||
db.Column('duration', db.Integer, default=10),
|
||||
db.Column('muted', db.Boolean, default=True)
|
||||
db.Column('muted', db.Boolean, default=True),
|
||||
db.Column('edit_on_player_enabled', db.Boolean, default=False)
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +79,8 @@ class Playlist(db.Model):
|
||||
stmt = select(playlist_content.c.content_id,
|
||||
playlist_content.c.position,
|
||||
playlist_content.c.duration,
|
||||
playlist_content.c.muted).where(
|
||||
playlist_content.c.muted,
|
||||
playlist_content.c.edit_on_player_enabled).where(
|
||||
playlist_content.c.playlist_id == self.id
|
||||
).order_by(playlist_content.c.position)
|
||||
|
||||
@@ -91,6 +93,7 @@ class Playlist(db.Model):
|
||||
content._playlist_position = row.position
|
||||
content._playlist_duration = row.duration
|
||||
content._playlist_muted = row.muted if len(row) > 3 else True
|
||||
content._playlist_edit_on_player_enabled = row.edit_on_player_enabled if len(row) > 4 else False
|
||||
ordered_content.append(content)
|
||||
|
||||
return ordered_content
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
<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>
|
||||
@@ -247,6 +248,23 @@
|
||||
<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) }}"
|
||||
@@ -427,6 +445,37 @@ function toggleAudio(contentId, 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 => {
|
||||
|
||||
@@ -296,6 +296,17 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; cursor: pointer; user-select: none;">
|
||||
<input type="checkbox" name="edit_on_player_enabled" id="edit_on_player_enabled"
|
||||
value="1" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
|
||||
<span>Allow editing on player (PDF, Images, PPTX)</span>
|
||||
</label>
|
||||
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
|
||||
✏️ Enable local editing of this media on the player device
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<button type="submit" class="btn-upload" id="upload-btn" disabled style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 20px; padding: 12px 30px;">
|
||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||
|
||||
@@ -8,14 +8,14 @@ mkdir -p /app/instance
|
||||
mkdir -p /app/app/static/uploads
|
||||
|
||||
# Initialize database if it doesn't exist
|
||||
if [ ! -f /app/instance/digiserver.db ]; then
|
||||
if [ ! -f /app/instance/dashboard.db ]; then
|
||||
echo "Initializing database..."
|
||||
python -c "
|
||||
from app.app import create_app
|
||||
from app.extensions import db, bcrypt
|
||||
from app.models import User
|
||||
|
||||
app = create_app()
|
||||
app = create_app('production')
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
|
||||
47
migrate_add_edit_enabled.py
Normal file
47
migrate_add_edit_enabled.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Migration: Add edit_on_player_enabled column to playlist_content table."""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
DB_PATH = 'instance/dashboard.db'
|
||||
|
||||
def migrate():
|
||||
"""Add edit_on_player_enabled column to playlist_content."""
|
||||
if not os.path.exists(DB_PATH):
|
||||
print(f"Database not found at {DB_PATH}")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if column already exists
|
||||
cursor.execute("PRAGMA table_info(playlist_content)")
|
||||
columns = [col[1] for col in cursor.fetchall()]
|
||||
|
||||
if 'edit_on_player_enabled' in columns:
|
||||
print("Column 'edit_on_player_enabled' already exists!")
|
||||
return True
|
||||
|
||||
# Add the new column with default value False
|
||||
print("Adding 'edit_on_player_enabled' column to playlist_content table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE playlist_content
|
||||
ADD COLUMN edit_on_player_enabled BOOLEAN DEFAULT 0
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
print("✅ Migration completed successfully!")
|
||||
print("Column 'edit_on_player_enabled' added with default value False (0)")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"❌ Migration failed: {e}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate()
|
||||
Reference in New Issue
Block a user