add footer version

This commit is contained in:
jrk 2024-04-29 18:57:33 +02:00
parent 787ff22b5b
commit 0a9b6f712c
48 changed files with 275 additions and 43 deletions

2
data/www/browserconfig.xml Executable file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/favicon/ms-icon-70x70.png"/><square150x150logo src="/favicon/ms-icon-150x150.png"/><square310x310logo src="/favicon/ms-icon-310x310.png"/><TileColor>#692fbd</TileColor></tile></msapplication></browserconfig>

View File

@ -545,4 +545,17 @@ span.empty {
text-transform: uppercase;
opacity: 0.5;
color: #999;
}
footer {
align-self: stretch;
justify-content: center;
align-items: center;
flex-direction: column;
display: flex;
padding: 20px;
}
footer .version {
opacity: 0.3;
}

BIN
data/www/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
data/www/favicon/apple-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -21,6 +21,17 @@ jQuery(document).ready(function ($) {
$(document).on('click', '.variable-edit', function () {
const variable = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
if (variable['selectables']) {
var $select = $('<select id="variable-edit-value" name="value" required="required"></select>');
$.each(variable['selectables'], function(index, option) {
$select.append($('<option></option>').val(option.key).html(option.label));
});
$('#variable-edit-value').replaceWith($select);
} else {
$('#variable-edit-value').replaceWith('<input type="text" name="value" id="variable-edit-value" required="required" />');
}
showModal('modal-variable-edit');
$('.modal-variable-edit input:visible:eq(0)').focus().select();
$('#variable-edit-name').val(variable.name);

41
data/www/manifest.json Executable file
View File

@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/favicon/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/favicon/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/favicon/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/favicon/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/favicon/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/favicon/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

View File

@ -16,7 +16,16 @@ class FleetmodeScreenRestart(ObPlugin):
return 'Fleetmode Screen Restart'
def use_variables(self) -> List[Variable]:
return []
return [
# self.add_variable(
# name="foo",
# description="foo",
# type=VariableType.SELECT_SINGLE,
# value="foo",
# editable=True,
# selectables={"alpha": "Alpha", "beta": "Beta"}
# )
]
def use_hooks_registrations(self) -> List[HookRegistration]:
return [

View File

@ -24,7 +24,7 @@ class Application:
def start(self) -> None:
self._web_server.run()
def signal_handler(self, signal, frame) -> None:
logging.info("Shutting down...")
self._stop_event.set()

View File

@ -55,9 +55,15 @@ 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, editable: bool, description: str) -> Variable:
def add_variable(self, name: str, value='', type: VariableType = VariableType.STRING, editable: bool = True, description: str = '', selectables: Optional[Dict[str, str]] = None) -> Variable:
return self._model_store.variable().set_variable(
name=self.get_plugin_variable_name(name), value=value, type=type, editable=editable, description=description, plugin=self.use_id()
name=self.get_plugin_variable_name(name),
value=value,
type=type,
editable=editable,
description=description,
selectables=selectables if isinstance(selectables, dict) else None,
plugin=self.use_id(),
)
def add_static_hook_registration(self, hook: HookType, priority: int = 0) -> StaticHookRegistration:

View File

@ -11,10 +11,12 @@ load_dotenv()
class ConfigManager:
DEFAULT_PORT = 5000
VERSION_FILE = 'version.txt'
def __init__(self, variable_manager: VariableManager):
self._variable_manager = variable_manager
self._CONFIG = {
'version': None,
'port': self.DEFAULT_PORT,
'bind': '0.0.0.0',
'debug': False,
@ -26,6 +28,7 @@ class ConfigManager:
'player_url': 'http://localhost:{}'.format(self.DEFAULT_PORT)
}
self.load_version()
self.load_from_env()
self.load_from_args()
@ -49,9 +52,14 @@ class ConfigManager:
parser.add_argument('--log-file', '-lf', default=self._CONFIG['log_file'], help='Log File path')
parser.add_argument('--log-level', '-ll', default=self._CONFIG['log_level'], help='Log Level')
parser.add_argument('--log-stdout', '-ls', default=self._CONFIG['log_stdout'], action='store_true', help='Log to standard output')
parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number')
return parser.parse_args()
def load_version(self) -> str:
with open(self.VERSION_FILE, 'r') as file:
self._CONFIG['version'] = file.read()
def load_from_args(self) -> None:
args = self.parse_arguments()
@ -67,6 +75,9 @@ class ConfigManager:
self._CONFIG['log_level'] = args.log_level
if args.log_stdout:
self._CONFIG['log_stdout'] = args.log_stdout
if args.version:
print("Obscreen version v{} (https://github.com/jr-k/obscreen)".format(self._CONFIG['version']))
sys.exit(0)
def load_from_env(self) -> None:
for key in self._CONFIG:

View File

@ -1,10 +1,13 @@
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Variable import Variable
from src.model.entity.Selectable import Selectable
from src.model.enum.VariableType import VariableType
from pysondb import PysonDB
from pysondb.errors import IdDoesNotExistError
from src.utils import get_keys
import time
SELECTABLE_BOOLEAN = {"1": "", "0": ""}
class VariableManager:
@ -15,27 +18,37 @@ class VariableManager:
self._var_map = {}
self.reload()
def set_variable(self, name: str, value, type: VariableType, editable: bool, description: str, plugin: Optional[None] = 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) -> Variable:
if isinstance(value, bool) and value:
value = '1'
elif isinstance(value, bool) and not value:
value = '0'
if type == VariableType.BOOL:
selectables = SELECTABLE_BOOLEAN
default_var = {
"name": name,
"value": value,
"type": type.value,
"editable": editable,
"description": description,
"plugin": plugin
"plugin": plugin,
"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'])
if not variable:
self.add_form(default_var)
variable = self.get_one_by_name(default_var['name'])
elif variable.description != default_var['description']:
self._db.update_by_id(variable.id, {"description": default_var['description']})
else:
same_selectables = get_keys(default_var, 'selectables') == get_keys(variable, 'selectables')
if variable.description != default_var['description']:
self._db.update_by_id(variable.id, {"description": default_var['description']})
if not same_selectables:
self._db.update_by_id(variable.id, {"selectables": default_var['selectables']})
if variable.name == 'last_restart':
self._db.update_by_id(variable.id, {"value": time.time()})
@ -44,7 +57,7 @@ class VariableManager:
def reload(self, lang_map: Optional[Dict] = None) -> None:
default_vars = [
{"name": "lang", "value": "en", "type": VariableType.STRING, "editable": True, "description": lang_map['settings_variable_help_lang'] if lang_map else ""},
{"name": "lang", "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": lang_map['settings_variable_help_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_help_fleet_enabled'] if lang_map else ""},
{"name": "external_url", "value": "", "type": VariableType.STRING, "editable": True, "description": lang_map['settings_variable_help_external_url'] if lang_map else ""},
{"name": "last_restart", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": lang_map['settings_variable_help_ro_editable'] if lang_map else ""},
@ -76,6 +89,9 @@ class VariableManager:
if id:
raw_variable['id'] = id
if 'selectables' in raw_variable and raw_variable['selectables']:
raw_variable['selectables'] = [Selectable(**selectable) for selectable in raw_variable['selectables']]
return Variable(**raw_variable)
@staticmethod
@ -105,7 +121,8 @@ class VariableManager:
return self.get_one_by(query=lambda v: v['name'] == name)
def get_one_by(self, query) -> Optional[Variable]:
variables = self.hydrate_dict(self._db.get_by_query(query=query))
object = self._db.get_by_query(query=query)
variables = self.hydrate_dict(object)
if len(variables) == 1:
return variables[0]
elif len(variables) > 1:

34
src/model/entity/Selectable.py Executable file
View File

@ -0,0 +1,34 @@
class Selectable:
def __init__(self, key: str = '', label: str = ''):
self._key = key
self._label = label
@property
def key(self) -> str:
return self._key
@key.setter
def key(self, value: str):
self._key = value
@property
def label(self) -> str:
return self._label
@label.setter
def label(self, value: str):
self._label = value
def __str__(self) -> str:
return f"Selectable(" \
f"key='{self.key}',\n" \
f"label='{self.label}',\n" \
f")"
def to_dict(self) -> dict:
return {
"key": self.key,
"label": self.label
}

View File

@ -1,8 +1,9 @@
import json
import time
from typing import Optional, Union
from typing import Optional, Union, Dict, List
from src.model.enum.VariableType import VariableType
from src.model.entity.Selectable import Selectable
from src.utils import str_to_enum
@ -10,7 +11,7 @@ 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):
plugin: Optional[str] = None, selectables: Optional[List[Selectable]] = None):
self._id = id if id else None
self._name = name
self._type = str_to_enum(type, VariableType) if isinstance(type, str) else type
@ -18,11 +19,23 @@ class Variable:
self._value = value
self._editable = editable
self._plugin = plugin
self._selectables = selectables
@property
def id(self) -> Union[int, str]:
return self._id
@property
def selectables(self) -> List[Selectable]:
return self._selectables
@selectables.setter
def selectables(self, value: List[Selectable]):
self._selectables = value
def add_selectable(self, value: Selectable):
self._selectables.append(value)
@property
def name(self) -> str:
return self._name
@ -80,6 +93,7 @@ class Variable:
f"description='{self.description}',\n" \
f"editable='{self.editable}',\n" \
f"plugin='{self.plugin}',\n" \
f"selectables='{self.selectables}',\n" \
f")"
def to_json(self) -> str:
@ -94,6 +108,7 @@ class Variable:
"description": self.description,
"editable": self.editable,
"plugin": self.plugin,
"selectables": [selectable.to_dict() for selectable in self.selectables] if isinstance(self._selectables, list) else None
}
def as_bool(self) -> bool:
@ -105,18 +120,22 @@ class Variable:
def as_int(self) -> int:
return int(self._value)
def as_ctime(self):
def as_ctime(self) -> int:
return time.ctime(self._value)
def display(self):
def display(self) -> Union[int, bool, str]:
if self.type == VariableType.INT:
return self.as_int()
elif self.type == VariableType.BOOL:
return self.as_bool()
elif self.type == VariableType.TIMESTAMP:
return self.as_ctime()
elif self.type == VariableType.SELECT_SINGLE:
for selectable in self.selectables:
if selectable.key == self.value:
return str(selectable.label)
return self.as_string()
def is_from_plugin(self):
return self.plugin
def is_from_plugin(self) -> Optional[str]:
return self.plugin

View File

@ -5,5 +5,6 @@ class VariableType(Enum):
BOOL = 'bool'
STRING = 'string'
SELECT_SINGLE = 'string'
INT = 'int'
TIMESTAMP = 'timestamp'

View File

@ -22,6 +22,7 @@ class TemplateRenderer:
globals = dict(
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
FLEET_ENABLED=self._model_store.variable().map().get('fleet_enabled').as_bool(),
VERSION=self._model_store.config().map().get('version'),
LANG=self._model_store.variable().map().get('lang').as_string(),
HOOK=self._render_hook,
)

View File

@ -2,10 +2,31 @@ import re
import subprocess
import platform
from typing import Optional
from typing import Optional, List
from enum import Enum
def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') -> Optional[List]:
if dict_or_object is None:
return None
is_dict = isinstance(dict_or_object, dict)
is_object = not is_dict and isinstance(dict_or_object, object)
if is_dict:
iterable = dict_or_object[key_list_name]
if iterable is None:
return None
return [item[key_attr_name] for item in iterable]
if is_object:
iterable = getattr(dict_or_object, key_list_name)
if iterable is None:
return None
return [getattr(item, key_attr_name) for item in iterable]
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.4
1.5

View File

@ -6,6 +6,24 @@
</title>
<meta name="robots" content="noindex, nofollow">
<meta name="google" content="notranslate">
<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">
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_PREFIX }}favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="{{ STATIC_PREFIX }}favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_PREFIX }}favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{ STATIC_PREFIX }}favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_PREFIX }}favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_PREFIX }}favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="{{ STATIC_PREFIX }}favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_PREFIX }}favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="{{ STATIC_PREFIX }}favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_PREFIX }}favicon/favicon-16x16.png">
<link rel="manifest" href="{{ STATIC_PREFIX }}/manifest.json">
<meta name="msapplication-TileColor" content="#692fbd">
<meta name="msapplication-TileImage" content="{{ STATIC_PREFIX }}favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#692fbd">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
@ -65,6 +83,8 @@
{% block footer %}
<footer>
{{ HOOK(H_ROOT_FOOTER) }}
<p class="version">Obscreen version {{ VERSION }}</p>
</footer>
{% endblock %}
</div>

