remove autoconfigure player file + add demo mode

This commit is contained in:
jr-k 2024-07-19 01:16:34 +02:00
parent 99d2518d92
commit 34ba9cd812
23 changed files with 91 additions and 124 deletions

View File

@ -1,4 +1,3 @@
DEBUG=false
PORT=5000
SECRET_KEY=ANY_SECRET_KEY_HERE
PLAYER_AUTOSTART_FILE=./var/run/play # Replace by "/dev/null" if not needed

View File

@ -15,7 +15,7 @@ Obscreen is a user-friendly self-hosted digital signage tool. Manage a fleet of
Try it!
Demo Server (Location: Roubaix - France): https://demo.obscreen.io
Demo Server (Location: Roubaix - France): [https://demo.obscreen.io](https://demo.obscreen.io?username=admin&password=admin)
It is a temporary live demo, all data will be deleted after 10 minutes. Sponsored by myself.

View File

@ -9,10 +9,8 @@ services:
environment:
- DEBUG=${DEBUG-false}
- PORT=${PORT-5000}
- PLAYER_AUTOSTART_FILE=/app/var/run/play
- SECRET_KEY=${SECRET_KEY-ANY_SECRET_KEY_HERE}
volumes:
- .:/app
- ${PLAYER_AUTOSTART_FILE-/dev/null}:/app/var/run/play
ports:
- ${PORT}:${PORT}

View File

@ -6,10 +6,8 @@ services:
environment:
- DEBUG=false
- PORT=5000
- PLAYER_AUTOSTART_FILE=/app/var/run/play
- SECRET_KEY=ANY_SECRET_KEY_HERE
volumes:
- /dev/null:/app/var/run/play
- ./data/db:/app/data/db
- ./data/uploads:/app/data/uploads
ports:

View File

@ -6,10 +6,8 @@ services:
environment:
- DEBUG=false
- PORT=5000
- PLAYER_AUTOSTART_FILE=/app/var/run/play
- SECRET_KEY=ANY_SECRET_KEY_HERE
volumes:
- ./var/run/play:/app/var/run/play
- ./data/db:/app/data/db
- ./data/uploads:/app/data/uploads
ports:

View File

@ -23,7 +23,6 @@ cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
docker run --restart=always --name obscreen --pull=always \
-e DEBUG=false \
-e PORT=5000 \
-e PLAYER_AUTOSTART_FILE=/app/var/run/play \
-e SECRET_KEY=ANY_SECRET_KEY_HERE \
-p 5000:5000 \
-v ./data/db:/app/data/db \

View File

@ -25,14 +25,10 @@ curl -sSL get.docker.com | sh && sudo usermod -aG docker $(whoami) && logout # t
# Prepare application data file tree
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
# Prepare player autostart file
mkdir -p var/run && touch var/run/play && chmod +x var/run/play
# Run the Docker container
docker run --rm --name obscreen --pull=always \
-e DEBUG=false \
-e PORT=5000 \
-e PLAYER_AUTOSTART_FILE=/app/var/run/play \
-e SECRET_KEY=ANY_SECRET_KEY_HERE \
-p 5000:5000 \
-v ./data/db:/app/data/db \
@ -52,9 +48,6 @@ docker run --rm --name obscreen --pull=always \
# Prepare application data file tree
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads obscreen/system && cd obscreen
# Prepare player autostart file
mkdir -p var/run && touch var/run/play && chmod +x var/run/play
# Download docker-compose.yml
curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.yml > docker-compose.yml

View File

@ -226,6 +226,7 @@
"basic_month_10": "October",
"basic_month_11": "November",
"basic_month_12": "December",
"common_bad_file_type": "Bad file type uploaded",
"common_restart_needed": "Please restart obscreen studio (or restart the device) for the changes to take effect",
"common_pick_element": "Pick an element",
"common_untitled": "<untitled>",
@ -275,9 +276,8 @@
"enum_variable_section_general": "1. General",
"enum_variable_section_player_options": "2. Player options",
"enum_variable_section_player_animation": "3. Player animation",
"enum_variable_section_playlist": "4. Playlists",
"enum_variable_section_fleet": "5. Fleet management",
"enum_variable_section_security": "6. Security",
"enum_variable_section_fleet": "4. Fleet management",
"enum_variable_section_security": "5. Security",
"enum_application_language_english": "English",
"enum_application_language_french": "French",
"enum_application_language_italian": "Italian",

View File

@ -227,6 +227,7 @@
"basic_month_10": "Octubre",
"basic_month_11": "Noviembre",
"basic_month_12": "Diciembre",
"common_bad_file_type": "Tipo de archivo incorrecto cargado",
"common_restart_needed": "Reinicie obscreen studio (o reinicie el dispositivo) para que los cambios surtan efecto",
"common_pick_element": "Elige un elemento",
"common_untitled": "<sin-título>",
@ -276,9 +277,8 @@
"enum_variable_section_general": "1. General",
"enum_variable_section_player_options": "2. Opciones del reproductor",
"enum_variable_section_player_animation": "3. Animación del reproductor",
"enum_variable_section_playlist": "4. Playlist",
"enum_variable_section_fleet": "5. Gestión de flota",
"enum_variable_section_security": "6. Seguridad",
"enum_variable_section_fleet": "4. Gestión de flota",
"enum_variable_section_security": "5. Seguridad",
"enum_application_language_english": "Inglés",
"enum_application_language_french": "Francés",
"enum_application_language_italian": "Italiano",

View File

@ -228,6 +228,7 @@
"basic_month_10": "Octobre",
"basic_month_11": "Novembre",
"basic_month_12": "Décembre",
"common_bad_file_type": "Type de fichier uploadé incorrect",
"common_restart_needed": "Veuillez redémarrer obscreen studio (ou redémarrer l'appareil) pour que les changements soient pris en compte",
"common_pick_element": "Choisissez un élément",
"common_untitled": "<sans-titre>",
@ -277,9 +278,8 @@
"enum_variable_section_general": "1. Général",
"enum_variable_section_player_options": "2. Options du lecteur",
"enum_variable_section_player_animation": "3. Animation du lecteur",
"enum_variable_section_playlist": "4. Playlists",
"enum_variable_section_fleet": "5. Gestion de flotte",
"enum_variable_section_security": "6. Sécurité",
"enum_variable_section_fleet": "4. Gestion de flotte",
"enum_variable_section_security": "5. Sécurité",
"enum_application_language_english": "Anglais",
"enum_application_language_french": "Français",
"enum_application_language_italian": "Italien",

View File

@ -227,6 +227,7 @@
"basic_month_10": "Ottobre",
"basic_month_11": "Novembre",
"basic_month_12": "Dicembre",
"common_bad_file_type": "Tipo di file caricato non valido",
"common_restart_needed": "Riavvia obscreen studio (o riavvia il dispositivo) affinché le modifiche abbiano effetto",
"common_pick_element": "Scegli un elemento",
"common_untitled": "<senza-titolo>",
@ -276,9 +277,8 @@
"enum_variable_section_general": "1. Generale",
"enum_variable_section_player_options": "2. Opzioni monitor",
"enum_variable_section_player_animation": "3. Animazioni monitor",
"enum_variable_section_playlist": "4. Playlist",
"enum_variable_section_fleet": "5. Gestione panoramica",
"enum_variable_section_security": "6. Sicurezza",
"enum_variable_section_fleet": "4. Gestione panoramica",
"enum_variable_section_security": "5. Sicurezza",
"enum_application_language_english": "Inglese",
"enum_application_language_french": "Francese",
"enum_application_language_italian": "Italiano",

View File

@ -48,7 +48,7 @@ class AuthController(ObController):
return render_template(
'auth/login.jinja.html',
login_error=login_error,
last_username=request.form['username'] if 'username' in request.form else ''
last_username=request.form['username'] if 'username' in request.form else None
)
def logout(self):

View File

@ -63,8 +63,11 @@ class ContentController(ObController):
def slideshow_content_add(self):
working_folder_path, working_folder = self.get_working_folder()
route_args = {
"path": working_folder_path,
}
self._model_store.content().add_form_raw(
content = self._model_store.content().add_form_raw(
name=request.form['name'],
type=str_to_enum(request.form['type'], ContentType),
request_files=request.files,
@ -73,7 +76,10 @@ class ContentController(ObController):
folder_id=working_folder.id if working_folder else None
)
return redirect(url_for('slideshow_content_list', path=working_folder_path))
if not content:
route_args["error"] = 'common_bad_file_type'
return redirect(url_for('slideshow_content_list', **route_args))
def slideshow_content_upload_bulk(self):
working_folder_path, working_folder = self.get_working_folder()

View File

@ -64,6 +64,7 @@ class PlayerController(ObController):
return render_template(
'player/default.jinja.html',
interfaces=[iface['ip_address'] for iface in get_network_interfaces()],
external_url=self._model_store.variable().get_one_by_name('external_url').as_string().strip(),
time_with_seconds=self._model_store.variable().get_one_by_name('default_slide_time_with_seconds'),
noplaylist=request.args.get('noplaylist', '0') == '1'
)

View File

@ -68,5 +68,6 @@ class SysinfoController(ObController):
def sysinfo_get_ipaddr(self):
return jsonify({
'external_url': self._model_store.variable().get_one_by_name('external_url').as_string().strip(),
'interfaces': [iface['ip_address'] for iface in get_network_interfaces()]
})

View File

@ -1,33 +1,27 @@
import re
import os
import sys
import logging
import argparse
from src.manager.VariableManager import VariableManager
from src.util.utils import am_i_in_docker
from dotenv import load_dotenv
load_dotenv()
class ConfigManager:
DEFAULT_PORT = 5000
DEFAULT_PLAYER_AUTOSTART_PATH = './var/run/play'
VERSION_FILE = 'version.txt'
def __init__(self, variable_manager: VariableManager):
self._variable_manager = variable_manager
def __init__(self):
self._CONFIG = {
'version': None,
'demo': False,
'port': self.DEFAULT_PORT,
'bind': '0.0.0.0',
'debug': False,
'player_autostart_file': self.DEFAULT_PLAYER_AUTOSTART_PATH,
'log_file': None,
'log_level': 'INFO',
'log_stdout': True,
'secret_key': 'ANY_SECRET_KEY_HERE',
'player_url': 'http://localhost:{}'.format(self.DEFAULT_PORT)
}
self.load_version()
@ -36,8 +30,6 @@ class ConfigManager:
self._CONFIG['port'] = self._CONFIG['port'] if self._CONFIG['port'] else self.DEFAULT_PORT
self.autoconfigure()
if self.map().get('debug'):
logging.debug(self._CONFIG)
@ -50,10 +42,10 @@ class ConfigManager:
parser.add_argument('--port', '-p', default=self._CONFIG['port'], help='Application port')
parser.add_argument('--bind', '-b', default=self._CONFIG['bind'], help='Application bind address')
parser.add_argument('--secret-key', '-s', default=self._CONFIG['secret_key'], help='Application secret key (any random string)')
parser.add_argument('--player-autostart-file', '-x', default=self._CONFIG['player_autostart_file'], help='Path to player autostart file')
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('--demo', '-o', default=self._CONFIG['demo'], help='Demo mode to showcase obscreen in a sandbox')
parser.add_argument('--version', '-v', default=None, action='store_true', help='Get version number')
return parser.parse_args()
@ -67,8 +59,8 @@ class ConfigManager:
if args.debug:
self._CONFIG['debug'] = args.debug
if args.player_autostart_file:
self._CONFIG['player_autostart_file'] = args.player_autostart_file
if args.demo:
self._CONFIG['demo'] = args.demo
if args.log_file:
self._CONFIG['log_file'] = args.log_file
if args.secret_key:
@ -91,60 +83,3 @@ class ConfigManager:
value = True
self._CONFIG[key.lower()] = value
logging.info(f"Env var {key} has been found")
def autoconfigure(self) -> None:
self.autoconfigure_player_url()
if self.map().get('player_autostart_file'):
self.autoconfigure_player_autostart_file()
def autoconfigure_player_url(self) -> str:
self._CONFIG['player_url'] = 'http://localhost:{}'.format(self.map().get('port'))
return self._CONFIG['player_url']
def autoconfigure_player_autostart_file(self) -> None:
path = self.map().get('player_autostart_file')
in_docker = am_i_in_docker()
player_autostart_path = self.DEFAULT_PLAYER_AUTOSTART_PATH if in_docker else path
if os.path.isdir(path) or not os.path.exists(path):
if not in_docker:
open(player_autostart_path, 'a').close()
else:
logging.error(
"Player autostart file {} doesn't exist on your server'\n".format(
player_autostart_path
)
)
sys.exit(1)
else:
logging.info("Overriding player autostart file {}".format(player_autostart_path))
player_url = self.map().get('player_url')
os.makedirs(os.path.dirname(player_autostart_path), exist_ok=True)
xenv_presets = """#!/bin/bash
# Disable screensaver and DPMS
xset s off
xset -dpms
xset s noblank
# Start unclutter to hide the mouse cursor
unclutter -display :0 -noevents -grab &
# Modify Chromium preferences to avoid restore messages
mkdir -p __HOME__/.config/chromium/Default 2>/dev/null
touch __HOME__/.config/chromium/Default/Preferences
sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' __HOME__/.config/chromium/Default/Preferences
RESOLUTION=$(DISPLAY=:0 xrandr | grep '*' | awk '{print $1}')
WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
# Start Chromium in kiosk mode
chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=${WIDTH},${HEIGHT} --display=:0 __PLAYER_URL__
""".replace('__PLAYER_URL__', player_url).replace('__HOME__', os.environ['HOME'])
with open(player_autostart_path, 'w') as file:
file.write(xenv_presets)

View File

@ -177,6 +177,11 @@ class ContentManager(ModelManager):
if not object or object.filename == '':
return None
guessed_type = ContentType.guess_content_type_file(object)
if not guessed_type or guessed_type != type:
return None
if object:
object.seek(0)
object_name = randomize_filename(object.filename)

View File

@ -5,6 +5,7 @@ from typing import Dict, Optional, List, Tuple, Union
from src.manager.DatabaseManager import DatabaseManager
from src.manager.LangManager import LangManager
from src.manager.ConfigManager import ConfigManager
from src.manager.UserManager import UserManager
from src.model.entity.Variable import Variable
from src.model.entity.Selectable import Selectable
@ -38,9 +39,10 @@ class VariableManager:
"value TEXT"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager, config_manager: ConfigManager):
self._lang_manager = lang_manager
self._user_manager = user_manager
self._config_manager = config_manager
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
self._var_map = {}
self.reload()
@ -94,6 +96,9 @@ class VariableManager:
if variable.refresh_player != default_var['refresh_player']:
self._db.update_by_id(self.TABLE_NAME, variable.id, {"refresh_player": default_var['refresh_player']})
if variable.editable != default_var['editable']:
self._db.update_by_id(self.TABLE_NAME, variable.id, {"editable": default_var['editable']})
if not same_selectables_keys or not same_selectables_label:
self._db.update_by_id(self.TABLE_NAME, variable.id, {"selectables": default_var['selectables']})
@ -103,13 +108,15 @@ class VariableManager:
return variable
def reload(self) -> None:
demo = self._config_manager.map().get('demo')
default_vars = [
# Editable (Customizable settings)
### General
{"name": "lang", "section": self.t(VariableSection.GENERAL), "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_lang'), "selectables": self.t(ApplicationLanguage), "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, "unit": VariableUnit.MEGABYTE, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False},
{"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": False if demo else True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False},
{"name": "slide_upload_limit", "section": self.t(VariableSection.GENERAL), "value": 32, "unit": VariableUnit.MEGABYTE, "type": VariableType.INT, "editable": False if demo else True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False},
{"name": "dark_mode", "section": self.t(VariableSection.GENERAL), "value": True, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_dark_mode'), "refresh_player": False},
### Player Options
@ -209,6 +216,16 @@ class VariableManager:
return self.get_by(query="editable = 0", sort="name")
def update_form(self, id: int, value: Union[int, bool, str]) -> None:
variable = self.get(id)
if not variable:
if var.name in self._var_map:
del self._var_map[var.name]
return None
if not variable.editable:
return None
self._db.update_by_id(self.TABLE_NAME, id, {"value": value})
var = self.get_one_by("id = {}".format(id))
self._var_map[var.name] = var

View File

@ -19,19 +19,19 @@ class ModelStore:
def __init__(self, get_plugins: Dict):
self._get_plugins = get_plugins
# Core
self._config_manager = ConfigManager()
self._logging_manager = LoggingManager(config_manager=self._config_manager)
# Pure
self._lang_manager = LangManager()
self._database_manager = DatabaseManager()
# Dynamics
self._user_manager = UserManager(lang_manager=self._lang_manager, database_manager=self._database_manager, on_user_delete=self.on_user_delete)
self._variable_manager = VariableManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
self._variable_manager = VariableManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, config_manager=self._config_manager)
self._lang_manager.set_lang(self.variable().map().get('lang').as_string())
# Core
self._config_manager = ConfigManager(variable_manager=self._variable_manager)
self._logging_manager = LoggingManager(config_manager=self._config_manager)
# Model
self._folder_manager = FolderManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)
self._node_player_manager = NodePlayerManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager, variable_manager=self._variable_manager)

