Merge pull request #21 from jr-k/feature/datetimepicker-slide-scheduler

Time and date picker for schedule feature
This commit is contained in:
JRK 2024-05-05 20:52:54 +02:00 committed by GitHub
commit 290b23d1fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 127 additions and 18 deletions

13
data/www/css/flatpickr.min.css vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -516,10 +516,14 @@ form .form-group input[type=checkbox] {
flex: 0; flex: 0;
} }
form .form-group input[type=checkbox].trigger { form .form-group .trigger {
margin-right: 10px; margin-right: 10px;
} }
form .form-group select.trigger {
max-width: 120px;
}
form .form-group span { form .form-group span {
margin-left: 10px; margin-left: 10px;
} }

2
data/www/js/flatpickr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,36 @@ jQuery(document).ready(function ($) {
const $tableInactive = $('table.inactive-slides'); const $tableInactive = $('table.inactive-slides');
const $modalsRoot = $('.modals'); const $modalsRoot = $('.modals');
const validateCronDateTime = function(cronExpression) {
const pattern = /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\*\s+(\d+)$/;
return pattern.test(cronExpression);
};
const getCronDateTime = function(cronExpression) {
const [minutes, hours, day, month, _, year] = cronExpression.split(' ');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}`;
};
const loadDateTimePicker = function($el) {
$el.val('');
const pickr = $el.flatpickr({
enableTime: true,
time_24hr: true,
allowInput: false,
allowInvalidPreload: false,
dateFormat: 'Y-m-d H:i',
onChange: function(selectedDates, dateStr, instance) {
const d = selectedDates[0];
const $target = $el.parents('.widget:eq(0)').find('.target');
$target.val(
d ? `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${(d.getMonth() + 1)} * ${d.getFullYear()}` : ''
);
}
});
$el.addClass('hidden');
};
const getId = function ($el) { const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level'); return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
}; };
@ -68,12 +98,21 @@ jQuery(document).ready(function ($) {
updateTable(); updateTable();
}); });
$(document).on('change', '.modal-slide input[type=checkbox]', function () { $(document).on('change', '.modal-slide select.trigger', function () {
const $target = $('#'+ $(this).attr('id').replace('-trigger', '')); const $target = $(this).parents('.widget:eq(0)').find('.target');
const hide = !$(this).is(':checked'); const $datetimepicker = $(this).parents('.widget:eq(0)').find('.datetimepicker');
$target.toggleClass('hidden', hide);
if (hide) { const isDateTime = $(this).val() === 'datetime';
const isLoop = $(this).val() === 'loop';
const flushValue = isLoop;
const hideCronField = isLoop || isDateTime;
const hideDateTimeField = !isDateTime;
$target.toggleClass('hidden', hideCronField);
$datetimepicker.toggleClass('hidden', hideDateTimeField)
if (flushValue) {
$target.val(''); $target.val('');
} }
}); });
@ -99,20 +138,28 @@ jQuery(document).ready(function ($) {
$(document).on('click', '.slide-add', function () { $(document).on('click', '.slide-add', function () {
showModal('modal-slide-add'); showModal('modal-slide-add');
loadDateTimePicker($('.modal-slide-add .datetimepicker'))
$('.modal-slide-add input:eq(0)').focus().select(); $('.modal-slide-add input:eq(0)').focus().select();
}); });
$(document).on('click', '.slide-edit', function () { $(document).on('click', '.slide-edit', function () {
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity')); const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-slide-edit'); showModal('modal-slide-edit');
loadDateTimePicker($('.modal-slide-edit .datetimepicker'))
const hasCron = slide.cron_schedule && slide.cron_schedule.length > 0; const hasCron = slide.cron_schedule && slide.cron_schedule.length > 0;
const hasDateTime = hasCron && validateCronDateTime(slide.cron_schedule);
$('.modal-slide-edit input:visible:eq(0)').focus().select(); $('.modal-slide-edit input:visible:eq(0)').focus().select();
$('#slide-edit-name').val(slide.name); $('#slide-edit-name').val(slide.name);
$('#slide-edit-type').val(slide.type); $('#slide-edit-type').val(slide.type);
$('#slide-edit-location').val(slide.location); $('#slide-edit-location').val(slide.location);
$('#slide-edit-duration').val(slide.duration); $('#slide-edit-duration').val(slide.duration);
$('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron); $('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron || hasDateTime);
$('#slide-edit-cron-schedule-trigger').prop('checked', hasCron); $('#slide-edit-cron-schedule-trigger').val(hasDateTime ? 'datetime' : (hasCron ? 'cron' : 'loop'));
$('#slide-edit-cron-schedule-datetimepicker').toggleClass('hidden', !hasDateTime).val(
hasDateTime ? getCronDateTime(slide.cron_schedule) : ''
);
$('#slide-edit-id').val(slide.id); $('#slide-edit-id').val(slide.id);
}); });

View File

@ -26,6 +26,10 @@
"slideshow_slide_form_label_duration": "Duration", "slideshow_slide_form_label_duration": "Duration",
"slideshow_slide_form_label_duration_unit": "seconds", "slideshow_slide_form_label_duration_unit": "seconds",
"slideshow_slide_form_label_cron_scheduled": "Scheduled", "slideshow_slide_form_label_cron_scheduled": "Scheduled",
"slideshow_slide_form_label_cron_scheduled_loop": "In the loop",
"slideshow_slide_form_label_cron_scheduled_datetime": "Date & Time",
"slideshow_slide_form_label_cron_scheduled_datetime_placeholder": "Set a date and time",
"slideshow_slide_form_label_cron_scheduled_cron": "Cron",
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Use crontab format: * * * * *", "slideshow_slide_form_widget_cron_scheduled_placeholder": "Use crontab format: * * * * *",
"slideshow_slide_form_button_cancel": "Cancel", "slideshow_slide_form_button_cancel": "Cancel",
"js_slideshow_slide_delete_confirmation": "Are you sure?", "js_slideshow_slide_delete_confirmation": "Are you sure?",

View File

@ -26,6 +26,10 @@
"slideshow_slide_form_label_duration": "Durée", "slideshow_slide_form_label_duration": "Durée",
"slideshow_slide_form_label_duration_unit": "secondes", "slideshow_slide_form_label_duration_unit": "secondes",
"slideshow_slide_form_label_cron_scheduled": "Programmer", "slideshow_slide_form_label_cron_scheduled": "Programmer",
"slideshow_slide_form_label_cron_scheduled_loop": "Dans la boucle",
"slideshow_slide_form_label_cron_scheduled_datetime": "Date & Heure",
"slideshow_slide_form_label_cron_scheduled_datetime_placeholder": "Choisir une date et un heure",
"slideshow_slide_form_label_cron_scheduled_cron": "Cron",
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Utiliser le format crontab: * * * * *", "slideshow_slide_form_widget_cron_scheduled_placeholder": "Utiliser le format crontab: * * * * *",
"slideshow_slide_form_button_cancel": "Annuler", "slideshow_slide_form_button_cancel": "Annuler",
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?", "js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",

View File

@ -9,7 +9,7 @@ from src.model.hook.HookRegistration import HookRegistration
from src.model.hook.StaticHookRegistration import StaticHookRegistration from src.model.hook.StaticHookRegistration import StaticHookRegistration
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
from src.constant.WebDirConstant import WebDirConstant from src.constant.WebDirConstant import WebDirConstant
from src.utils import get_safe_cron_descriptor from src.utils import get_safe_cron_descriptor, is_validate_cron_date_time
class TemplateRenderer: class TemplateRenderer:
@ -29,7 +29,8 @@ class TemplateRenderer:
VERSION=self._model_store.config().map().get('version'), VERSION=self._model_store.config().map().get('version'),
LANG=self._model_store.variable().map().get('lang').as_string(), LANG=self._model_store.variable().map().get('lang').as_string(),
HOOK=self._render_hook, HOOK=self._render_hook,
cron_descriptor=self.cron_descriptor cron_descriptor=self.cron_descriptor,
is_validate_cron_date_time=is_validate_cron_date_time
) )
for hook in HookType: for hook in HookType:

View File

@ -8,7 +8,22 @@ from cron_descriptor import ExpressionDescriptor
from cron_descriptor.Exception import FormatException, WrongArgumentException, MissingFieldException from cron_descriptor.Exception import FormatException, WrongArgumentException, MissingFieldException
def is_validate_cron_date_time(expression) -> bool:
pattern = re.compile(r'^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\*\s+(\d+)$')
return bool(pattern.match(expression))
def get_safe_cron_descriptor(expression: str, use_24hour_time_format=True, locale_code: Optional[str] = None) -> str: def get_safe_cron_descriptor(expression: str, use_24hour_time_format=True, locale_code: Optional[str] = None) -> str:
if is_validate_cron_date_time(expression):
[minutes, hours, day, month, _, year] = expression.split(' ')
return "{}-{}-{} at {}:{}".format(
year,
month.zfill(2),
day.zfill(2),
hours.zfill(2),
minutes.zfill(2)
)
options = { options = {
"expression": expression, "expression": expression,
"use_24hour_time_format": use_24hour_time_format "use_24hour_time_format": use_24hour_time_format

View File

@ -6,6 +6,7 @@
</title> </title>
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
<link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_PREFIX }}favicon/apple-icon-57x57.png"> <link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_PREFIX }}favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_PREFIX }}favicon/apple-icon-60x60.png"> <link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_PREFIX }}favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_PREFIX }}favicon/apple-icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_PREFIX }}favicon/apple-icon-72x72.png">

View File

@ -4,7 +4,10 @@
<title>Obscreen</title> <title>Obscreen</title>
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/animate.min.css" /> <link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
{% if slide_animation_enabled.eval() %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/animate.min.css" />
{% endif %}
<style> <style>
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: white; display: flex; flex-direction: row; justify-content: center; align-items: center; } html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: white; display: flex; flex-direction: row; justify-content: center; align-items: center; }
.slide { display: flex; flex-direction: row; justify-content: center; align-items: center; background: black; } .slide { display: flex; flex-direction: row; justify-content: center; align-items: center; background: black; }

View File

@ -47,7 +47,11 @@
{% if slide.cron_schedule %} {% if slide.cron_schedule %}
{% set cron_desc = cron_descriptor(slide.cron_schedule) %} {% set cron_desc = cron_descriptor(slide.cron_schedule) %}
{% if cron_desc %} {% if cron_desc %}
{% if is_validate_cron_date_time(slide.cron_schedule) %}
📆 {{ cron_desc }}
{% else %}
⏳ {{ cron_desc }} ⏳ {{ cron_desc }}
{% endif %}
{% else %} {% else %}
<span class="error">⚠️ {{ l.slideshow_slide_panel_td_cron_scheduled_bad_cron }}</span> <span class="error">⚠️ {{ l.slideshow_slide_panel_td_cron_scheduled_bad_cron }}</span>
{% endif %} {% endif %}

View File

@ -5,10 +5,12 @@
{% endblock %} {% endblock %}
{% block add_css %} {% block add_css %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/flatpickr.min.css" />
{{ HOOK(H_SLIDESHOW_CSS) }} {{ HOOK(H_SLIDESHOW_CSS) }}
{% endblock %} {% endblock %}
{% block add_js %} {% block add_js %}
<script src="{{ STATIC_PREFIX }}js/flatpickr.min.js"></script>
<script src="{{ STATIC_PREFIX }}js/tablednd-fixed.js"></script> <script src="{{ STATIC_PREFIX }}js/tablednd-fixed.js"></script>
<script src="{{ STATIC_PREFIX }}js/slideshow.js"></script> <script src="{{ STATIC_PREFIX }}js/slideshow.js"></script>
<script src="{{ STATIC_PREFIX }}js/restart.js"></script> <script src="{{ STATIC_PREFIX }}js/restart.js"></script>

View File

@ -25,8 +25,7 @@
<label for="slide-add-duration">{{ l.slideshow_slide_form_label_object }}</label> <label for="slide-add-duration">{{ l.slideshow_slide_form_label_object }}</label>
<div class="widget"> <div class="widget">
<input type="text" name="object" id="slide-add-object-input-text" class="slide-add-object-input" /> <input type="text" name="object" id="slide-add-object-input-text" class="slide-add-object-input" />
<input type="file" name="object" id="slide-add-object-input-upload" <input type="file" name="object" id="slide-add-object-input-upload" class="slide-add-object-input hidden" disabled="disabled" />
class="slide-add-object-input hidden" disabled="disabled" />
</div> </div>
</div> </div>
@ -41,8 +40,13 @@
<div class="form-group"> <div class="form-group">
<label for="slide-add-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label> <label for="slide-add-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
<div class="widget"> <div class="widget">
<input type="checkbox" id="slide-add-cron-schedule-trigger" class="trigger" /> <select id="slide-add-cron-schedule-trigger" class="trigger">
<input type="text" name="cron_schedule" id="slide-add-cron-schedule" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" class="hidden" /> <option value="loop">{{ l.slideshow_slide_form_label_cron_scheduled_loop }}</option>
<option value="datetime">{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}</option>
<option value="cron">{{ l.slideshow_slide_form_label_cron_scheduled_cron }}</option>
</select>
<input type="text" id="slide-add-cron-schedule-datetimepicker" class="datetimepicker" value="" placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}" />
<input type="text" name="cron_schedule" id="slide-add-cron-schedule" class="target hidden" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" />
</div> </div>
</div> </div>

View File

@ -41,8 +41,13 @@
<div class="form-group"> <div class="form-group">
<label for="slide-edit-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label> <label for="slide-edit-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
<div class="widget"> <div class="widget">
<input type="checkbox" id="slide-edit-cron-schedule-trigger" class="trigger" /> <select id="slide-edit-cron-schedule-trigger" class="trigger">
<input type="text" name="cron_schedule" id="slide-edit-cron-schedule" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" class="hidden" /> <option value="loop">{{ l.slideshow_slide_form_label_cron_scheduled_loop }}</option>
<option value="datetime">{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}</option>
<option value="cron">{{ l.slideshow_slide_form_label_cron_scheduled_cron }}</option>
</select>
<input type="text" id="slide-edit-cron-schedule-datetimepicker" class="datetimepicker" value="" placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}" />
<input type="text" name="cron_schedule" id="slide-edit-cron-schedule" class="target hidden" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" />
</div> </div>
</div> </div>