View File

@ -7,21 +7,21 @@
<div class="form-group">
<label for="screen-add-name">{{ l.fleet_screen_form_label_name }}</label>
<div class="widget">
<input name="name" type="text" id="screen-add-name" required="required"/>
<input name="name" type="text" id="screen-add-name" required="required" />
</div>
</div>
<div class="form-group">
<label for="screen-add-host">{{ l.fleet_screen_form_label_host }}</label>
<div class="widget">
<input type="text" name="host" id="screen-add-host" required="required"/>
<input type="text" name="host" id="screen-add-host" required="required" />
</div>
</div>
<div class="form-group">
<label for="screen-add-port">{{ l.fleet_screen_form_label_port }}</label>
<div class="widget">
<input type="number" name="port" id="screen-add-port" required="required"/>
<input type="number" name="port" id="screen-add-port" required="required" />
</div>
</div>

View File

@ -4,26 +4,26 @@
</h2>
<form action="/fleet/screen/edit" method="POST">
<input type="hidden" name="id" id="screen-edit-id"/>
<input type="hidden" name="id" id="screen-edit-id" />
<div class="form-group">
<label for="screen-edit-name">{{ l.fleet_screen_form_label_name }}</label>
<div class="widget">
<input type="text" name="name" id="screen-edit-name" required="required"/>
<input type="text" name="name" id="screen-edit-name" required="required" />
</div>
</div>
<div class="form-group">
<label for="screen-edit-host">{{ l.fleet_screen_form_label_host }}</label>
<div class="widget">
<input type="text" name="host" id="screen-edit-host" required="required"/>
<input type="text" name="host" id="screen-edit-host" required="required" />
</div>
</div>
<div class="form-group">
<label for="screen-edit-port">{{ l.fleet_screen_form_label_port }}</label>
<div class="widget">
<input type="number" name="port" id="screen-edit-port" required="required"/>
<input type="number" name="port" id="screen-edit-port" required="required" />
</div>
</div>

