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; 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?",
@ -65,6 +69,7 @@
"settings_variable_desc_lang": "Server language", "settings_variable_desc_lang": "Server language",
"settings_variable_desc_fleet_enabled": "Enable fleet screen management view", "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_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_enabled": "Enable animation effect between slides",
"settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect", "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": "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 ?",
@ -65,6 +69,7 @@
"settings_variable_desc_lang": "Langage de l'application", "settings_variable_desc_lang": "Langage de l'application",
"settings_variable_desc_fleet_enabled": "Activer la gestion de flotte des écrans", "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_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_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", "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 pysondb-v2==2.1.0
python-dotenv python-dotenv
cron-descriptor cron-descriptor
waitress

View File

@ -5,6 +5,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import Optional, List, Dict, Union from typing import Optional, List, Dict, Union
from src.model.entity.Variable import Variable from src.model.entity.Variable import Variable
from src.model.enum.VariableType import VariableType from src.model.enum.VariableType import VariableType
from src.model.enum.VariableUnit import VariableUnit
from src.model.enum.HookType import HookType from src.model.enum.HookType import HookType
from src.model.hook.HookRegistration import HookRegistration from src.model.hook.HookRegistration import HookRegistration
from src.model.hook.StaticHookRegistration import StaticHookRegistration from src.model.hook.StaticHookRegistration import StaticHookRegistration
@ -55,13 +56,14 @@ class ObPlugin(abc.ABC):
def get_plugin_variable_name(self, name: str) -> str: def get_plugin_variable_name(self, name: str) -> str:
return "{}_{}".format(self.get_plugin_variable_prefix(), name) 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( return self._model_store.variable().set_variable(
name=self.get_plugin_variable_name(name), name=self.get_plugin_variable_name(name),
value=value, value=value,
type=type, type=type,
editable=editable, editable=editable,
description=description, description=description,
unit=unit,
selectables=selectables if isinstance(selectables, dict) else None, selectables=selectables if isinstance(selectables, dict) else None,
plugin=self.use_id(), 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 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: class ScreenManager:
DB_FILE = "data/db/fleet.json" TABLE_NAME = "fleet"
TABLE_MODEL = [
"name",
"enabled",
"position",
"host",
"port"
]
def __init__(self): def __init__(self, database_manager: DatabaseManager):
self._db = PysonDB(self.DB_FILE) self._database_manager = database_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod @staticmethod
def hydrate_object(raw_screen: dict, id: Optional[str] = None) -> Screen: def hydrate_object(raw_screen: dict, id: Optional[str] = None) -> Screen:

View File

@ -1,18 +1,29 @@
import os import os
from typing import Dict, Optional, List, Tuple, Union from typing import Dict, Optional, List, Tuple, Union
from pysondb.errors import IdDoesNotExistError
from src.model.entity.Slide import Slide from src.model.entity.Slide import Slide
from src.utils import str_to_enum, get_optional_string from src.utils import str_to_enum, get_optional_string
from pysondb import PysonDB from src.manager.DatabaseManager import DatabaseManager
from pysondb.errors import IdDoesNotExistError
class SlideManager: class SlideManager:
DB_FILE = "data/db/slideshow.json" TABLE_NAME = "slideshow"
TABLE_MODEL = [
"name",
"type",
"enabled",
"duration",
"position",
"location",
"cron_schedule"
]
def __init__(self): def __init__(self, database_manager: DatabaseManager):
self._db = PysonDB(self.DB_FILE) self._database_manager = database_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@staticmethod @staticmethod
def hydrate_object(raw_slide: dict, id: str = None) -> Slide: 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 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.Variable import Variable
from src.model.entity.Selectable import Selectable from src.model.entity.Selectable import Selectable
from src.model.enum.VariableType import VariableType 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.AnimationEntranceEffect import AnimationEntranceEffect
from src.model.enum.AnimationExitEffect import AnimationExitEffect from src.model.enum.AnimationExitEffect import AnimationExitEffect
from src.model.enum.AnimationSpeed import AnimationSpeed from src.model.enum.AnimationSpeed import AnimationSpeed
from pysondb import PysonDB from src.utils import get_keys, enum_to_str
from pysondb.errors import IdDoesNotExistError
from src.utils import get_keys
import time
SELECTABLE_BOOLEAN = {"1": "", "0": ""} SELECTABLE_BOOLEAN = {"1": "", "0": ""}
class VariableManager: class VariableManager:
DB_FILE = "data/db/settings.json" TABLE_NAME = "settings"
TABLE_MODEL = [
"description",
"editable",
"name",
"plugin",
"selectables",
"type",
"unit",
"value"
]
def __init__(self): def __init__(self, database_manager: DatabaseManager):
self._db = PysonDB(self.DB_FILE) self._database_manager = database_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
self._var_map = {} self._var_map = {}
self.reload() 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: if isinstance(value, bool) and value:
value = '1' value = '1'
elif isinstance(value, bool) and not value: elif isinstance(value, bool) and not value:
@ -37,6 +51,7 @@ class VariableManager:
"editable": editable, "editable": editable,
"description": description, "description": description,
"plugin": plugin, "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 "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']) variable = self.get_one_by_name(default_var['name'])
@ -50,6 +65,9 @@ class VariableManager:
if variable.description != default_var['description']: if variable.description != default_var['description']:
self._db.update_by_id(variable.id, {"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: if not same_selectables:
self._db.update_by_id(variable.id, {"selectables": default_var['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": "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": "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": "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_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_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()}, {"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 typing import Optional, Union, Dict, List
from src.model.enum.VariableType import VariableType from src.model.enum.VariableType import VariableType
from src.model.enum.VariableUnit import VariableUnit
from src.model.entity.Selectable import Selectable from src.model.entity.Selectable import Selectable
from src.utils import str_to_enum 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, 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, 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._id = id if id else None
self._name = name self._name = name
self._type = str_to_enum(type, VariableType) if isinstance(type, str) else type 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._description = description
self._value = value self._value = value
self._editable = editable self._editable = editable
@ -52,6 +54,14 @@ class Variable:
def type(self, value: VariableType): def type(self, value: VariableType):
self._type = value self._type = value
@property
def unit(self) -> VariableUnit:
return self._unit
@unit.setter
def unit(self, value: VariableUnit):
self._unit = value
@property @property
def description(self) -> str: def description(self) -> str:
return self._description return self._description
@ -90,6 +100,7 @@ class Variable:
f"name='{self.name}',\n" \ f"name='{self.name}',\n" \
f"value='{self.value}',\n" \ f"value='{self.value}',\n" \
f"type='{self.type}',\n" \ f"type='{self.type}',\n" \
f"unit='{self.unit}',\n" \
f"description='{self.description}',\n" \ f"description='{self.description}',\n" \
f"editable='{self.editable}',\n" \ f"editable='{self.editable}',\n" \
f"plugin='{self.plugin}',\n" \ f"plugin='{self.plugin}',\n" \
@ -105,6 +116,7 @@ class Variable:
"name": self.name, "name": self.name,
"value": self.value, "value": self.value,
"type": self.type.value, "type": self.type.value,
"unit": self.unit.value if self.unit else None,
"description": self.description, "description": self.description,
"editable": self.editable, "editable": self.editable,
"plugin": self.plugin, "plugin": self.plugin,
@ -127,9 +139,17 @@ class Variable:
value = self.eval() value = self.eval()
if self.type == VariableType.SELECT_SINGLE: if self.type == VariableType.SELECT_SINGLE:
if isinstance(self._selectables, list):
for selectable in self.selectables: for selectable in self.selectables:
if selectable.key == value: if selectable.key == value:
return str(selectable.label) value = str(selectable.label)
break
if self.unit == VariableUnit.BYTE:
value = "{} {}".format(
value / 1024 / 1024,
"MB"
)
return value 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.ScreenManager import ScreenManager
from src.manager.VariableManager import VariableManager from src.manager.VariableManager import VariableManager
from src.manager.LangManager import LangManager from src.manager.LangManager import LangManager
from src.manager.DatabaseManager import DatabaseManager
from src.manager.ConfigManager import ConfigManager from src.manager.ConfigManager import ConfigManager
from src.manager.LoggingManager import LoggingManager from src.manager.LoggingManager import LoggingManager
@ -9,11 +10,12 @@ from src.manager.LoggingManager import LoggingManager
class ModelStore: class ModelStore:
def __init__(self): 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._config_manager = ConfigManager(variable_manager=self._variable_manager)
self._logging_manager = LoggingManager(config_manager=self._config_manager) self._logging_manager = LoggingManager(config_manager=self._config_manager)
self._screen_manager = ScreenManager() self._screen_manager = ScreenManager(database_manager=self._database_manager)
self._slide_manager = SlideManager() self._slide_manager = SlideManager(database_manager=self._database_manager)
self._lang_manager = LangManager(lang=self.variable().map().get('lang').as_string()) self._lang_manager = LangManager(lang=self.variable().map().get('lang').as_string())
self._variable_manager.reload(lang_map=self._lang_manager.map()) self._variable_manager.reload(lang_map=self._lang_manager.map())
@ -26,6 +28,9 @@ class ModelStore:
def variable(self) -> VariableManager: def variable(self) -> VariableManager:
return self._variable_manager return self._variable_manager
def database(self) -> DatabaseManager:
return self._database_manager
def slide(self) -> SlideManager: def slide(self) -> SlideManager:
return self._slide_manager 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.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

@ -1,5 +1,6 @@
import os import os
import time import time
from waitress import serve
from flask import Flask, send_from_directory from flask import Flask, send_from_directory
from src.service.ModelStore import ModelStore from src.service.ModelStore import ModelStore
@ -14,8 +15,6 @@ from src.constant.WebDirConstant import WebDirConstant
class WebServer: class WebServer:
MAX_UPLOADS = 16 * 1024 * 1024
def __init__(self, project_dir: str, model_store: ModelStore, template_renderer: TemplateRenderer): def __init__(self, project_dir: str, model_store: ModelStore, template_renderer: TemplateRenderer):
self._project_dir = project_dir self._project_dir = project_dir
self._model_store = model_store self._model_store = model_store
@ -24,10 +23,10 @@ class WebServer:
self.setup() self.setup()
def run(self) -> None: def run(self) -> None:
self._app.run( serve(
self._app,
host=self._model_store.config().map().get('bind'), host=self._model_store.config().map().get('bind'),
port=self._model_store.config().map().get('port'), port=self._model_store.config().map().get('port')
debug=self._debug
) )
def setup(self) -> None: 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['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: if self._debug:
self._app.config['TEMPLATES_AUTO_RELOAD'] = True 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 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
@ -60,6 +75,14 @@ def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') ->
return None 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: def str_to_enum(str_val: str, enum_class) -> Enum:
for enum_item in enum_class: for enum_item in enum_class:
if enum_item.value == str_val: if enum_item.value == str_val:

View File

@ -1 +1 @@
1.6 1.7

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="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
{% if slide_animation_enabled.eval() %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/animate.min.css" /> <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>