This commit is contained in:
jr-k 2024-07-26 17:34:06 +02:00
parent ea66a89ce1
commit bd85d39af5
18 changed files with 650 additions and 37 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,271 @@
const getPayload = function() {
let screen = $('#screen');
let screenWidth = screen.width();
let screenHeight = screen.height();
$('.element').each(function () {
let offset = $(this).position();
let x = offset.left;
let y = offset.top;
let width = $(this).width();
let height = $(this).height();
let xPercent = (x / screenWidth) * 100;
let yPercent = (y / screenHeight) * 100;
let widthPercent = (width / screenWidth) * 100;
let heightPercent = (height / screenHeight) * 100;
console.log(JSON.stringify({
xPercent: xPercent,
yPercent: yPercent,
widthPercent: widthPercent,
heightPercent: heightPercent
}));
});
};
jQuery(document).ready(function ($) {
let currentElement = null;
let elementCounter = 0;
$('.screen').css({
width: $('.screen').width(),
height: $('.screen').height(),
position: 'relative',
}).parents('.screen-holder:eq(0)').css({
width: 'auto',
'padding-top': '0px'
});
function createElement() {
let screen = $('#screen');
let screenWidth = screen.width();
let screenHeight = screen.height();
// Dimensions de l'élément
let elementWidth = 100;
let elementHeight = 50;
// Générer des positions aléatoires
let x = Math.round(Math.random() * (screenWidth - elementWidth));
let y = Math.round(Math.random() * (screenHeight - elementHeight));
// Constrain x and y
x = Math.round(Math.max(0, Math.min(x, screenWidth - elementWidth)));
y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
let elementId = elementCounter++;
let element = $('<div class="element" id="element-' + elementId + '" data-id="' + elementId + '"><button>Button</button></div>');
// let element = $('<div class="element" id="' + elementId + '"><button>Button</button><div class="rotate-handle"></div></div>');
element.css({
left: x,
top: y,
width: elementWidth,
height: elementHeight,
transform: `rotate(0deg)`
});
element.draggable({
containment: "#screen",
start: function (event, ui) {
focusElement(ui.helper);
},
drag: function (event, ui) {
updateForm(ui.helper);
}
});
element.resizable({
containment: "#screen",
handles: 'nw, ne, sw, se',
start: function (event, ui) {
focusElement(ui.element);
},
resize: function (event, ui) {
updateForm(ui.element);
}
});
/*
element.rotatable({
handle: element.find('.rotate-handle'),
rotate: function(event, ui) {
updateForm(ui.element);
}
});
*/
element.click(function () {
focusElement($(this));
});
$('#screen').append(element);
addElementToList(elementId);
setTimeout(function() {
focusElement(element);
}, 10);
}
$(document).on('click', '.element-list-item', function(){
focusElement($('#element-' + $(this).attr('data-id')));
})
$(document).on('click', '.remove-element', function(){
removeElementById($(this).attr('data-id'));
})
function removeElementById(elementId) {
$('.element[data-id='+elementId+'], .element-list-item[data-id='+elementId+']').remove();
updateZIndexes();
}
function addElementToList(elementId) {
let listItem = $('<div class="element-list-item" data-id="' + elementId + '">Element ' + elementId + ' <button type="button" class="remove-element" data-id="' + elementId + '">remove</button></div>');
$('#elementList').append(listItem);
updateZIndexes();
}
function unfocusElements() {
$('.element, .element-list-item').removeClass('focused');
currentElement = null;
updateForm(null);
}
function focusElement(element) {
unfocusElements();
currentElement = element;
currentElement.addClass('focused');
const listElement = $('.element-list-item[data-id="' + currentElement.attr('data-id') + '"]');
listElement.addClass('focused');
updateForm(currentElement);
}
function updateForm(element) {
if (!element) {
$('form#elementForm input').val('').prop('disabled', true);
return;
}
$('form#elementForm input').prop('disabled', false);
let offset = element.position();
if (offset !== undefined) {
$('#elem-x').val(offset.left);
$('#elem-y').val(offset.top);
$('#elem-width').val(element.width());
$('#elem-height').val(element.height());
}
/*
let rotation = element.css('transform');
let values = rotation.split('(')[1].split(')')[0].split(',');
let angle = Math.round(Math.atan2(values[1], values[0]) * (180/Math.PI));
$('#elem-rotate').val(angle);
*/
}
$(document).on('input', '#elementForm input', function () {
if (currentElement) {
let screenWidth = $('#screen').width();
let screenHeight = $('#screen').height();
let x = Math.round(parseInt($('#elem-x').val()));
let y = Math.round(parseInt($('#elem-y').val()));
let width = Math.round(parseInt($('#elem-width').val()));
let height = Math.round(parseInt($('#elem-height').val()));
// let rotation = parseInt($('#elem-rotate').val());
// Constrain x and y
x = Math.max(0, Math.min(x, screenWidth - width));
y = Math.max(0, Math.min(y, screenHeight - height));
// Constrain width and height
width = Math.min(width, screenWidth - x);
height = Math.min(height, screenHeight - y);
currentElement.css({
left: x,
top: y,
width: width,
height: height
// transform: `rotate(${rotation}deg)`
});
// Update form values to reflect clamped values
$('#elem-x').val(x);
$('#elem-y').val(y);
$('#elem-width').val(width);
$('#elem-height').val(height);
}
});
$(document).on('click', '#addElement', function () {
createElement();
});
$(document).on('click', '#removeAllElements', function () {
$('.element, .element-list-item').remove();
updateZIndexes();
});
$(document).on('mousedown', function (e) {
const keepFocusedElement = $(e.target).hasClass('element')
|| $(e.target).hasClass('element-list-item')
|| $(e.target).parents('.element:eq(0)').length !== 0
|| $(e.target).parents('.element-list-item:eq(0)').length !== 0
|| $(e.target).is('input,select,textarea')
if (!keepFocusedElement) {
unfocusElements();
}
});
$(document).on('click', '#presetGrid2x2', function () {
let screenWidth = $('#screen').width();
let screenHeight = $('#screen').height();
let elements = $('.element');
if (elements.length < 4) {
while (elements.length < 4) {
createElement();
elements = $('.element');
}
}
let gridPositions = [
{x: 0, y: 0},
{x: screenWidth / 2, y: 0},
{x: 0, y: screenHeight / 2},
{x: screenWidth / 2, y: screenHeight / 2}
];
elements.each(function (index) {
let position = gridPositions[index];
$(this).css({
left: position.x,
top: position.y,
width: screenWidth / 2,
height: screenHeight / 2
});
updateForm($(this));
});
});
function updateZIndexes() {
const zindex = $('.element-list-item').length + 1;
$('.element-list-item').each(function(index) {
let id = $(this).attr('data-id');
$('#element-' + id).css('z-index', zindex - index);
});
}
$('#elementList').sortable({
update: function(event, ui) {
updateZIndexes();
}
});
createElement();
updateForm(null);
});

