add config through UI

This commit is contained in:
jr-k 2024-02-27 15:03:40 +01:00
parent 51f1f55163
commit 709e065ed4
22 changed files with 458 additions and 60 deletions

View File

@ -16,6 +16,7 @@ cd obscreen && pip3 install -r requirements.txt && cp data/slideshow.json.dist d
## Configure
- Server configuration is available in `config.py` file.
- Application configuration is available in `http://localhost:5000/settings` page.
## Run
@ -62,11 +63,4 @@ sudo rm /etc/nginx/sites-enabled/default 2>/dev/null
sudo ln -s "$(pwd)/system/nginx-obscreen" /etc/nginx/sites-enabled
sudo systemctl reload nginx
```
2. Configure `nano config.py`
```js
{
// ...
"reverse_proxy_mode": True,
// ...
}
```
2. Set `reverse_proxy_mode` to `true` in settings page

View File

@ -1,8 +1,5 @@
config = {
"config": False, # Enable autoreload for html/jinja files
"port": 5000, # Application port
"debug": False, # Enable autoreload for html/jinja files
"reverse_proxy_mode": False, # True if you want to use nginx on port 80
"lang": "en", # Language for manage view "fr" or "en"
"lx_file": '/home/pi/.config/lxsession/LXDE-pi/autostart' # Path to lx autostart file,
"fleet_enabled": False # Enable fleet management view
"lx_file": '/home/pi/.config/lxsession/LXDE-pi/autostart' # Path to lx autostart file
}

38
data/www/js/settings.js Normal file
View File

@ -0,0 +1,38 @@
jQuery(document).ready(function ($) {
const $modalsRoot = $('.modals');
const showModal = function (modalClass) {
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
$modalsRoot.find('.modal').addClass('hidden');
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
};
const hideModal = function () {
$modalsRoot.addClass('hidden').find('form').trigger('reset');
};
const main = function () {
};
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.variable-edit', function () {
const variable = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-variable-edit');
$('.modal-variable-edit input:visible:eq(0)').focus().select();
$('#variable-edit-name').val(variable.name);
$('#variable-edit-value').val(variable.value);
$('#variable-edit-id').val(variable.id);
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -46,11 +46,25 @@
"fleet_screen_form_button_cancel": "Cancel",
"js_fleet_screen_delete_confirmation": "Are you sure?",
"settings_page_title": "Settings",
"settings_variable_panel": "Variables",
"settings_variable_panel_th_name": "Name",
"settings_variable_panel_th_description": "Help",
"settings_variable_panel_th_value": "Value",
"settings_variable_panel_th_activity": "Options",
"settings_variable_form_edit_title": "Edit Variable",
"settings_variable_form_edit_submit": "Save",
"settings_variable_form_label_name": "Name",
"settings_variable_form_label_value": "Value",
"settings_variable_form_button_cancel": "Cancel",
"settings_variable_help_port": "Application server port (restart needed)",
"settings_variable_help_bind": "Application server bind (restart needed)",
"settings_variable_help_lang": "Server language [fr,en] (restart needed)",
"settings_variable_help_fleet_enabled": "Enable fleet screen management view (restart needed)",
"sysinfo_page_title": "System infos",
"sysinfo_panel_title": "Infos",
"sysinfo_panel_th_attribute": "Attribute",
"sysinfo_panel_th_value": "Value",
"sysinfo_panel_td_ipaddr": "IP Address",
"settings_page_title": "Settings"
"sysinfo_panel_td_ipaddr": "IP Address"
}

View File

@ -46,11 +46,25 @@
"fleet_screen_form_button_cancel": "Annuler",
"js_fleet_screen_delete_confirmation": "Êtes-vous sûr ?",
"settings_page_title": "Paramètres",
"settings_variable_panel": "Variables",
"settings_variable_panel_th_name": "Nom",
"settings_variable_panel_th_description": "Aide",
"settings_variable_panel_th_value": "Valeur",
"settings_variable_panel_th_activity": "Options",
"settings_variable_form_edit_title": "Modification de la variable",
"settings_variable_form_edit_submit": "Enregistrer",
"settings_variable_form_label_name": "Nom",
"settings_variable_form_label_value": "Valeur",
"settings_variable_form_button_cancel": "Annuler",
"settings_variable_help_port": "Port du serveur d'application (redémarrage nécessaire)",
"settings_variable_help_bind": "Hôte d'attache du serveur d'application (redémarrage nécessaire)",
"settings_variable_help_lang": "Langage de l'application [fr,en] (redémarrage nécessaire)",
"settings_variable_help_fleet_enabled": "Activer la gestion de flotte des écrans (redémarrage nécessaire)",
"sysinfo_page_title": "Système",
"sysinfo_panel_title": "Informations",
"sysinfo_panel_th_attribute": "Attribut",
"sysinfo_panel_th_value": "Valeur",
"sysinfo_panel_td_ipaddr": "Adresse IP",
"settings_page_title": "Paramètres"
"sysinfo_panel_td_ipaddr": "Adresse IP"
}

View File

@ -8,20 +8,28 @@ import sys
from flask import Flask, send_from_directory
from config import config
from src.SlideManager import SlideManager
from src.ScreenManager import ScreenManager
from src.manager.SlideManager import SlideManager
from src.manager.ScreenManager import ScreenManager
from src.manager.VariableManager import VariableManager
from src.controller.PlayerController import PlayerController
from src.controller.SlideshowController import SlideshowController
from src.controller.FleetController import FleetController
from src.controller.SysinfoController import SysinfoController
from src.controller.SettingsController import SettingsController
from config import config
# <config>
PLAYER_URL = 'http://localhost:{}'.format(config['port'])
variable_manager = VariableManager()
vars = variable_manager.get_variable_map()
screen_manager = ScreenManager()
slide_manager = SlideManager()
with open('./lang/{}.json'.format(config['lang']), 'r') as file:
PLAYER_URL = 'http://localhost:{}'.format(vars['port'].as_int())
with open('./lang/{}.json'.format(vars['lang'].as_string()), 'r') as file:
LANGDICT = json.load(file)
variable_manager.init(LANGDICT)
# </config>
@ -73,17 +81,18 @@ if config['lx_file']:
@app.context_processor
def inject_global_vars():
return dict(
FLEET_MODE=config['fleet_enabled'],
LANG=config['lang'],
FLEET_ENABLED=vars['fleet_enabled'].as_bool(),
LANG=vars['lang'].as_string(),
STATIC_PREFIX='/data/www/'
)
PlayerController(app, LANGDICT, slide_manager)
SlideshowController(app, LANGDICT, slide_manager)
SettingsController(app, LANGDICT, variable_manager)
SysinfoController(app, LANGDICT)
if config['fleet_enabled']:
if vars['fleet_enabled'].as_bool():
FleetController(app, LANGDICT, screen_manager)
@app.errorhandler(404)
@ -94,8 +103,8 @@ def not_found(e):
if __name__ == '__main__':
app.run(
host=config['bind'] if 'bind' in config else '0.0.0.0',
port=config['port'],
host=vars['bind'].as_string(),
port=vars['port'].as_int(),
debug=config['debug']
)

View File

@ -6,9 +6,9 @@ from src.model.Screen import Screen
class FleetController:
def __init__(self, app, l, screen_manager):
def __init__(self, app, lang_dict, screen_manager):
self._app = app
self._l = l
self._lang_dict = lang_dict
self._screen_manager = screen_manager
self.register()
@ -24,14 +24,13 @@ class FleetController:
def fleet(self):
return render_template(
'fleet/fleet.jinja.html',
l=self._l,
screens=self._screen_manager.get_enabled_screens(),
)
def fleet_screen_list(self):
return render_template(
'fleet/list.jinja.html',
l=self._l,
l=self._lang_dict,
enabled_screens=self._screen_manager.get_enabled_screens(),
disabled_screens=self._screen_manager.get_disabled_screens(),
)

View File

@ -6,9 +6,9 @@ from src.utils import get_ip_address
class PlayerController:
def __init__(self, app, l, slide_manager):
def __init__(self, app, lang_dict, slide_manager):
self._app = app
self._l = l
self._lang_dict = lang_dict
self._slide_manager = slide_manager
self.register()

View File

@ -0,0 +1,27 @@
import json
from flask import Flask, render_template, redirect, request, url_for
class SettingsController:
def __init__(self, app, lang_dict, variable_manager):
self._app = app
self._lang_dict = lang_dict
self._variable_manager = variable_manager
self.register()
def register(self):
self._app.add_url_rule('/settings/variable/list', 'settings_variable_list', self.settings_variable_list, methods=['GET'])
self._app.add_url_rule('/settings/variable/edit', 'settings_variable_edit', self.settings_variable_edit, methods=['POST'])
def settings_variable_list(self):
return render_template(
'settings/list.jinja.html',
l=self._lang_dict,
variables=self._variable_manager.get_all(),
)
def settings_variable_edit(self):
self._variable_manager.update_form(request.form['id'], request.form['value'])
return redirect(url_for('settings_variable_list'))

View File

@ -10,9 +10,9 @@ from src.utils import str_to_enum
class SlideshowController:
def __init__(self, app, l, slide_manager):
def __init__(self, app, lang_dict, slide_manager):
self._app = app
self._l = l
self._lang_dict = lang_dict
self._slide_manager = slide_manager
self.register()
@ -27,7 +27,7 @@ class SlideshowController:
def slideshow(self):
return render_template(
'slideshow/list.jinja.html',
l=self._l,
l=self._lang_dict,
enabled_slides=self._slide_manager.get_enabled_slides(),
disabled_slides=self._slide_manager.get_disabled_slides(),
)

View File

@ -1,14 +1,12 @@
import json
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from flask import Flask, render_template
from src.utils import get_ip_address
class SysinfoController:
def __init__(self, app, l):
def __init__(self, app, lang_dict):
self._app = app
self._l = l
self._lang_dict = lang_dict
self.register()
def register(self):
@ -18,5 +16,5 @@ class SysinfoController:
return render_template(
'sysinfo/list.jinja.html',
ipaddr=get_ip_address(),
l=self._l,
l=self._lang_dict,
)

View File

@ -1,6 +1,7 @@
from typing import Dict, Optional, List, Tuple, Union
from src.model.Screen import Screen
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
class ScreenManager:
@ -26,7 +27,21 @@ class ScreenManager:
return [ScreenManager.hydrate_object(raw_screen) for raw_screen in raw_screens]
def get(self, id: str) -> Optional[Screen]:
return self.hydrate_object(self._db.get_by_id(id), id)
try:
self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get_by(self, query) -> List[Screen]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_one_by(self, query) -> Optional[Screen]:
screens = self.hydrate_dict(self._db.get_by_query(query=query))
if len(screens) == 1:
return screens[0]
elif len(screens) > 1:
raise Error("More than one result for query")
return None
def get_all(self, sort: bool = False) -> List[Screen]:
raw_screens = self._db.get_all()
@ -54,9 +69,13 @@ class ScreenManager:
def update_form(self, id: str, name: str, host: str, port: int) -> None:
self._db.update_by_id(id, {"name": name, "host": host, "port": port})
def add_form(self, screen: Screen) -> None:
db_screen = screen.to_dict()
del db_screen['id']
def add_form(self, screen: Union[Screen, Dict]) -> None:
db_screen = screen
if not isinstance(screen, dict):
db_screen = screen.to_dict()
del db_screen['id']
self._db.add(db_screen)
def delete(self, id: str) -> None:

View File

@ -4,6 +4,7 @@ from typing import Dict, Optional, List, Tuple, Union
from src.model.Slide import Slide
from src.utils import str_to_enum
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
class SlideManager:
@ -29,7 +30,21 @@ class SlideManager:
return [SlideManager.hydrate_object(raw_slide) for raw_slide in raw_slides]
def get(self, id: str) -> Optional[Slide]:
return self.hydrate_object(self._db.get_by_id(id), id)
try:
self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get_by(self, query) -> List[Slide]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_one_by(self, query) -> Optional[Slide]:
slides = self.hydrate_dict(self._db.get_by_query(query=query))
if len(slides) == 1:
return slides[0]
elif len(slides) > 1:
raise Error("More than one result for query")
return None
def get_all(self, sort: bool = False) -> List[Slide]:
raw_slides = self._db.get_all()
@ -57,9 +72,13 @@ class SlideManager:
def update_form(self, id: str, name: str, duration: int) -> None:
self._db.update_by_id(id, {"name": name, "duration": duration})
def add_form(self, slide: Slide) -> None:
db_slide = slide.to_dict()
del db_slide['id']
def add_form(self, slide: Union[Slide, Dict]) -> None:
db_slide = slide
if not isinstance(slide, dict):
db_slide = slide.to_dict()
del db_slide['id']
self._db.add(db_slide)
def delete(self, id: str) -> None:

View File

@ -0,0 +1,95 @@
from typing import Dict, Optional, List, Tuple, Union
from src.model.Variable import Variable
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
class VariableManager:
DB_FILE = "data/db/settings.json"
def __init__(self):
self._db = PysonDB(self.DB_FILE)
self.init()
def init(self, lang_dict: Optional[Dict] = None) -> None:
default_vars = [
{"name": "port", "value": 5000, "description": lang_dict['settings_variable_help_port'] if lang_dict else ""},
{"name": "bind", "value": '0.0.0.0', "description": lang_dict['settings_variable_help_bind'] if lang_dict else ""},
{"name": "lang", "value": "en", "description": lang_dict['settings_variable_help_lang'] if lang_dict else ""},
{"name": "fleet_enabled", "value": "0", "description": lang_dict['settings_variable_help_fleet_enabled'] if lang_dict else ""},
]
for default_var in default_vars:
variable = self.get_one_by(query=lambda v: v['name'] == default_var['name'])
if not variable:
self.add_form(default_var)
elif variable.description != default_var['description']:
self._db.update_by_id(variable.id, {"description": default_var['description']})
def get_variable_map(self) -> Dict[str, Variable]:
var_map = {}
for var in self.get_all():
var_map[var.name] = var
return var_map
@staticmethod
def hydrate_object(raw_variable: dict, id: Optional[str] = None) -> Variable:
if id:
raw_variable['id'] = id
return Variable(**raw_variable)
@staticmethod
def hydrate_dict(raw_variables: dict) -> List[Variable]:
return [VariableManager.hydrate_object(raw_variable, raw_id) for raw_id, raw_variable in raw_variables.items()]
@staticmethod
def hydrate_list(raw_variables: list) -> List[Variable]:
return [VariableManager.hydrate_object(raw_variable) for raw_variable in raw_variables]
def get(self, id: str) -> Optional[Variable]:
try:
self.hydrate_object(self._db.get_by_id(id), id)
except IdDoesNotExistError:
return None
def get_by(self, query) -> List[Variable]:
return self.hydrate_dict(self._db.get_by_query(query=query))
def get_one_by(self, query) -> Optional[Variable]:
variables = self.hydrate_dict(self._db.get_by_query(query=query))
if len(variables) == 1:
return variables[0]
elif len(variables) > 1:
raise Error("More than one result for query")
return None
def get_all(self) -> List[Variable]:
raw_variables = self._db.get_all()
if isinstance(raw_variables, dict):
return VariableManager.hydrate_dict(raw_variables)
return VariableManager.hydrate_list(raw_variables)
def update_form(self, id: str, value: Union[int, bool, str]) -> None:
self._db.update_by_id(id, {"value": value})
def add_form(self, variable: Union[Variable, Dict]) -> None:
db_variable = variable
if not isinstance(variable, dict):
db_variable = variable.to_dict()
del db_variable['id']
self._db.add(db_variable)
def delete(self, id: str) -> None:
self._db.delete_by_id(id)
def to_dict(self, variables: List[Variable]) -> dict:
return [variable.to_dict() for variable in variables]

View File

@ -58,7 +58,7 @@ class Screen:
self._position = value
def __str__(self) -> str:
return f"Slide(" \
return f"Screen(" \
f"id='{self.id}',\n" \
f"name='{self.name}',\n" \
f"enabled='{self.enabled}',\n" \
@ -72,8 +72,8 @@ class Screen:
def to_dict(self) -> dict:
return {
"name": self.name,
"id": self.id,
"name": self.name,
"enabled": self.enabled,
"position": self.position,
"host": self.host,

View File

@ -84,8 +84,8 @@ class Slide:
def to_dict(self) -> dict:
return {
"name": self.name,
"id": self.id,
"name": self.name,
"enabled": self.enabled,
"position": self.position,
"type": self.type.value,

68
src/model/Variable.py Normal file
View File

@ -0,0 +1,68 @@
import json
from typing import Optional, Union
class Variable:
def __init__(self, name: str = '', description: str = '', value: Union[int, bool, str] = '', id: Optional[str] = None):
self._id = id if id else None
self._name = name
self._description = description
self._value = value
@property
def id(self) -> Union[int, str]:
return self._id
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
self._name = value
@property
def description(self) -> str:
return self._description
@description.setter
def description(self, value: str):
self._description = value
@property
def value(self) -> Union[int, bool, str]:
return self._value
@value.setter
def value(self, value: Union[int, bool, str]):
self._value = value
def __str__(self) -> str:
return f"Variable(" \
f"id='{self.id}',\n" \
f"name='{self.name}',\n" \
f"value='{self.value}',\n" \
f"description='{self.description}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"value": self.value,
"description": self.description,
}
def as_bool(self):
return bool(int(self._value))
def as_string(self):
return str(self._value)
def as_int(self):
return int(self._value)

View File

@ -15,9 +15,10 @@
</head>
<body>
<div class="container">
{% set fleet_mode = request.args.get('fleet_mode') == '1' %}
{% block header %}
{% if request.args.get('fleet_mode') != '1' %}
{% if not fleet_mode %}
<header>
<h1 class="logo">
<img src="{{ STATIC_PREFIX }}img/logo.png" />
@ -30,13 +31,18 @@
<i class="fa-regular fa-clock"></i> {{ l.slideshow_page_title }}
</a>
</li>
{% if FLEET_MODE %}
{% if FLEET_ENABLED %}
<li class="{{ 'active' if request.url_rule.endpoint == 'fleet_screen_list' }}">
<a href="{{ url_for('fleet_screen_list') }}">
<i class="fa fa-tv"></i> {{ l.fleet_page_title }}
</a>
</li>
{% endif %}
<li class="{{ 'active' if request.url_rule.endpoint == 'settings_variable_list' }}">
<a href="{{ url_for('settings_variable_list') }}">
<i class="fa-solid fa-cogs"></i> {{ l.settings_page_title }}
</a>
</li>
<li class="{{ 'active' if request.url_rule.endpoint == 'sysinfo_attribute_list' }}">
<a href="{{ url_for('sysinfo_attribute_list') }}">
<i class="fa-solid fa-list-check"></i> {{ l.sysinfo_page_title }}

View File

@ -0,0 +1,33 @@
<table class="variables">
<thead>
<tr>
<th>{{ l.settings_variable_panel_th_name }}</th>
<th>{{ l.settings_variable_panel_th_value }}</th>
<th>{{ l.settings_variable_panel_th_description }}</th>
<th class="tac">{{ l.settings_variable_panel_th_activity }}</th>
</tr>
</thead>
<tbody>
{% for variable in variables %}
<tr class="variable-item" data-level="{{ variable.id }}" data-entity="{{ variable.to_json() }}">
<td class="infos">
<div class="inner">
<i class="fa fa-cog icon-left"></i>
{{ variable.name }}
</div>
</td>
<td>
{{ variable.value }}
</td>
<td>
{{ variable.description }}
</td>
<td class="actions tac">
<a href="javascript:void(0);" class="item-edit variable-edit">
<i class="fa fa-pencil"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,34 @@
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.settings_page_title }}
{% endblock %}
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/settings.js"></script>
{% endblock %}
{% block page %}
<div class="toolbar">
<h2>{{ l.settings_page_title }}</h2>
</div>
<div class="panel">
<div class="panel-body">
<h3>{{ l.settings_variable_panel }}</h3>
{% include 'settings/component/table.jinja.html' %}
</div>
</div>
<div class="modals hidden">
<div class="modals-outer">
<a href="javascript:void(0);" class="modal-close">
<i class="fa fa-close"></i>
</a>
<div class="modals-inner">
{% include 'settings/modal/edit.jinja.html' %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,32 @@
<div class="modal modal-variable-edit hidden">
<h2>
{{ l.settings_variable_form_edit_submit }}
</h2>
<form action="/settings/variable/edit" method="POST">
<input type="hidden" name="id" id="variable-edit-id"/>
<div class="form-group">
<label for="variable-edit-name">{{ l.settings_variable_form_label_name }}</label>
<div class="widget">
<input type="text" name="name" id="variable-edit-name" required="required" disabled="disabled"/>
</div>
</div>
<div class="form-group">
<label for="variable-edit-value">{{ l.settings_variable_form_label_value }}</label>
<div class="widget">
<input type="text" name="value" id="variable-edit-value" required="required"/>
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.settings_variable_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-save icon-left"></i>{{ l.settings_variable_form_edit_submit }}
</button>
</div>
</form>
</div>

View File

@ -46,9 +46,11 @@
<a href="javascript:void(0);" class="item-edit slide-edit">
<i class="fa fa-pencil"></i>
</a>
<a href="{{ slide.location }}" class="item-download slide-download" target="_blank">
<i class="fa fa-eye"></i>
</a>
{% if not fleet_mode %}
<a href="{{ slide.location }}" class="item-download slide-download" target="_blank">
<i class="fa fa-eye"></i>
</a>
{% endif %}
<a href="javascript:void(0);" class="item-delete slide-delete">
<i class="fa fa-trash"></i>
</a>