View File

@ -29,13 +29,13 @@
<div class="form-group">
<label for="password">{{ l.login_form_username }}</label>
<div class="widget">
<input type="text" name="username" required="required" value="{{ last_username }}" />
<input type="text" name="username" required="required" value="{{ last_username|d(request.args.get('username', ''), True) }}" />
</div>
</div>
<div class="form-group">
<label for="password">{{ l.login_form_password }}</label>
<div class="widget">
<input type="password" name="password" required="required"/>
<input type="password" name="password" required="required" value="{{ request.args.get('password', '') }}" />
</div>
</div>
<div class="actions actions-center">

View File

@ -52,9 +52,9 @@
{% if ro_variable.value %}
{% if ro_variable.type.value == 'bool' %}
{% if ro_variable.display() %}
<i class="fa fa-check alert alert-success alert-icon icon-value"></i>
{% else %}
<i class="fa fa-times alert alert-error alert-icon icon-value"></i>
{% endif %}
{% else %}
{{ ro_variable.display() }}
@ -78,9 +78,9 @@
<td class="description">{{ env_key.replace('_',' ')|capitalize }}</td>
<td class="value">
{% if env_value == true %}
<i class="fa fa-check alert alert-success alert-icon icon-value"></i>
{% elif env_value == false %}
<i class="fa fa-times alert alert-error alert-icon icon-value"></i>
{% elif env_value == none %}
<span class="empty">{{ l.common_empty }}</span>
{% else %}

