diff --git a/data/www/css/main.css b/data/www/css/main.css index fbac64c..5a2bc09 100644 --- a/data/www/css/main.css +++ b/data/www/css/main.css @@ -189,6 +189,24 @@ button.purple:hover { border: 1px solid #fff; } +.alert { + margin: 10px 30px 20px 30px; + padding: 20px 50px; + flex: 1; + align-self: stretch; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.alert-success { + color: rgb(14, 239, 95); + border: 1px solid rgb(14, 239, 95); + background: rgba(14, 239, 95, .2); + border-radius: 4px; +} + .panel { background: rgba(255, 255, 255, 0.15); @@ -548,7 +566,7 @@ form .actions button { form .actions button.green:hover { background: white; color: rgb(14, 239, 95); - border-color: rgb(14, 239, 95) + border-color: rgb(14, 239, 95); } form .actions button.modal-close { diff --git a/data/www/js/settings.js b/data/www/js/settings.js index 2359534..00a83d8 100644 --- a/data/www/js/settings.js +++ b/data/www/js/settings.js @@ -29,7 +29,8 @@ jQuery(document).ready(function ($) { }); $('#variable-edit-value').replaceWith($select); } else { - $('#variable-edit-value').replaceWith(''); + var type = variable.type === 'int' ? 'number' : 'text'; + $('#variable-edit-value').replaceWith(''); } showModal('modal-variable-edit'); diff --git a/lang/en.json b/lang/en.json index b9ee7fa..175eeba 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,6 +1,8 @@ { "slideshow_page_title": "Schedule Overview", "slideshow_goto_player": "Go to player", + "slideshow_refresh_player": "Refresh player", + "slideshow_refresh_player_success": "A player refresh has been schedueld, it should happen soon enough (%time% seconds maximum)", "slideshow_slide_button_add": "Add a slide", "slideshow_slide_panel_active": "Active slides", "slideshow_slide_panel_inactive": "Inactive slides", @@ -71,6 +73,8 @@ "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_default_slide_duration": "Intro slide duration (in seconds)", + "settings_variable_desc_polling_interval": "Refresh interval applied for settings to the player (in seconds)", "settings_variable_desc_slide_animation_enabled": "Enable animation effect between slides", "settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect", @@ -79,6 +83,7 @@ "settings_variable_desc_ro_editable": "Last application reboot datetime", "settings_variable_desc_ro_last_slide_update": "Last slide update datetime", + "settings_variable_desc_ro_refresh_player_request": "Last player refresh request datetime", "sysinfo_page_title": "System infos", "sysinfo_panel_button_restart": "Restart", @@ -120,5 +125,6 @@ "enum_animation_speed_fast": "Fast", "enum_animation_speed_faster": "Faster", "enum_variable_section_general": "General", - "enum_variable_section_animation": "Animation" + "enum_variable_section_player_animation": "Player animation", + "enum_variable_section_player_options": "Options du lecteur" } diff --git a/lang/fr.json b/lang/fr.json index 3a9f038..5372a14 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1,6 +1,8 @@ { "slideshow_page_title": "Vue Planning", "slideshow_goto_player": "Voir le lecteur", + "slideshow_refresh_player": "Rafraîchir le lecteur", + "slideshow_refresh_player_success": "Un rafraîchissement du lecteur a été programmé, il devrait avoir lieu sous peu (%time% secondes maximum)", "slideshow_slide_button_add": "Ajouter une slide", "slideshow_slide_panel_active": "Slides actives", "slideshow_slide_panel_inactive": "Slides inactives", @@ -71,6 +73,8 @@ "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_default_slide_duration": "Durée de la slide d'introduction (en secondes)", + "settings_variable_desc_polling_interval": "Intervalle de rafraîchissement des paramètres à appliquer au lecteur (en secondes)", "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", @@ -79,6 +83,7 @@ "settings_variable_desc_ro_editable": "Date de dernier redémarrage de l'application", "settings_variable_desc_ro_last_slide_update": "Date de dernière modification d'une slide", + "settings_variable_desc_ro_refresh_player_request": "Date de dernière demande de rafraîchissement du lecteur", "sysinfo_page_title": "Système", "sysinfo_panel_button_restart": "Redémarrer", @@ -120,5 +125,6 @@ "enum_animation_speed_fast": "Rapide", "enum_animation_speed_faster": "Très rapide", "enum_variable_section_general": "Général", - "enum_variable_section_animation": "Animation" + "enum_variable_section_player_animation": "Animation du lecteur", + "enum_variable_section_player_options": "Options du lecteur" } diff --git a/src/controller/PlayerController.py b/src/controller/PlayerController.py index 1f2a2ce..e8e5479 100644 --- a/src/controller/PlayerController.py +++ b/src/controller/PlayerController.py @@ -24,7 +24,8 @@ class PlayerController(ObController): playlists = { 'loop': playlist_loop, - 'cron': playlist_cron + 'cron': playlist_cron, + 'hard_refresh_request': self._model_store.variable().get_one_by_name("refresh_player_request").as_int() } return playlists @@ -38,6 +39,8 @@ class PlayerController(ObController): return render_template( 'player/player.jinja.html', items=json.dumps(self._get_playlist()), + default_slide_duration=self._model_store.variable().get_one_by_name('default_slide_duration'), + polling_interval=self._model_store.variable().get_one_by_name('polling_interval'), slide_animation_enabled=self._model_store.variable().get_one_by_name('slide_animation_enabled'), slide_animation_entrance_effect=self._model_store.variable().get_one_by_name('slide_animation_entrance_effect'), slide_animation_exit_effect=self._model_store.variable().get_one_by_name('slide_animation_exit_effect'), diff --git a/src/controller/SettingsController.py b/src/controller/SettingsController.py index 8de049a..f81fa85 100644 --- a/src/controller/SettingsController.py +++ b/src/controller/SettingsController.py @@ -1,3 +1,4 @@ +import time import json from flask import Flask, render_template, redirect, request, url_for @@ -21,4 +22,12 @@ class SettingsController(ObController): def settings_variable_edit(self): self._model_store.variable().update_form(request.form['id'], request.form['value']) - return redirect(url_for('settings_variable_list')) \ No newline at end of file + self._post_update(request.form['id']) + return redirect(url_for('settings_variable_list')) + + def _post_update(self, id: str): + variable = self._model_store.variable().get(id) + + if variable.refresh_player: + self._model_store.variable().update_by_name("refresh_player_request", time.time()) + diff --git a/src/controller/SlideshowController.py b/src/controller/SlideshowController.py index 5c82f2a..10cc477 100644 --- a/src/controller/SlideshowController.py +++ b/src/controller/SlideshowController.py @@ -21,6 +21,7 @@ class SlideshowController(ObController): self._app.add_url_rule('/slideshow/slide/toggle', 'slideshow_slide_toggle', self.slideshow_slide_toggle, methods=['POST']) self._app.add_url_rule('/slideshow/slide/delete', 'slideshow_slide_delete', self.slideshow_slide_delete, methods=['DELETE']) self._app.add_url_rule('/slideshow/slide/position', 'slideshow_slide_position', self.slideshow_slide_position, methods=['POST']) + self._app.add_url_rule('/slideshow/player-refresh', 'slideshow_player_refresh', self.slideshow_player_refresh, methods=['GET']) def manage(self): return redirect(url_for('slideshow_slide_list')) @@ -88,6 +89,15 @@ class SlideshowController(ObController): self._post_update() return jsonify({'status': 'ok'}) + def slideshow_player_refresh(self): + self._model_store.variable().update_by_name("refresh_player_request", time.time()) + return redirect( + url_for( + 'slideshow_slide_list', + refresh_player=self._model_store.variable().get_one_by_name('polling_interval').as_int() + ) + ) + def _post_update(self): self._model_store.variable().update_by_name("last_slide_update", time.time()) diff --git a/src/interface/ObPlugin.py b/src/interface/ObPlugin.py index f377e60..651e1fc 100644 --- a/src/interface/ObPlugin.py +++ b/src/interface/ObPlugin.py @@ -56,7 +56,7 @@ 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='', section: str = '', type: VariableType = VariableType.STRING, editable: bool = True, description: str = '', selectables: Optional[Dict[str, str]] = None, unit: Optional[VariableUnit] = None) -> Variable: + def add_variable(self, name: str, value='', section: str = '', type: VariableType = VariableType.STRING, editable: bool = True, description: str = '', selectables: Optional[Dict[str, str]] = None, unit: Optional[VariableUnit] = None, refresh_player: bool = False) -> Variable: return self._model_store.variable().set_variable( name=self.get_plugin_variable_name(name), section=section, @@ -65,6 +65,7 @@ class ObPlugin(abc.ABC): editable=editable, description=description, unit=unit, + refresh_player=refresh_player, selectables=selectables if isinstance(selectables, dict) else None, plugin=self.use_id(), ) diff --git a/src/manager/VariableManager.py b/src/manager/VariableManager.py index 2335c30..8b06b2c 100644 --- a/src/manager/VariableManager.py +++ b/src/manager/VariableManager.py @@ -30,6 +30,7 @@ class VariableManager(ModelManager): "selectables", "type", "unit", + "refresh_player", "value" ] @@ -39,7 +40,7 @@ class VariableManager(ModelManager): 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, unit: Optional[VariableUnit] = None, section: str = '') -> 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, section: str = '', refresh_player: bool = False) -> Variable: if isinstance(value, bool) and value: value = '1' elif isinstance(value, bool) and not value: @@ -54,6 +55,7 @@ class VariableManager(ModelManager): "value": value, "type": type.value, "editable": editable, + "refresh_player": refresh_player, "description": description, "plugin": plugin, "unit": unit.value if unit else None, @@ -76,6 +78,9 @@ class VariableManager(ModelManager): if variable.section != default_var['section']: self._db.update_by_id(variable.id, {"section": default_var['section']}) + if variable.refresh_player != default_var['refresh_player']: + self._db.update_by_id(variable.id, {"refresh_player": default_var['refresh_player']}) + if not same_selectables: self._db.update_by_id(variable.id, {"selectables": default_var['selectables']}) @@ -87,18 +92,27 @@ class VariableManager(ModelManager): def reload(self) -> None: default_vars = [ # Editable (Customizable settings) - {"name": "lang", "section": self.t(VariableSection.GENERAL), "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_lang'), "selectables": {"en": "English", "fr": "French"}}, - {"name": "fleet_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_fleet_enabled')}, - {"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": True, "description": self.t('settings_variable_desc_external_url')}, - {"name": "slide_upload_limit", "section": self.t(VariableSection.ANIMATION), "value": 32 * 1024 * 1024, "unit": VariableUnit.BYTE, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_slide_upload_limit')}, - {"name": "slide_animation_enabled", "section": self.t(VariableSection.ANIMATION), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_slide_animation_enabled')}, - {"name": "slide_animation_entrance_effect", "section": self.t(VariableSection.ANIMATION), "value": AnimationEntranceEffect.FADE_IN.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_slide_animation_entrance_effect'), "selectables": enum_to_dict(AnimationEntranceEffect)}, - {"name": "slide_animation_exit_effect", "section": self.t(VariableSection.ANIMATION), "value": AnimationExitEffect.NONE.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_slide_animation_exit_effect'), "selectables": enum_to_dict(AnimationExitEffect)}, - {"name": "slide_animation_speed", "section": self.t(VariableSection.ANIMATION), "value": AnimationSpeed.NORMAL.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_slide_animation_speed'), "selectables": self.t(AnimationSpeed)}, + + ### General + {"name": "lang", "section": self.t(VariableSection.GENERAL), "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_lang'), "selectables": {"en": "English", "fr": "French"}, "refresh_player": False}, + {"name": "fleet_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_fleet_enabled'), "refresh_player": False}, + {"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False}, + {"name": "slide_upload_limit", "section": self.t(VariableSection.GENERAL), "value": 32 * 1024 * 1024, "unit": VariableUnit.BYTE, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False}, + + ### Player Options + {"name": "default_slide_duration", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 3, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_default_slide_duration'), "refresh_player": False}, + {"name": "polling_interval", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 5, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_polling_interval'), "refresh_player": True}, + + ### Player Animation + {"name": "slide_animation_enabled", "section": self.t(VariableSection.PLAYER_ANIMATION), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_slide_animation_enabled'), "refresh_player": True}, + {"name": "slide_animation_entrance_effect", "section": self.t(VariableSection.PLAYER_ANIMATION), "value": AnimationEntranceEffect.FADE_IN.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_slide_animation_entrance_effect'), "selectables": enum_to_dict(AnimationEntranceEffect), "refresh_player": True}, + {"name": "slide_animation_exit_effect", "section": self.t(VariableSection.PLAYER_ANIMATION), "value": AnimationExitEffect.NONE.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_slide_animation_exit_effect'), "selectables": enum_to_dict(AnimationExitEffect), "refresh_player": True}, + {"name": "slide_animation_speed", "section": self.t(VariableSection.PLAYER_ANIMATION), "value": AnimationSpeed.NORMAL.value, "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_slide_animation_speed'), "selectables": self.t(AnimationSpeed), "refresh_player": True}, # Not editable (System information) {"name": "last_restart", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_editable')}, {"name": "last_slide_update", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_last_slide_update')}, + {"name": "refresh_player_request", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_refresh_player_request')}, ] for default_var in default_vars: diff --git a/src/model/entity/Variable.py b/src/model/entity/Variable.py index 98f5b34..44944ab 100644 --- a/src/model/entity/Variable.py +++ b/src/model/entity/Variable.py @@ -12,7 +12,8 @@ class Variable: def __init__(self, name: str = '', section: 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, unit: Optional[VariableUnit] = None): + plugin: Optional[str] = None, selectables: Optional[List[Selectable]] = None, unit: Optional[VariableUnit] = None, + refresh_player: bool = False): self._id = id if id else None self._name = name self._section = section @@ -22,6 +23,7 @@ class Variable: self._value = value self._editable = editable self._plugin = plugin + self._refresh_player = refresh_player self._selectables = selectables @property @@ -87,6 +89,14 @@ class Variable: def editable(self, value: bool): self._editable = value + @property + def refresh_player(self) -> bool: + return self._refresh_player + + @refresh_player.setter + def refresh_player(self, value: bool): + self._refresh_player = value + @property def value(self) -> Union[int, bool, str]: return self._value @@ -113,6 +123,7 @@ class Variable: f"unit='{self.unit}',\n" \ f"description='{self.description}',\n" \ f"editable='{self.editable}',\n" \ + f"refresh_player='{self.refresh_player}',\n" \ f"plugin='{self.plugin}',\n" \ f"selectables='{self.selectables}',\n" \ f")" @@ -130,6 +141,7 @@ class Variable: "unit": self.unit.value if self.unit else None, "description": self.description, "editable": self.editable, + "refresh_player": self.refresh_player, "plugin": self.plugin, "selectables": [selectable.to_dict() for selectable in self.selectables] if isinstance(self._selectables, list) else None } @@ -161,6 +173,8 @@ class Variable: value / 1024 / 1024, "MB" ) + elif self.unit == VariableUnit.SECOND: + value = "{}{}".format(value, "s") return value diff --git a/src/model/enum/VariableSection.py b/src/model/enum/VariableSection.py index 38282f7..cb09cd1 100644 --- a/src/model/enum/VariableSection.py +++ b/src/model/enum/VariableSection.py @@ -4,4 +4,5 @@ from enum import Enum class VariableSection(Enum): GENERAL = 'general' - ANIMATION = 'animation' + PLAYER_OPTIONS = 'player_options' + PLAYER_ANIMATION = 'player_animation' diff --git a/src/model/enum/VariableUnit.py b/src/model/enum/VariableUnit.py index f20c1d6..6455b51 100644 --- a/src/model/enum/VariableUnit.py +++ b/src/model/enum/VariableUnit.py @@ -4,3 +4,4 @@ from enum import Enum class VariableUnit(Enum): BYTE = 'byte' + SECOND = 'second' diff --git a/views/player/player.jinja.html b/views/player/player.jinja.html index 735cd12..730e009 100755 --- a/views/player/player.jinja.html +++ b/views/player/player.jinja.html @@ -19,16 +19,21 @@