Merge pull request #22 from jr-k/develop

Release v1.7
This commit is contained in:
JRK 2024-05-05 20:53:49 +02:00 committed by GitHub
commit 74a8283211
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 276 additions and 53 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;
}
form .form-group input[type=checkbox].trigger {
form .form-group .trigger {
margin-right: 10px;
}
form .form-group select.trigger {
max-width: 120px;
}
form .form-group span {
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 $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) {
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();
});
$(document).on('change', '.modal-slide input[type=checkbox]', function () {
const $target = $('#'+ $(this).attr('id').replace('-trigger', ''));
const hide = !$(this).is(':checked');
$target.toggleClass('hidden', hide);
$(document).on('change', '.modal-slide select.trigger', function () {
const $target = $(this).parents('.widget:eq(0)').find('.target');
const $datetimepicker = $(this).parents('.widget:eq(0)').find('.datetimepicker');
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('');
}
});
@ -99,20 +138,28 @@ jQuery(document).ready(function ($) {
$(document).on('click', '.slide-add', function () {
showModal('modal-slide-add');
loadDateTimePicker($('.modal-slide-add .datetimepicker'))
$('.modal-slide-add input:eq(0)').focus().select();
});
$(document).on('click', '.slide-edit', function () {
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-slide-edit');
loadDateTimePicker($('.modal-slide-edit .datetimepicker'))
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();
$('#slide-edit-name').val(slide.name);
$('#slide-edit-type').val(slide.type);
$('#slide-edit-location').val(slide.location);
$('#slide-edit-duration').val(slide.duration);
$('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron);
$('#slide-edit-cron-schedule-trigger').prop('checked', hasCron);
$('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron || hasDateTime);
$('#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);
});
@ -137,4 +184,4 @@ jQuery(document).ready(function ($) {
});
main();
});
});

View File

@ -26,6 +26,10 @@
"slideshow_slide_form_label_duration": "Duration",
"slideshow_slide_form_label_duration_unit": "seconds",
"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_button_cancel": "Cancel",
"js_slideshow_slide_delete_confirmation": "Are you sure?",
@ -65,6 +69,7 @@
"settings_variable_desc_lang": "Server language",
"settings_variable_desc_fleet_enabled": "Enable fleet screen management view",
"settings_variable_desc_external_url": "External url (i.e: https://screen-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Slide upload limit (in bytes, 32*1024*1024 for 32MB)",
"settings_variable_desc_slide_animation_enabled": "Enable animation effect between slides",
"settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect",

View File

@ -26,6 +26,10 @@
"slideshow_slide_form_label_duration": "Durée",
"slideshow_slide_form_label_duration_unit": "secondes",
"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_button_cancel": "Annuler",
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
@ -65,6 +69,7 @@
"settings_variable_desc_lang": "Langage de l'application",
"settings_variable_desc_fleet_enabled": "Activer la gestion de flotte des écrans",
"settings_variable_desc_external_url": "URL externe (i.e: https://screen-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Limite d'upload du fichier d'une slide (en octets, 32*1024*1024 pour 32Mo)",
"settings_variable_desc_slide_animation_enabled": "Activer les effets d'animation entre les slides",
"settings_variable_desc_slide_animation_entrance_effect": "Effet d'animation d'arrivée de la slide",

View File

@ -2,4 +2,4 @@ flask==2.3.3
pysondb-v2==2.1.0
python-dotenv
cron-descriptor
waitress

View File

@ -5,6 +5,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import Optional, List, Dict, Union
from src.model.entity.Variable import Variable
from src.model.enum.VariableType import VariableType
from src.model.enum.VariableUnit import VariableUnit
from src.model.enum.HookType import HookType
from src.model.hook.HookRegistration import HookRegistration
from src.model.hook.StaticHookRegistration import StaticHookRegistration
@ -55,13 +56,14 @@ class ObPlugin(abc.ABC):
def get_plugin_variable_name(self, name: str) -> str:
return "{}_{}".format(self.get_plugin_variable_prefix(), name)
def add_variable(self, name: str, value='', type: VariableType = VariableType.STRING, editable: bool = True, description: str = '', selectables: Optional[Dict[str, str]] = None) -> Variable:
def add_variable(self, name: str, value='', type: VariableType = VariableType.STRING, editable: bool = True, description: str = '', selectables: Optional[Dict[str, str]] = None, unit: Optional[VariableUnit] = None) -> Variable:
return self._model_store.variable().set_variable(
name=self.get_plugin_variable_name(name),
value=value,
type=type,
editable=editable,
description=description,
unit=unit,
selectables=selectables if isinstance(selectables, dict) else None,
plugin=self.use_id(),
)

View File

@ -0,0 +1,33 @@
import json
import sys
from pysondb import PysonDB
from typing import Optional
class DatabaseManager:
DB_DIR = 'data/db'
def __init__(self):
pass
def open(self, table_name: str, table_model: list) -> PysonDB:
db_file = "{}/{}.json".format(self.DB_DIR, table_name)
db = PysonDB(db_file)
db = self._update_model(db_file, table_model)
return db
@staticmethod
def _update_model(db_file: str, table_model: list) -> Optional[PysonDB]:
try:
with open(db_file, 'r') as file:
db_model = file.read()
db_model = json.loads(db_model)
db_model['keys'] = table_model
with open(db_file, 'w') as file:
file.write(json.dumps(db_model, indent=4))
return PysonDB(db_file)
except FileNotFoundError:
logging.error("Database file {} not found".format(db_file))
return None

View File

@ -1,15 +1,24 @@
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Screen import Screen
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Screen import Screen
from src.manager.DatabaseManager import DatabaseManager
class ScreenManager:
DB_FILE = "data/db/fleet.json"
TABLE_NAME = "fleet"
TABLE_MODEL = [
"name",
"enabled",
"position",
"host",
"port"
]
def __init__(self):
self._db = PysonDB(self.DB_FILE)
def __init__(self, database_manager: DatabaseManager):
self._database_manager = database_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod
def hydrate_object(raw_screen: dict, id: Optional[str] = None) -> Screen:

View File

@ -1,18 +1,29 @@
import os
from typing import Dict, Optional, List, Tuple, Union
from pysondb.errors import IdDoesNotExistError
from src.model.entity.Slide import Slide
from src.utils import str_to_enum, get_optional_string
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
from src.manager.DatabaseManager import DatabaseManager
class SlideManager:
DB_FILE = "data/db/slideshow.json"
TABLE_NAME = "slideshow"
TABLE_MODEL = [
"name",
"type",
"enabled",
"duration",
"position",
"location",
"cron_schedule"
]
def __init__(self):
self._db = PysonDB(self.DB_FILE)
def __init__(self, database_manager: DatabaseManager):
self._database_manager = database_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod
def hydrate_object(raw_slide: dict, id: str = None) -> Slide:

View File

@ -1,27 +1,41 @@
import time
from typing import Dict, Optional, List, Tuple, Union
from pysondb.errors import IdDoesNotExistError
from src.manager.DatabaseManager import DatabaseManager
from src.model.entity.Variable import Variable
from src.model.entity.Selectable import Selectable
from src.model.enum.VariableType import VariableType
from src.model.enum.VariableUnit import VariableUnit
from src.model.enum.AnimationEntranceEffect import AnimationEntranceEffect
from src.model.enum.AnimationExitEffect import AnimationExitEffect
from src.model.enum.AnimationSpeed import AnimationSpeed
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
from src.utils import get_keys
import time
from src.utils import get_keys, enum_to_str
SELECTABLE_BOOLEAN = {"1": "", "0": ""}
class VariableManager:
DB_FILE = "data/db/settings.json"
TABLE_NAME = "settings"
TABLE_MODEL = [
"description",
"editable",
"name",
"plugin",
"selectables",
"type",
"unit",
"value"
]
def __init__(self):
self._db = PysonDB(self.DB_FILE)
def __init__(self, database_manager: DatabaseManager):
self._database_manager = database_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
self._var_map = {}
self.reload()
def set_variable(self, name: str, value, type: VariableType, editable: bool, description: str, plugin: Optional[None] = None, selectables: Optional[Dict[str, str]] = None) -> Variable:
def set_variable(self, name: str, value, type: VariableType, editable: bool, description: str, plugin: Optional[None] = None, selectables: Optional[Dict[str, str]] = None, unit: Optional[VariableUnit] = None) -> Variable:
if isinstance(value, bool) and value:
value = '1'
elif isinstance(value, bool) and not value:
@ -37,6 +51,7 @@ class VariableManager:
"editable": editable,
"description": description,
"plugin": plugin,
"unit": unit.value if unit else None,
"selectables": ([{"key": key, "label": label} for key, label in selectables.items()]) if isinstance(selectables, dict) else None
}
variable = self.get_one_by_name(default_var['name'])
@ -50,6 +65,9 @@ class VariableManager:
if variable.description != default_var['description']:
self._db.update_by_id(variable.id, {"description": default_var['description']})
if variable.unit != default_var['unit']:
self._db.update_by_id(variable.id, {"unit": default_var['unit']})
if not same_selectables:
self._db.update_by_id(variable.id, {"selectables": default_var['selectables']})
@ -63,6 +81,7 @@ class VariableManager:
{"name": "lang", "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": lang_map['settings_variable_desc_lang'] if lang_map else "", "selectables": {"en": "English", "fr": "French"}},
{"name": "fleet_enabled", "value": False, "type": VariableType.BOOL, "editable": True, "description": lang_map['settings_variable_desc_fleet_enabled'] if lang_map else ""},
{"name": "external_url", "value": "", "type": VariableType.STRING, "editable": True, "description": lang_map['settings_variable_desc_external_url'] if lang_map else ""},
{"name": "slide_upload_limit", "value": 32 * 1024 * 1024, "unit": VariableUnit.BYTE, "type": VariableType.INT, "editable": True, "description": lang_map['settings_variable_desc_slide_upload_limit'] if lang_map else ""},
{"name": "slide_animation_enabled", "value": False, "type": VariableType.BOOL, "editable": True, "description": lang_map['settings_variable_desc_slide_animation_enabled'] if lang_map else ""},
{"name": "slide_animation_entrance_effect", "value": AnimationEntranceEffect.FADE_IN.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": lang_map['settings_variable_desc_slide_animation_entrance_effect'] if lang_map else "", "selectables": AnimationEntranceEffect.get_values()},
{"name": "slide_animation_exit_effect", "value": AnimationExitEffect.NONE.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": lang_map['settings_variable_desc_slide_animation_exit_effect'] if lang_map else "", "selectables": AnimationExitEffect.get_values()},

View File

@ -3,6 +3,7 @@ import time
from typing import Optional, Union, Dict, List
from src.model.enum.VariableType import VariableType
from src.model.enum.VariableUnit import VariableUnit
from src.model.entity.Selectable import Selectable
from src.utils import str_to_enum
@ -11,10 +12,11 @@ class Variable:
def __init__(self, name: str = '', description: str = '', type: Union[VariableType, str] = VariableType.STRING,
value: Union[int, bool, str] = '', editable: bool = True, id: Optional[str] = None,
plugin: Optional[str] = None, selectables: Optional[List[Selectable]] = None):
plugin: Optional[str] = None, selectables: Optional[List[Selectable]] = None, unit: Optional[VariableUnit] = None):
self._id = id if id else None
self._name = name
self._type = str_to_enum(type, VariableType) if isinstance(type, str) else type
self._unit = str_to_enum(unit, VariableUnit) if isinstance(unit, str) else unit
self._description = description
self._value = value
self._editable = editable
@ -52,6 +54,14 @@ class Variable:
def type(self, value: VariableType):
self._type = value
@property
def unit(self) -> VariableUnit:
return self._unit
@unit.setter
def unit(self, value: VariableUnit):
self._unit = value
@property
def description(self) -> str:
return self._description
@ -90,6 +100,7 @@ class Variable:
f"name='{self.name}',\n" \
f"value='{self.value}',\n" \
f"type='{self.type}',\n" \
f"unit='{self.unit}',\n" \
f"description='{self.description}',\n" \
f"editable='{self.editable}',\n" \
f"plugin='{self.plugin}',\n" \
@ -105,6 +116,7 @@ class Variable:
"name": self.name,
"value": self.value,
"type": self.type.value,
"unit": self.unit.value if self.unit else None,
"description": self.description,
"editable": self.editable,
"plugin": self.plugin,
@ -127,9 +139,17 @@ class Variable:
value = self.eval()
if self.type == VariableType.SELECT_SINGLE:
for selectable in self.selectables:
if selectable.key == value:
return str(selectable.label)
if isinstance(self._selectables, list):
for selectable in self.selectables:
if selectable.key == value:
value = str(selectable.label)
break
if self.unit == VariableUnit.BYTE:
value = "{} {}".format(
value / 1024 / 1024,
"MB"
)
return value

View File

@ -0,0 +1,6 @@
from enum import Enum
class VariableUnit(Enum):
BYTE = 'byte'

View File

@ -2,6 +2,7 @@ from src.manager.SlideManager import SlideManager
from src.manager.ScreenManager import ScreenManager
from src.manager.VariableManager import VariableManager
from src.manager.LangManager import LangManager
from src.manager.DatabaseManager import DatabaseManager
from src.manager.ConfigManager import ConfigManager
from src.manager.LoggingManager import LoggingManager
@ -9,11 +10,12 @@ from src.manager.LoggingManager import LoggingManager
class ModelStore:
def __init__(self):
self._variable_manager = VariableManager()
self._database_manager = DatabaseManager()
self._variable_manager = VariableManager(database_manager=self._database_manager)
self._config_manager = ConfigManager(variable_manager=self._variable_manager)
self._logging_manager = LoggingManager(config_manager=self._config_manager)
self._screen_manager = ScreenManager()
self._slide_manager = SlideManager()
self._screen_manager = ScreenManager(database_manager=self._database_manager)
self._slide_manager = SlideManager(database_manager=self._database_manager)
self._lang_manager = LangManager(lang=self.variable().map().get('lang').as_string())
self._variable_manager.reload(lang_map=self._lang_manager.map())
@ -26,6 +28,9 @@ class ModelStore:
def variable(self) -> VariableManager:
return self._variable_manager
def database(self) -> DatabaseManager:
return self._database_manager
def slide(self) -> SlideManager:
return self._slide_manager

View File

@ -9,7 +9,7 @@ from src.model.hook.HookRegistration import HookRegistration
from src.model.hook.StaticHookRegistration import StaticHookRegistration
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
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:
@ -29,7 +29,8 @@ class TemplateRenderer:
VERSION=self._model_store.config().map().get('version'),
LANG=self._model_store.variable().map().get('lang').as_string(),
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:

View File

@ -1,5 +1,6 @@
import os
import time
from waitress import serve
from flask import Flask, send_from_directory
from src.service.ModelStore import ModelStore
@ -14,8 +15,6 @@ from src.constant.WebDirConstant import WebDirConstant
class WebServer:
MAX_UPLOADS = 16 * 1024 * 1024
def __init__(self, project_dir: str, model_store: ModelStore, template_renderer: TemplateRenderer):
self._project_dir = project_dir
self._model_store = model_store
@ -24,10 +23,10 @@ class WebServer:
self.setup()
def run(self) -> None:
self._app.run(
serve(
self._app,
host=self._model_store.config().map().get('bind'),
port=self._model_store.config().map().get('port'),
debug=self._debug
port=self._model_store.config().map().get('port')
)
def setup(self) -> None:
@ -53,7 +52,7 @@ class WebServer:
)
self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS)
self._app.config['MAX_CONTENT_LENGTH'] = self.MAX_UPLOADS
self._app.config['MAX_CONTENT_LENGTH'] = self._model_store.variable().map().get('slide_upload_limit').as_int()
if self._debug:
self._app.config['TEMPLATES_AUTO_RELOAD'] = True

View File

@ -8,7 +8,22 @@ from cron_descriptor import ExpressionDescriptor
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:
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 = {
"expression": expression,
"use_24hour_time_format": use_24hour_time_format
@ -60,6 +75,14 @@ def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') ->
return None
def enum_to_str(enum: Optional[Enum]) -> Optional[str]:
if enum:
print(enum)
return str(enum.value)
return None
def str_to_enum(str_val: str, enum_class) -> Enum:
for enum_item in enum_class:
if enum_item.value == str_val:

View File

@ -1 +1 @@
1.6
1.7

View File

@ -6,6 +6,7 @@
</title>
<meta name="robots" content="noindex, nofollow">
<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="60x60" href="{{ STATIC_PREFIX }}favicon/apple-icon-60x60.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>
<meta name="robots" content="noindex, nofollow">
<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>
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; }

View File

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

View File

@ -5,10 +5,12 @@
{% endblock %}
{% block add_css %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/flatpickr.min.css" />
{{ HOOK(H_SLIDESHOW_CSS) }}
{% endblock %}
{% 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/slideshow.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>
<div class="widget">
<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"
class="slide-add-object-input hidden" disabled="disabled" />
<input type="file" name="object" id="slide-add-object-input-upload" class="slide-add-object-input hidden" disabled="disabled" />
</div>
</div>
@ -41,8 +40,13 @@
<div class="form-group">
<label for="slide-add-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
<div class="widget">
<input type="checkbox" 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" />
<select id="slide-add-cron-schedule-trigger" class="trigger">
<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>

View File

@ -41,8 +41,13 @@
<div class="form-group">
<label for="slide-edit-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
<div class="widget">
<input type="checkbox" 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" />
<select id="slide-edit-cron-schedule-trigger" class="trigger">
<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>