Replace emoji icons with local SVG files for consistent rendering

- Created 10 SVG icon files in app/static/icons/ (Feather Icons style)
- Updated base.html with SVG icons in navigation and dark mode toggle
- Updated dashboard.html with icons in stats cards and quick actions
- Updated content_list_new.html (playlist management) with SVG icons
- Updated upload_media.html with upload-related icons
- Updated manage_player.html with player management icons
- Icons use currentColor for automatic theme adaptation
- Removed emoji dependency for better Raspberry Pi compatibility
- Added ICON_INTEGRATION.md documentation
This commit is contained in:
ske087
2025-11-13 21:00:07 +02:00
parent e5a00d19a5
commit 498c03ef00
37 changed files with 4240 additions and 840 deletions

View File

@@ -3,6 +3,7 @@ from flask import Blueprint, request, jsonify, current_app
from functools import wraps
from datetime import datetime, timedelta
import secrets
import bcrypt
from typing import Optional, Dict, List
from app.extensions import db, cache
@@ -142,7 +143,7 @@ def authenticate_player():
'player_name': player.name,
'hostname': player.hostname,
'auth_code': player.auth_code,
'group_id': player.group_id,
'playlist_id': player.playlist_id,
'orientation': player.orientation,
'status': player.status
}
@@ -186,7 +187,7 @@ def verify_auth_code():
'player_id': player.id,
'player_name': player.name,
'hostname': player.hostname,
'group_id': player.group_id,
'playlist_id': player.playlist_id,
'orientation': player.orientation,
'status': player.status
}
@@ -194,6 +195,103 @@ def verify_auth_code():
return jsonify(response), 200
@api_bp.route('/playlists', methods=['GET'])
@rate_limit(max_requests=30, window=60)
def get_playlist_by_quickconnect():
"""Get playlist using hostname and quickconnect code (Kivy player compatible).
Query parameters:
hostname: Player hostname/identifier
quickconnect_code: Quick connect code for authentication
Returns:
JSON with playlist, playlist_version, and hashed_quickconnect
"""
try:
import bcrypt
hostname = request.args.get('hostname')
quickconnect_code = request.args.get('quickconnect_code')
if not hostname or not quickconnect_code:
return jsonify({
'error': 'hostname and quickconnect_code are required',
'playlist': [],
'playlist_version': 0
}), 400
# Find player by hostname and validate quickconnect
player = Player.query.filter_by(hostname=hostname).first()
if not player:
log_action('warning', f'Player not found with hostname: {hostname}')
return jsonify({
'error': 'Player not found',
'playlist': [],
'playlist_version': 0
}), 404
# Validate quickconnect code
if not player.quickconnect_code:
log_action('warning', f'Player {hostname} has no quickconnect code set')
return jsonify({
'error': 'Quickconnect not configured',
'playlist': [],
'playlist_version': 0
}), 403
# Check if quickconnect matches
if player.quickconnect_code != quickconnect_code:
log_action('warning', f'Invalid quickconnect code for player: {hostname}')
return jsonify({
'error': 'Invalid quickconnect code',
'playlist': [],
'playlist_version': 0
}), 403
# Get playlist (with caching)
playlist = get_cached_playlist(player.id)
# Update player's last seen timestamp and status
player.last_seen = datetime.utcnow()
player.status = 'online'
db.session.commit()
# Get playlist version from the assigned playlist
playlist_version = 1
if player.playlist_id:
from app.models import Playlist
assigned_playlist = Playlist.query.get(player.playlist_id)
if assigned_playlist:
playlist_version = assigned_playlist.version
# Hash the quickconnect code for validation on client side
hashed_quickconnect = bcrypt.hashpw(
quickconnect_code.encode('utf-8'),
bcrypt.gensalt()
).decode('utf-8')
log_action('info', f'Playlist fetched for player: {player.name} ({hostname})')
return jsonify({
'player_id': player.id,
'player_name': player.name,
'playlist_id': player.playlist_id,
'playlist_version': playlist_version,
'playlist': playlist,
'hashed_quickconnect': hashed_quickconnect,
'count': len(playlist)
}), 200
except Exception as e:
log_action('error', f'Error getting playlist: {str(e)}')
return jsonify({
'error': 'Internal server error',
'playlist': [],
'playlist_version': 0
}), 500
@api_bp.route('/playlists/<int:player_id>', methods=['GET'])
@rate_limit(max_requests=30, window=60)
@verify_player_auth
@@ -216,11 +314,19 @@ def get_player_playlist(player_id: int):
player.last_seen = datetime.utcnow()
db.session.commit()
# Get playlist version from the assigned playlist
playlist_version = 1
if player.playlist_id:
from app.models import Playlist
assigned_playlist = Playlist.query.get(player.playlist_id)
if assigned_playlist:
playlist_version = assigned_playlist.version
return jsonify({
'player_id': player_id,
'player_name': player.name,
'group_id': player.group_id,
'playlist_version': player.playlist_version,
'playlist_id': player.playlist_id,
'playlist_version': playlist_version,
'playlist': playlist,
'count': len(playlist)
})
@@ -263,78 +369,110 @@ def get_playlist_version(player_id: int):
@cache.memoize(timeout=300)
def get_cached_playlist(player_id: int) -> List[Dict]:
"""Get cached playlist for a player."""
"""Get cached playlist for a player based on assigned playlist."""
from flask import url_for
from app.models import Playlist
player = Player.query.get(player_id)
if not player:
if not player or not player.playlist_id:
return []
# Get content based on group assignment
if player.group_id:
group = Group.query.get(player.group_id)
contents = group.contents.order_by(Content.position).all() if group else []
else:
# Show all content if not in a group
contents = Content.query.order_by(Content.position).all()
# Get the playlist assigned to this player
playlist = Playlist.query.get(player.playlist_id)
if not playlist:
return []
# Build playlist
playlist = []
for content in contents:
playlist.append({
# Get content from playlist (ordered)
content_list = playlist.get_content_ordered()
# Build playlist response
playlist_data = []
for idx, content in enumerate(content_list, start=1):
# Generate full URL for content
from flask import request as current_request
# Get server base URL
server_base = current_request.host_url.rstrip('/')
content_url = f"{server_base}/static/uploads/{content.filename}"
playlist_data.append({
'id': content.id,
'filename': content.filename,
'file_name': content.filename, # Player expects 'file_name' not 'filename'
'type': content.content_type,
'duration': content.duration or 10,
'position': content.position,
'url': f"/static/uploads/{content.filename}",
'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
})
return playlist
return playlist_data
@api_bp.route('/player-feedback', methods=['POST'])
@rate_limit(max_requests=100, window=60)
@verify_player_auth
def receive_player_feedback():
"""Receive feedback/status updates from players.
"""Receive feedback/status updates from players (Kivy player compatible).
Expected JSON payload:
{
"status": "playing|paused|error",
"current_content_id": 123,
"message": "Optional status message",
"error": "Optional error message"
"player_name": "Screen1",
"quickconnect_code": "ABC123",
"status": "playing|paused|error|restarting",
"message": "Status message",
"playlist_version": 1,
"error_details": "Optional error details",
"timestamp": "ISO timestamp"
}
"""
try:
player = request.player
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
player_name = data.get('player_name')
quickconnect_code = data.get('quickconnect_code')
if not player_name or not quickconnect_code:
return jsonify({'error': 'player_name and quickconnect_code required'}), 400
# Find player by name and validate quickconnect
player = Player.query.filter_by(name=player_name).first()
if not player:
log_action('warning', f'Player feedback from unknown player: {player_name}')
return jsonify({'error': 'Player not found'}), 404
# Validate quickconnect code
if player.quickconnect_code != quickconnect_code:
log_action('warning', f'Invalid quickconnect in feedback from: {player_name}')
return jsonify({'error': 'Invalid quickconnect code'}), 403
# Create feedback record
status = data.get('status', 'unknown')
message = data.get('message', '')
error_details = data.get('error_details')
feedback = PlayerFeedback(
player_id=player.id,
status=data.get('status', 'unknown'),
current_content_id=data.get('current_content_id'),
message=data.get('message'),
error=data.get('error')
status=status,
message=message,
error=error_details
)
db.session.add(feedback)
# Update player's last seen
# Update player's last seen and status
player.last_seen = datetime.utcnow()
player.status = data.get('status', 'unknown')
player.status = status
db.session.commit()
log_action('info', f'Feedback received from {player_name}: {status} - {message}')
return jsonify({
'success': True,
'message': 'Feedback received'
})
'message': 'Feedback received',
'player_id': player.id
}), 200
except Exception as e:
db.session.rollback()