View File

@ -7,9 +7,9 @@
</tr>
</thead>
<tbody>
{% set last_plugin = '' %}
{% set ns = namespace(last_plugin='') %}
{% for variable in variables %}
{% if variable.plugin and last_plugin != variable.plugin %}
{% if variable.plugin and ns.last_plugin != variable.plugin %}
<tr>
<td colspan="2">
<h3>
@ -26,8 +26,16 @@
</div>
</td>
<td>
{% if variable.value %}
{{ variable.value }}
{% if variable.value %}
{% if variable.type.value == 'bool' %}
{% if variable.display() %}
{% else %}
{% endif %}
{% else %}
{{ variable.display() }}
{% endif %}
{% else %}
<span class="empty">{{ l.common_empty }}</span>
{% endif %}
@ -38,7 +46,7 @@
</a>
</td>
</tr>
{% set last_plugin = variable.plugin %}
{% set ns.last_plugin = variable.plugin %}
{% endfor %}
</tbody>
</table>

View File

@ -4,19 +4,19 @@
</h2>
<form action="/settings/variable/edit" method="POST">
<input type="hidden" name="id" id="variable-edit-id"/>
<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"/>
<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"/>
<input type="text" name="value" id="variable-edit-value" required="required" />
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="form-group">
<label for="slide-add-name">{{ l.slideshow_slide_form_label_name }}</label>
<div class="widget">
<input name="name" type="text" id="slide-add-name" required="required"/>
<input name="name" type="text" id="slide-add-name" required="required" />
</div>
</div>
<div class="form-group">
@ -24,16 +24,16 @@
<div class="form-group object-input">
<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="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"/>
class="slide-add-object-input hidden" disabled="disabled" />
</div>
</div>
<div class="form-group">
<label for="slide-add-duration">{{ l.slideshow_slide_form_label_duration }}</label>
<div class="widget">
<input type="number" name="duration" id="slide-add-duration" required="required"/>
<input type="number" name="duration" id="slide-add-duration" required="required" />
<span>{{ l.slideshow_slide_form_label_duration_unit }}</span>
</div>
</div>