View File

@ -3,6 +3,7 @@
<head>
<script type="text/javascript">
const time_with_seconds = {{ 'true' if time_with_seconds.as_bool() else 'false' }};
let external_url = '{{ external_url }}';
function updateTime() {
const date = new Date();
@ -13,11 +14,11 @@
const month = date.getMonth();
const year = date.getFullYear();
const day = date.getDay();
const dayLabels = ["{{l.basic_day_7}}", "{{l.basic_day_1}}", "{{l.basic_day_2}}", "{{l.basic_day_3}}", "{{l.basic_day_4}}", "{{l.basic_day_5}}", "{{l.basic_day_6}}"];
const monthLabels = ["{{l.basic_month_1}}", "{{l.basic_month_2}}", "{{l.basic_month_3}}", "{{l.basic_month_4}}", "{{l.basic_month_5}}", "{{l.basic_month_6}}", "{{l.basic_month_7}}", "{{l.basic_month_8}}", "{{l.basic_month_9}}", "{{l.basic_month_10}}", "{{l.basic_month_11}}", "{{l.basic_month_12}}"];
const day_labels = ["{{l.basic_day_7}}", "{{l.basic_day_1}}", "{{l.basic_day_2}}", "{{l.basic_day_3}}", "{{l.basic_day_4}}", "{{l.basic_day_5}}", "{{l.basic_day_6}}"];
const month_labels = ["{{l.basic_month_1}}", "{{l.basic_month_2}}", "{{l.basic_month_3}}", "{{l.basic_month_4}}", "{{l.basic_month_5}}", "{{l.basic_month_6}}", "{{l.basic_month_7}}", "{{l.basic_month_8}}", "{{l.basic_month_9}}", "{{l.basic_month_10}}", "{{l.basic_month_11}}", "{{l.basic_month_12}}"];
const timeLabel = hours + ":" + minutes + (time_with_seconds ? ':' + seconds : '');
const dateLabel = dayLabels[day] + " " + dayInMonth + " " + monthLabels[month] + " " + year;
const dateLabel = day_labels[day] + " " + dayInMonth + " " + month_labels[month] + " " + year;
document.getElementById('time').innerHTML = timeLabel;
document.getElementById('date').innerHTML = dateLabel;
@ -32,8 +33,9 @@
xhr.open("GET", "{{ url_for('sysinfo_get_ipaddr') }}", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const jsonResponse = JSON.parse(xhr.responseText);
setIps(jsonResponse.interfaces)
const json_response = JSON.parse(xhr.responseText);
external_url = json_response.external_url;
setIps(json_response.interfaces)
}
};
xhr.send();
@ -51,6 +53,8 @@
.ipaddr a { text-decoration: none; font-weight: normal; font-size: 1.2em; color: #666; transition: .1s ease-in all; display:block; flex-direction:row; align-self: stretch; flex: 1; text-align: center; margin: 0 20px; border-radius: 6px; padding: 10px 0; }
.ipaddr a span { color: white; font-size: 1.2em; }
.ipaddr a:hover { color: #fff; background: #017BFF; }
.ipaddr a.external { color: #017BFF; }
.ipaddr a.external:hover { background: #FFFFFF; }
#hidden-container { display: none; }
</style>
</head>
@ -82,21 +86,29 @@
Object.assign($ipaddrs, { className: 'ipaddrs'});
$container.appendChild($ipaddrs);
if (external_url.length) {
addLink($ipaddrs, external_url, external_url, 'external');
}
for (let i = 0; i < ips.length; i++) {
addIp(ips[i], $ipaddrs);
addIp($ipaddrs, ips[i]);
}
}
};
const addIp = function(ip, $container) {
const addIp = function($container, ip) {
const href_label = manage_url_template.replace('%ipaddr%', '<span>'+ip+'</span>');
const href = manage_url_template.replace('%ipaddr%', ip);
const link = '<a href="' + href + '" target="_blank">' + href_label + '</a>';
addLink($container, href_label, href, 'ip');
};
const addLink = function($container, href_label, href, classname) {
const link = '<a href="' + href + '" target="_blank" class="' + classname + '">' + href_label + '</a>';
const $ipaddr = document.createElement('li');
Object.assign($ipaddr, { className: 'ipaddr'});
$ipaddr.innerHTML = link;
$container.appendChild($ipaddr);
}
};
setIps(interfaces);
</script>

View File

@ -95,6 +95,11 @@
<i class="fa fa-warning icon-left"></i>
{{ l.slideshow_content_referenced_in_slide_error }}
</div>
{% elif request.args.get('error') %}
<div class="alert alert-danger">
<i class="fa fa-warning icon-left"></i>
{{ t(request.args.get('error')) }}
</div>
{% else %}
<div class="alert alert-danger hidden"></div>
{% endif %}