View File

@ -23,6 +23,29 @@
.view-content-edit main .main-container {
.top-content {
h3 {
color: $gscaleF;
padding: 10px 10px 10px 0;
font-size: 16px;
align-self: stretch;
flex: 1;
text-align: right;
span {
border-width: 1px;
border-style: solid;
border-radius: $baseRadius;
padding: 4px 10px;
margin-left: 5px;
}
i {
font-size: 16px;
}
}
}
.bottom-content {
.page-content {
flex: 1;
@ -43,27 +66,6 @@
align-items: center;
padding: 20px;
h3 {
color: $gscaleF;
padding: 10px 10px 10px 0;
margin-bottom: 20px;
font-size: 16px;
align-self: stretch;
margin-left: -8px;
span {
border-width: 1px;
border-style: solid;
border-radius: $baseRadius;
padding: 4px 10px;
margin-left: 5px;
}
i {
font-size: 16px;
}
}
.iframe-wrapper {
display: flex;
flex-direction: column;
@ -88,6 +90,179 @@
}
.view-content-edit.view-content-edit-composition main .main-container {
.page-panel.left-panel {
flex: 1;
.form-holder {
margin: 20px 20px 20px 10px;
flex: 1;
}
}
.page-content {
flex: 2;
}
.page-panel.right-panel {
flex: 1;
}
.toolbar {
margin: 20px;
}
.screen-holder {
//display: flex;
//flex-direction: row;
display: flex;
flex-direction: column;
width: 100%;
position: relative;
padding-top: 56.25%; /* 16:9 aspect ratio */
overflow: hidden;
border-radius: $baseRadius;
outline: 4px solid rgba($gscaleF, .1);
.screen {
background-color: #ddd;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
.element {
position: absolute !important;
background-color: #f0f0f0;
outline: 1px solid rgba($black, .5);
text-align: center;
box-sizing: border-box;
&.focused {
border: none;
outline: 2px solid blue;
z-index: 89 !important;
.ui-resizable-handle {
display: block;
}
}
.rotate-handle {
width: 10px;
height: 10px;
background-color: red;
position: absolute;
top: 50%;
right: -15px;
cursor: pointer;
transform: translateY(-50%);
}
.ui-resizable-handle {
background: black;
border: 1px solid #000;
width: 10px;
height: 10px;
z-index: 90;
display: none;
position: absolute;
&.ui-resizable-nw {
cursor: nw-resize;
top: -5px;
left: -5px;
}
&.ui-resizable-ne {
cursor: ne-resize;
top: -5px;
right: -5px;
}
&.ui-resizable-sw {
cursor: sw-resize;
bottom: -5px;
left: -5px;
}
&.ui-resizable-se {
cursor: se-resize;
bottom: -5px;
right: -5px;
}
}
}
}
}
.elements-holder {
align-self: stretch;
h3 {
font-size: 16px;
font-weight: 500;
color: $gscaleD;
text-decoration: none;
margin: 0 0 20px 0;
&.divide {
border-top: 1px solid $gscale2;
margin-top: 10px;
padding-top: 20px;
}
}
.form-elements-list {
background: $gscale2;
padding: 10px;
border-radius: $baseRadius;
.element-list-item {
cursor: pointer;
padding: 5px;
border: 1px solid #ccc;
margin-bottom: 5px;
&.focused {
background-color: #d0ebff;
}
}
}
}
.form-element-properties {
margin-left: 20px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ccc;
form {
display: flex;
flex-direction: column;
label,
input {
margin-bottom: 10px;
}
}
#elementList {
h3 {
margin: 0 0 10px 0;
}
}
}
}

View File

@ -14,6 +14,7 @@ $layoutBorder: 1px solid $gscale2;
// Packs
$colors: (
warning: $warning,
orange: $orange,
info: $info,
info-alt: $bitterBlue,
success: $success,

View File

@ -290,6 +290,7 @@
"enum_content_type_external_storage": "External Storage",
"enum_content_type_external_storage_object_label": "Specify an existing directory relative to the following path",
"enum_content_type_external_storage_flashdrive_label": "Path relative to a removeable device",
"enum_content_type_composition": "Composition",
"enum_content_type_url": "URL",
"enum_content_type_video": "Video",
"enum_content_type_picture": "Picture",

View File

@ -291,6 +291,7 @@
"enum_content_type_external_storage": "Almacenamiento externo",
"enum_content_type_external_storage_object_label": "Especifique un directorio existente relativo a la siguiente ruta",
"enum_content_type_external_storage_flashdrive_label": "Ruta relativa a un dispositivo extraíble",
"enum_content_type_composition": "Composición",
"enum_content_type_url": "URL",
"enum_content_type_video": "Video",
"enum_content_type_picture": "Imagen",

View File

@ -292,6 +292,7 @@
"enum_content_type_external_storage": "Stockage externe",
"enum_content_type_external_storage_object_label": "Spécifiez un répertoire existant par rapport au chemin suivant",
"enum_content_type_external_storage_flashdrive_label": "Chemin relatif à un périphérique amovible",
"enum_content_type_composition": "Composition",
"enum_content_type_url": "URL",
"enum_content_type_video": "Vidéo",
"enum_content_type_picture": "Image",

View File

@ -291,6 +291,7 @@
"enum_content_type_external_storage": "Archiviazione esterna",
"enum_content_type_external_storage_object_label": "Specificare una directory esistente relativi al seguente percorso",
"enum_content_type_external_storage_flashdrive_label": "Percorso relativo ad un dispositivo rimovibile",
"enum_content_type_composition": "Composizione",
"enum_content_type_url": "URL",
"enum_content_type_video": "Video",
"enum_content_type_picture": "Immagine",

View File

@ -112,9 +112,13 @@ class ContentController(ObController):
return abort(404)
working_folder_path, working_folder = self.get_working_folder()
edit_view = 'slideshow/contents/edit.jinja.html'
if content.type == ContentType.COMPOSITION:
edit_view = 'slideshow/contents/edit-composition.jinja.html'
return render_template(
'slideshow/contents/edit.jinja.html',
edit_view,
content=content,
working_folder_path=working_folder_path,
working_folder=working_folder,

View File

@ -11,6 +11,7 @@ class ContentInputType(Enum):
UPLOAD = 'upload'
TEXT = 'text'
STORAGE = 'storage'
HIDDEN = 'hidden'
@staticmethod
def is_editable(value: Enum) -> bool:
@ -29,6 +30,7 @@ class ContentType(Enum):
YOUTUBE = 'youtube'
VIDEO = 'video'
EXTERNAL_STORAGE = 'external_storage'
COMPOSITION = 'composition'
@staticmethod
def guess_content_type_file(filename: str):
@ -61,6 +63,8 @@ class ContentType(Enum):
return ContentInputType.TEXT
elif value == ContentType.EXTERNAL_STORAGE:
return ContentInputType.STORAGE
elif value == ContentType.COMPOSITION:
return ContentInputType.HIDDEN
@staticmethod
def get_fa_icon(value: Union[Enum, str]) -> str:
@ -77,6 +81,8 @@ class ContentType(Enum):
return 'fa-link'
elif value == ContentType.EXTERNAL_STORAGE:
return 'fa-brands fa-usb'
elif value == ContentType.COMPOSITION:
return 'fa-solid fa-clone'
return 'fa-file'
@ -95,5 +101,7 @@ class ContentType(Enum):
return 'danger'
elif value == ContentType.EXTERNAL_STORAGE:
return 'other'
elif value == ContentType.COMPOSITION:
return 'warning'
return 'neutral'

View File

@ -36,6 +36,7 @@ class TemplateRenderer:
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
last_pillmenu_slideshow=self._model_store.variable().map().get('last_pillmenu_slideshow').as_string(),
last_pillmenu_configuration=self._model_store.variable().map().get('last_pillmenu_configuration').as_string(),
external_url=self._model_store.variable().map().get('external_url').as_string().strip('/'),
last_pillmenu_fleet=self._model_store.variable().map().get('last_pillmenu_fleet').as_string(),
last_pillmenu_security=self._model_store.variable().map().get('last_pillmenu_security').as_string(),
track_created=self._model_store.user().track_user_created,

View File

@ -64,7 +64,8 @@
{% if current_player_group.playlist_id %}
<div class="preview-holder">
{% set preview_url = request.scheme ~ '://' ~ request.headers.get('host') ~ url_for('player_use', playlist_slug_or_id=current_player_group.playlist_id) %}
{% set base_url = external_url if external_url else request.scheme ~ '://' ~ request.headers.get('host') %}
{% set preview_url = base_url~ url_for('player_use', playlist_slug_or_id=current_player_group.playlist_id) %}
<h4 class="divide">
Iframe

View File

@ -82,7 +82,8 @@
</form>
</div>
<div class="preview-holder">
{% set preview_url = request.scheme ~ '://' ~ request.headers.get('host') ~ url_for('player_use', playlist_slug_or_id=current_playlist.slug) %}
{% set base_url = external_url if external_url else request.scheme ~ '://' ~ request.headers.get('host') %}
{% set preview_url = base_url ~ url_for('player_use', playlist_slug_or_id=current_playlist.slug) %}
<h4 class="divide">
URL
</h4>

View File

@ -24,7 +24,7 @@
<i class="fa fa-play"></i>
</sub>
{% endif %}
<a href="{% if use_href %}{{ url_for('slideshow_content_edit', content_id=content.id) }}{% else %}javascript:void(0);{% endif %}"
<a href="{% if use_href %}{{ url_for('slideshow_content_edit', content_id=content.id, path=folder.path) }}{% else %}javascript:void(0);{% endif %}"
class="{{ 'explr-pick-element' if not use_href }}">
{{ content.name }}
</a>

View File

@ -0,0 +1,145 @@
{% set active_pill_route='slideshow_content_list' %}
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.slideshow_content_page_title }}
{% endblock %}
{% block add_css %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css"/>
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/jquery-explr-1.4.css"/>
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
{% endblock %}
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
<script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
{# <script src="{{ STATIC_PREFIX }}js/lib/jquery-ui-rotatable.min.js"></script> #}
<script src="{{ STATIC_PREFIX }}js/slideshow/content-composition.js"></script>
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
{% endblock %}
{% block body_class %}view-content-edit view-content-edit-composition edit-page{% endblock %}
{% block page %}
<div class="top-content">
<h1>
{{ l.slideshow_content_form_edit_title }}
</h1>
{% set icon = enum_content_type.get_fa_icon(content.type) %}
{% set color = enum_content_type.get_color_icon(content.type) %}
<h3>
<span class="{{ color }} border-{{ color }}">
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
</span>
</h3>
</div>
{% if request.args.get('saved') %}
<div class="alert alert-success alert-timeout">
<i class="fa fa-check icon-left"></i>
{{ l.common_saved }}
</div>
{% endif %}
<div class="bottom-content">
<div class="page-panel left-panel">
<div class="inner dirview">
<div class="breadcrumb-container">
<ul class="breadcrumb">
{% set ns = namespace(breadpath='') %}
{% for dir in working_folder_path[1:].split('/') %}
{% set ns.breadpath = ns.breadpath ~ '/' ~ dir %}
<li>
<a href="{{ url_for('slideshow_content_cd', path=ns.breadpath) }}">
<i class="explr-icon explr-icon-folder"></i>
{{ truncate(dir, 25, '...') }}
</a>
</li>
{% if not loop.last %}
<li class="divider">
<i class="fa fa-chevron-right"></i>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="horizontal">
<div class="form-holder">
<form class="form"
action="{{ url_for('slideshow_content_save', content_id=content.id) }}?path={{ working_folder_path }}"
method="POST">
<div class="form-group">
<label for="content-edit-name">{{ l.slideshow_content_form_label_name }}</label>
<div class="widget">
<input type="text" name="name" id="content-edit-name" required="required"
value="{{ content.name }}"/>
</div>
</div>
<div class="elements-holder">
<h3 class="divide">Elements</h3>
<div class="form-elements-list" id="elementList">
</div>
</div>
<div class="actions actions-right">
<button type="submit" class="btn btn-info">
<i class="fa fa-save icon-left"></i>
{{ l.common_save }}
</button>
<a href="{{ url_for('slideshow_content_list') }}" class="btn btn-naked">
<i class="fa fa-rectangle-xmark icon-left"></i>
{{ l.common_cancel }}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="page-content">
<div class="inner">
<div class="toolbar">
<button id="presetGrid2x2">Grid 2x2</button>
<button id="addElement">Add Element</button>
<button id="removeAllElements">Remove All Elements</button>
</div>
<div class="screen-holder">
<div class="screen" id="screen">
<!-- Elements will be dynamically added here -->
</div>
</div>
</div>
</div>
<div class="page-panel right-panel">
<div class="form-element-properties">
<form id="elementForm">
<h3>Element Properties</h3>
<label for="elem-x">X:</label>
<input type="number" id="elem-x" name="elem-x"><br>
<label for="elem-y">Y:</label>
<input type="number" id="elem-y" name="elem-y"><br>
<label for="elem-width">Width:</label>
<input type="number" id="elem-width" name="elem-width"><br>
<label for="elem-height">Height:</label>
<input type="number" id="elem-height" name="elem-height"><br>
<!--<label for="elem-rotate">Rotate (deg):</label>-->
<!--<input type="number" id="elem-rotate" name="elem-rotate"><br>-->
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -25,6 +25,15 @@
<h1>
{{ l.slideshow_content_form_edit_title }}
</h1>
{% set icon = enum_content_type.get_fa_icon(content.type) %}
{% set color = enum_content_type.get_color_icon(content.type) %}
<h3>
<span class="{{ color }} border-{{ color }}">
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
</span>
</h3>
</div>
{% if request.args.get('saved') %}
@ -80,7 +89,7 @@
{% if content.type == enum_content_type.EXTERNAL_STORAGE %}
<input type="text" class="disabled" disabled value="{{ chroot_http_external_storage }}/" />
{% endif %}
{% set location = content.location %}
{% if content.type == enum_content_type.YOUTUBE %}
{% set location = 'https://www.youtube.com/watch?v=' ~ content.location %}
@ -89,7 +98,7 @@
</div>
</div>
<div class="actions actions-left">
<div class="actions actions-right">
<button type="submit" class="btn btn-info">
<i class="fa fa-save icon-left"></i>
{{ l.common_save }}
@ -107,14 +116,6 @@
<div class="page-panel right-panel">
{% set icon = enum_content_type.get_fa_icon(content.type) %}
{% set color = enum_content_type.get_color_icon(content.type) %}
<h3>
<span class="{{ color }} border-{{ color }}">
<i class="fa {{ icon }} {{ color }}"></i> {{ t(content.type) }}
</span>
</h3>
<div class="iframe-wrapper">
<iframe src="{{ url_for('player', preview_content_id=content.id) }}"></iframe>
</div>