View File

@ -4,12 +4,12 @@
</h2>
<form action="/slideshow/slide/edit" method="POST">
<input type="hidden" name="id" id="slide-edit-id"/>
<input type="hidden" name="id" id="slide-edit-id" />
<div class="form-group">
<label for="slide-edit-name">{{ l.slideshow_slide_form_label_name }}</label>
<div class="widget">
<input type="text" name="name" id="slide-edit-name" required="required"/>
<input type="text" name="name" id="slide-edit-name" required="required" />
</div>
</div>
<div class="form-group">
@ -26,14 +26,14 @@
<div class="form-group">
<label for="slide-edit-location">{{ l.slideshow_slide_form_label_location }}</label>
<div class="widget">
<input type="text" name="location" id="slide-edit-location" disabled="disabled"/>
<input type="text" name="location" id="slide-edit-location" disabled="disabled" />
</div>
</div>
<div class="form-group">
<label for="slide-edit-duration">{{ l.slideshow_slide_form_label_duration }}</label>
<div class="widget">
<input type="number" name="duration" id="slide-edit-duration" required="required"/>
<input type="number" name="duration" id="slide-edit-duration" required="required" />
<span>{{ l.slideshow_slide_form_label_duration_unit }}</span>
</div>
</div>

View File

@ -34,13 +34,31 @@
{% for ro_variable in ro_variables %}
<tr>
<td>{{ ro_variable.description }}</td>
<td>{{ ro_variable.display() }}</td>
<td>
{% if ro_variable.type.value == 'bool' %}
{% if ro_variable.display() %}
{% else %}
{% endif %}
{% else %}
{{ ro_variable.display() }}
{% endif %}
</td>
</tr>
{% endfor %}
{% for env_key, env_value in env_variables.items() %}
<tr>
<td>{{ env_key }}</td>
<td>{{ env_value }}</td>
<td>
{% if env_value == true %}
{% elif env_value == false %}
{% else %}
{{ env_value }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>