composition is ok

This commit is contained in:
jr-k 2024-08-23 22:56:44 +02:00
parent 56c421ed63
commit 442f02ff37
8 changed files with 207 additions and 38 deletions

View File

@ -162,5 +162,12 @@ jQuery(document).ready(function ($) {
showToast(l.js_common_copied);
});
$(window).on('beforeunload', function(event) {
$('.modal').each(function() {
$(this).find('button[type=submit]').removeClass('hidden');
$(this).find('.btn-loading').addClass('hidden');
});
});
});

View File

@ -2,9 +2,9 @@ jQuery(document).ready(function ($) {
let currentElement = null;
let elementCounter = 0;
$('.screen').css({
width: $('.screen').width(),
height: $('.screen').height(),
$('#screen').css({
width: $('#screen').width(),
height: $('#screen').height(),
position: 'relative',
}).parents('.screen-holder:eq(0)').css({
width: 'auto',
@ -72,7 +72,7 @@ jQuery(document).ready(function ($) {
focusElement($(this));
});
$('#screen').append(element);
screen.append(element);
addElementToList(elementId);
if (config !== null && config.contentId !== null) {
@ -291,8 +291,8 @@ jQuery(document).ready(function ($) {
$(document).on('submit', 'form.form', function (e) {
unfocusElements();
const composites = getCompositesPayload();
$('#content-edit-location').val(JSON.stringify(composites));
const layers = getLayersPayload();
$('#content-edit-location').val(JSON.stringify(layers));
});
function updateZIndexes() {
@ -319,11 +319,11 @@ jQuery(document).ready(function ($) {
applyElementsFromContent();
});
const getCompositesPayload = function() {
const getLayersPayload = function() {
const screen = $('#screen');
const screenWidth = screen.width();
const screenHeight = screen.height();
const composites = [];
const layers = [];
$('.element').each(function () {
const offset = $(this).position();
@ -340,7 +340,7 @@ const getCompositesPayload = function() {
const contentName = $(this).attr('data-content-name');
const contentType = $(this).attr('data-content-type');
composites.push({
layers.push({
xPercent: xPercent,
yPercent: yPercent,
widthPercent: widthPercent,
@ -352,9 +352,9 @@ const getCompositesPayload = function() {
});
});
composites.sort(function(a, b) {
layers.sort(function(a, b) {
return parseInt(b.zIndex) - parseInt(a.zIndex);
});
return composites;
return layers;
};

View File

@ -28,28 +28,31 @@ class PlayerController(ObController):
self._app.add_url_rule('/player/playlist', 'player_playlist', self.player_playlist, methods=['GET'])
self._app.add_url_rule('/player/playlist/use/<playlist_slug_or_id>', 'player_playlist_use', self.player_playlist, methods=['GET'])
self._app.add_url_rule('/serve/content/<content_type>/<content_id>/<content_location>', 'serve_content_file', self.serve_content_file, methods=['GET'])
self._app.add_url_rule('/serve/content/composition/<content_id>', 'serve_content_composition', self.serve_content_composition, methods=['GET'])
def player(self, playlist_slug_or_id: str = ''):
preview_playlist = request.args.get('preview_playlist')
preview_content_id = request.args.get('preview_content_id')
playlist_id = None
playlist_slug_or_id = self._get_dynamic_playlist_id(playlist_slug_or_id)
query = " (slug = ? OR id = ?) "
query_args = {
"slug": playlist_slug_or_id,
"id": playlist_slug_or_id,
}
if not preview_content_id:
query = " (slug = ? OR id = ?) "
query_args = {
"slug": playlist_slug_or_id,
"id": playlist_slug_or_id,
}
if not preview_playlist:
query = query + " AND enabled = ? "
query_args["enabled"] = True
if not preview_playlist:
query = query + " AND enabled = ? "
query_args["enabled"] = True
current_playlist = self._model_store.playlist().get_one_by(query, query_args)
current_playlist = self._model_store.playlist().get_one_by(query, query_args)
if playlist_slug_or_id and not current_playlist:
return abort(404)
if playlist_slug_or_id and not current_playlist:
return abort(404)
playlist_id = current_playlist.id if current_playlist else None
playlist_id = current_playlist.id if current_playlist else None
try:
items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id)
@ -63,6 +66,8 @@ class PlayerController(ObController):
slide_animation_entrance_effect = request.args.get('animation_effect', self._model_store.variable().get_one_by_name('slide_animation_entrance_effect').eval())
slide_animation_exit_effect = request.args.get('slide_animation_exit_effect', self._model_store.variable().get_one_by_name('slide_animation_exit_effect').eval())
return render_template(
'player/player.jinja.html',
items=items,
@ -122,7 +127,7 @@ class PlayerController(ObController):
preview_content = self._model_store.content().get(preview_content_id) if preview_content_id else None
preview_mode = preview_content is not None
if playlist_id == 0 or not playlist_id:
if not preview_mode and (playlist_id == 0 or not playlist_id):
playlist = self._model_store.playlist().get_one_by(query="fallback = 1")
if playlist:
@ -132,8 +137,9 @@ class PlayerController(ObController):
enabled_slides = [Slide(content_id=preview_content.id, duration=1000000)] if preview_mode else self._model_store.slide().get_slides(enabled=True, playlist_id=playlist_id)
slides = self._model_store.slide().to_dict(enabled_slides)
contents = self._model_store.content().get_all_indexed()
playlist = self._model_store.playlist().get(playlist_id)
content_ids = [str(slide['content_id']) for slide in slides if slide['content_id'] is not None]
contents = self._model_store.content().get_all_indexed(query="id IN ({})".format(','.join(content_ids)))
playlist = self._model_store.playlist().get(playlist_id) if not preview_mode else None
position = 9999
playlist_loop = []
@ -248,3 +254,14 @@ class PlayerController(ObController):
response.headers['ETag'] = etag
return response
def serve_content_composition(self, content_id):
content = self._model_store.content().get(content_id)
if not content or content.type != ContentType.COMPOSITION:
abort(404, 'Content not found')
return render_template(
'player/content/composition.jinja.html',
content=content,
)

View File

@ -73,10 +73,12 @@ class ContentManager(ModelManager):
def get_all(self, sort: Optional[str] = 'created_at', ascending=False) -> List[Content]:
return self.hydrate_list(self._db.get_all(table_name=self.TABLE_NAME, sort=sort, ascending=ascending))
def get_all_indexed(self, attribute: str = 'id', multiple=False) -> Dict[str, Content]:
def get_all_indexed(self, attribute: str = 'id', multiple=False, query: str = None) -> Dict[str, Content]:
index = {}
for item in self.get_contents():
items = self.get_by(query) if query else self.get_contents()
for item in items:
id = getattr(item, attribute)
if multiple:
if id not in index:
@ -237,6 +239,14 @@ class ContentManager(ModelManager):
if content.type == ContentType.YOUTUBE:
location = content.location
elif content.type == ContentType.COMPOSITION:
location = "{}/{}".format(
var_external_url if len(var_external_url) > 0 else "",
url_for(
'serve_content_composition',
content_id=content.id
).strip('/')
)
elif content.has_file() or content.type == ContentType.EXTERNAL_STORAGE:
location = "{}/{}".format(
var_external_url if len(var_external_url) > 0 else "",

View File

@ -50,7 +50,6 @@ class WebServer:
host=self._model_store.config().map().get('bind'),
port=self._model_store.config().map().get('port'),
threads=100,
max_request_body_size=self.get_max_upload_size(),
)
def reload(self) -> None:
@ -58,6 +57,7 @@ class WebServer:
def setup(self) -> None:
self._setup_flask_app()
self._setup_flask_headers()
self._setup_web_globals()
self._setup_web_errors()
self._setup_web_controllers()
@ -160,6 +160,19 @@ class WebServer:
def inject_global_vars() -> dict:
return self._template_renderer.get_view_globals()
def _setup_flask_headers(self) -> None:
@self._app.after_request
def modify_headers(response):
# Supprimer les en-têtes de sécurité
response.headers.pop('X-Frame-Options', None)
response.headers.pop('X-Content-Type-Options', None)
response.headers.pop('X-XSS-Protection', None)
# Modifier ou supprimer la politique CSP
response.headers['Content-Security-Policy'] = "frame-ancestors *"
return response
def _setup_web_errors(self) -> None:
def handle_error(error):
if request.headers.get('Content-Type') == 'application/json' or request.headers.get('Accept') == 'application/json':
@ -171,14 +184,16 @@ class WebServer:
})
return make_response(response, error.code)
if error.code == 404:
return send_from_directory(self.get_template_dir(), 'core/error404.html'), 404
return error
return self._template_renderer.render_view(
'@core/http-error.html',
error_code=error.code,
error_message=error.description
)
self._app.register_error_handler(400, handle_error)
self._app.register_error_handler(404, handle_error)
self._app.register_error_handler(409, handle_error)
self._app.register_error_handler(413, handle_error)
self._app.register_error_handler(HttpClientException, handle_error)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<style>
html, body, #screen {
margin: 0;
padding: 0;
background: black;
width: 100vw;
height: 100vh;
overflow: hidden;
}
iframe {
border: none;
}
</style>
</head>
<body>
<div id="screen"></div>
<script src="{{ STATIC_PREFIX }}js/lib/jquery.min.js"></script>
<script>
const base_iframe_route = '{{ url_for('player', preview_content_id='!c!', autoplay=1) }}';
jQuery(function($) {
function createElement(config = null) {
const screen = $('#screen');
const screenWidth = screen.width();
const screenHeight = screen.height();
const elementWidth = (config.widthPercent / 100) * screenWidth
const elementHeight = (config.heightPercent / 100) * screenHeight;
let x = (config.xPercent / 100) * screenWidth;
let y = (config.yPercent / 100) * screenHeight;
const zIndex = config.zIndex;
x = Math.round(Math.max(0, Math.min(x, screenWidth - elementWidth)));
y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
const element = $('<iframe class="element" id="element-' + zIndex + '" data-id="' + zIndex + '" src="'+base_iframe_route.replace('!c!', config.contentId)+'"></iframe>');
element.css({
left: x,
top: y,
width: elementWidth,
height: elementHeight,
zIndex: zIndex,
display: 'block',
position: 'absolute',
transform: `rotate(0deg)`
});
screen.append(element);
}
const applyElementsFromContent = function() {
$('#screen').html('');
const contentData = JSON.parse({{ json_dumps(content.location) | safe }});
for (let i = 0; i < contentData.length; i++) {
if (contentData[i].contentId !== null) {
createElement(contentData[i]);
}
}
};
const main = function() {
applyElementsFromContent();
};
$(window).on('resize', function() {
main();
});
main();
});
</script>
</body>
</html>

View File

@ -47,6 +47,7 @@
// Frontend config
const syncWithTime = items['time_sync'];
const previewMode = items['preview_mode'];
const disableAutoplay = {{ 'false' if request.args.get('autoplay') == '1' else 'previewMode' }};
const tickRefreshResolutionMs = 100;
// Frontend flag updates
@ -345,6 +346,9 @@
case 'youtube':
loadYoutube(element, callbackReady, item);
break;
case 'composition':
loadComposition(element, callbackReady, item);
break;
default:
loadUrl(element, callbackReady, item);
break;
@ -361,6 +365,26 @@
callbackReady(function() {});
};
const loadComposition = function(element, callbackReady, item) {
element.innerHTML = `composition`;
callbackReady(function() {});
const loadingDelayMs = 1000;
let delayNoisyContentJIT = Math.max(100, (lookupCurrentItem().duration * 1000) - loadingDelayMs);
delayNoisyContentJIT = lookupCurrentItem().id !== item.id ? delayNoisyContentJIT : 0;
const autoplayLoader = function() {
if (secondsBeforeNext * 1000 > loadingDelayMs) {
return setTimeout(autoplayLoader, 500);
}
if (element.innerHTML === 'composition') {
element.innerHTML = `<iframe src="${item.location}" frameborder="0" allow="autoplay" allowfullscreen></iframe>`;
}
}
setTimeout(autoplayLoader, delayNoisyContentJIT);
};
const loadYoutube = function(element, callbackReady, item) {
element.innerHTML = `youtube`;
callbackReady(function() {});
@ -375,7 +399,7 @@
}
if (element.innerHTML === 'youtube') {
const autoplay = previewMode ? '0' : '1';
const autoplay = disableAutoplay ? '0' : '1';
element.innerHTML = `<iframe src="https://www.youtube.com/embed/${item.location}?version=3&autoplay=${autoplay}&showinfo=0&controls=0&modestbranding=1&fs=1&rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>`;
}
}
@ -383,7 +407,7 @@
};
const loadVideo = function(element, callbackReady, item) {
element.innerHTML = `<video ${previewMode ? 'controls' : ''}><source src=${item.location} type="video/mp4" /></video>`;
element.innerHTML = `<video ${disableAutoplay ? 'controls' : ''}><source src=${item.location} type="video/mp4" /></video>`;
const video = element.querySelector('video');
callbackReady(function() {});
@ -407,7 +431,7 @@
}
if (element.innerHTML.match('<video>')) {
if (!previewMode) {
if (!disableAutoplay) {
setTimeout(function() {
video.play();
pausableContent = video;