add fleet mode + split views in controllers

This commit is contained in:
jr-k 2024-02-27 12:45:03 +01:00
parent e51061c984
commit eec10c81dc
30 changed files with 849 additions and 239 deletions

2
.gitignore vendored
View File

@ -5,7 +5,7 @@
out/
data/uploads/*
!data/uploads/reclame.jpg
data/slideshow.json
data/db/slideshow.json
config.py
*.lock
__pycache__/

View File

@ -3,5 +3,6 @@ config = {
"port": 5000, # Application port
"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
"lx_file": '/home/pi/.config/lxsession/LXDE-pi/autostart' # Path to lx autostart file,
"fleet_enabled": False # Enable fleet management view
}

23
data/db/fleet.json Normal file
View File

@ -0,0 +1,23 @@
{
"version": 2,
"keys": [
"address",
"enabled",
"name",
"position"
],
"data": {
"179670684589565125": {
"name": "aaaxx",
"enabled": false,
"position": 0,
"address": "bbbz"
},
"460215553937488565": {
"name": "ff",
"enabled": false,
"position": 1,
"address": "ee"
}
}
}

View File

@ -220,19 +220,19 @@ button.purple:hover {
padding: 10px;
}
.panel td a.slide-sort {
.panel td a.item.sort {
cursor: move;
}
.panel td a.slide-name {
.panel td a.item-name {
color: white;
}
.panel-inactive td a.slide-name {
.panel-inactive td a.item-name {
color: #AAA;
}
.panel td a.slide-name:hover {
.panel td a.item-name:hover {
text-decoration: underline;
}
@ -253,12 +253,12 @@ button.purple:hover {
border-color: #0eef5f;
}
.panel td.actions a.slide-edit:hover {
.panel td.actions a.item-edit:hover {
color: #bc48ff;
border-color: #bc48ff;
}
.panel td.actions a.slide-delete:hover {
.panel td.actions a.item-delete:hover {
color: #ef0e0e;
border-color: #ef0e0e;
}

125
data/www/js/fleet.js Normal file
View File

@ -0,0 +1,125 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-screens');
const $tableInactive = $('table.inactive-screens');
const $modalsRoot = $('.modals');
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
};
const updateTable = function () {
$('table').each(function () {
if ($(this).find('tbody tr.screen-item:visible').length === 0) {
$(this).find('tr.empty-tr').removeClass('hidden');
} else {
$(this).find('tr.empty-tr').addClass('hidden');
}
}).tableDnDUpdate();
updatePositions();
}
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 updatePositions = function (table, row) {
const positions = {};
$('.screen-item').each(function (index) {
positions[getId($(this))] = index;
});
$.ajax({
method: 'POST',
url: '/fleet/screen/position',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify(positions),
});
};
const main = function () {
$("table").tableDnD({
dragHandle: 'td a.screen-sort',
onDrop: updatePositions
});
};
$(document).on('change', 'input[type=checkbox]', function () {
$.ajax({
url: 'fleet/screen/toggle',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this)), enabled: $(this).is(':checked')}),
method: 'POST',
});
const $tr = $(this).parents('tr:eq(0)').remove().clone();
if ($(this).is(':checked')) {
$tableActive.append($tr);
} else {
$tableInactive.append($tr);
}
updateTable();
});
$(document).on('change', '#screen-add-type', function () {
const value = $(this).val();
const inputType = $(this).find('option').filter(function (i, el) {
return $(el).val() === value;
}).data('input');
$('.screen-add-object-input')
.addClass('hidden')
.prop('disabled', true)
.filter('#screen-add-object-input-' + inputType)
.removeClass('hidden')
.prop('disabled', false)
;
});
$(document).on('click', '.modal-close', function () {
hideModal();
});
$(document).on('click', '.screen-add', function () {
showModal('modal-screen-add');
$('.modal-screen-add input:eq(0)').focus().select();
});
$(document).on('click', '.screen-edit', function () {
const screen = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-screen-edit');
$('.modal-screen-edit input:visible:eq(0)').focus().select();
$('#screen-edit-name').val(screen.name);
$('#screen-edit-address').val(screen.address);
$('#screen-edit-id').val(screen.id);
});
$(document).on('click', '.screen-delete', function () {
if (confirm(l.fleet_screen_delete_confirmation)) {
const $tr = $(this).parents('tr:eq(0)');
$tr.remove();
updateTable();
$.ajax({
method: 'DELETE',
url: '/fleet/screen/delete',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this))}),
});
}
});
$(document).keyup(function (e) {
if (e.key === "Escape") {
hideModal();
}
});
main();
});

View File

@ -3,19 +3,19 @@ jQuery(document).ready(function ($) {
const $tableInactive = $('table.inactive-slides');
const $modalsRoot = $('.modals');
const getId = function($el) {
const getId = function ($el) {
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
};
const updateTable = function () {
$('table').each(function () {
$('table').each(function () {
if ($(this).find('tbody tr.slide-item:visible').length === 0) {
$(this).find('tr.empty-tr').removeClass('hidden');
} else {
$(this).find('tr.empty-tr').addClass('hidden');
}
}).tableDnDUpdate();
updatePositions();
updatePositions();
}
const showModal = function (modalClass) {
@ -30,19 +30,19 @@ jQuery(document).ready(function ($) {
const updatePositions = function (table, row) {
const positions = {};
$('.slide-item').each(function(index) {
$('.slide-item').each(function (index) {
positions[getId($(this))] = index;
});
$.ajax({
method: 'POST',
url: '/manage/slide/position',
url: '/slideshow/slide/position',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify(positions),
});
};
const main = function() {
const main = function () {
$("table").tableDnD({
dragHandle: 'td a.slide-sort',
onDrop: updatePositions
@ -51,7 +51,7 @@ jQuery(document).ready(function ($) {
$(document).on('change', 'input[type=checkbox]', function () {
$.ajax({
url: 'manage/slide/toggle',
url: 'slideshow/slide/toggle',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this)), enabled: $(this).is(':checked')}),
method: 'POST',
@ -104,13 +104,13 @@ jQuery(document).ready(function ($) {
});
$(document).on('click', '.slide-delete', function () {
if (confirm(l.manage_slide_delete_confirmation)) {
if (confirm(l.slideshow_slide_delete_confirmation)) {
const $tr = $(this).parents('tr:eq(0)');
$tr.remove();
updateTable();
$.ajax({
method: 'DELETE',
url: '/manage/slide/delete',
url: '/slideshow/slide/delete',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this))}),
});

View File

@ -1,29 +1,47 @@
{
"manage_page_title": "Schedule Overview",
"manage_slide_button_add": "Add a slide",
"manage_slide_panel_active": "Active slides",
"manage_slide_panel_inactive": "Inactive slides",
"manage_slide_panel_empty": "Currently, there are no slides. %link% now.",
"manage_slide_panel_th_name": "Name",
"manage_slide_panel_th_duration": "Duration",
"manage_slide_panel_th_duration_unit": "sec",
"manage_slide_panel_th_enabled": "Enabled",
"manage_slide_panel_th_activity": "Activity",
"manage_slide_form_add_title": "Add Slide",
"manage_slide_form_add_submit": "Add",
"manage_slide_form_edit_title": "Edit Slide",
"manage_slide_form_edit_submit": "Save",
"manage_slide_form_label_name": "Name",
"manage_slide_form_label_location": "Location",
"manage_slide_form_label_type": "Type",
"manage_slide_form_label_type_url": "URL",
"manage_slide_form_label_type_video": "Video",
"manage_slide_form_label_type_picture": "Picture",
"manage_slide_form_label_object": "Object",
"manage_slide_form_label_duration": "Duration",
"manage_slide_form_label_duration_unit": "seconds",
"manage_slide_form_button_cancel": "Cancel",
"js_manage_slide_delete_confirmation": "Are you sure?",
"slideshow_page_title": "Schedule Overview",
"slideshow_slide_button_add": "Add a slide",
"slideshow_slide_panel_active": "Active slides",
"slideshow_slide_panel_inactive": "Inactive slides",
"slideshow_slide_panel_empty": "Currently, there are no slides. %link% now.",
"slideshow_slide_panel_th_name": "Name",
"slideshow_slide_panel_th_duration": "Duration",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_enabled": "Enabled",
"slideshow_slide_panel_th_activity": "Activity",
"slideshow_slide_form_add_title": "Add Slide",
"slideshow_slide_form_add_submit": "Add",
"slideshow_slide_form_edit_title": "Edit Slide",
"slideshow_slide_form_edit_submit": "Save",
"slideshow_slide_form_label_name": "Name",
"slideshow_slide_form_label_location": "Location",
"slideshow_slide_form_label_type": "Type",
"slideshow_slide_form_label_type_url": "URL",
"slideshow_slide_form_label_type_video": "Video",
"slideshow_slide_form_label_type_picture": "Picture",
"slideshow_slide_form_label_object": "Object",
"slideshow_slide_form_label_duration": "Duration",
"slideshow_slide_form_label_duration_unit": "seconds",
"slideshow_slide_form_button_cancel": "Cancel",
"js_slideshow_slide_delete_confirmation": "Are you sure?",
"fleet_page_title": "Devices",
"fleet_screen_button_add": "Add a screen",
"fleet_screen_panel_active": "Active screens",
"fleet_screen_panel_inactive": "Inactive screens",
"fleet_screen_panel_empty": "Currently, there are no screens. %link% now.",
"fleet_screen_panel_th_name": "Name",
"fleet_screen_panel_th_address": "Address",
"fleet_screen_panel_th_enabled": "Enabled",
"fleet_screen_panel_th_activity": "Activity",
"fleet_screen_form_add_title": "Add Screen",
"fleet_screen_form_add_submit": "Add",
"fleet_screen_form_edit_title": "Edit Screen",
"fleet_screen_form_edit_submit": "Save",
"fleet_screen_form_label_name": "Name",
"fleet_screen_form_label_address": "Address",
"fleet_screen_form_button_cancel": "Cancel",
"js_fleet_screen_delete_confirmation": "Are you sure?",
"sysinfo_page_title": "System infos",
"sysinfo_panel_title": "Infos",
@ -31,7 +49,5 @@
"sysinfo_panel_th_value": "Value",
"sysinfo_panel_td_ipaddr": "IP Address",
"settings_page_title": "Settings"
}

View File

@ -1,29 +1,47 @@
{
"manage_page_title": "Vue Planning",
"manage_slide_button_add": "Ajouter une slide",
"manage_slide_panel_active": "Slides actives",
"manage_slide_panel_inactive": "Slides inactives",
"manage_slide_panel_empty": "Actuellement, il n'y a aucune slide. %link% maintenant.",
"manage_slide_panel_th_name": "Nom",
"manage_slide_panel_th_duration": "Durée",
"manage_slide_panel_th_duration_unit": "sec",
"manage_slide_panel_th_enabled": "Activé",
"manage_slide_panel_th_activity": "Options",
"manage_slide_form_add_title": "Ajouter d'une slide",
"manage_slide_form_add_submit": "Ajouter",
"manage_slide_form_edit_title": "Modification d'une slide",
"manage_slide_form_edit_submit": "Enregistrer",
"manage_slide_form_label_name": "Nom",
"manage_slide_form_label_location": "Chemin",
"manage_slide_form_label_type": "Type",
"manage_slide_form_label_type_url": "URL",
"manage_slide_form_label_type_video": "Vidéo",
"manage_slide_form_label_type_picture": "Image",
"manage_slide_form_label_object": "Objet",
"manage_slide_form_label_duration": "Durée",
"manage_slide_form_label_duration_unit": "secondes",
"manage_slide_form_button_cancel": "Annuler",
"js_manage_slide_delete_confirmation": "Êtes-vous sûr ?",
"slideshow_page_title": "Vue Planning",
"slideshow_slide_button_add": "Ajouter une slide",
"slideshow_slide_panel_active": "Slides actives",
"slideshow_slide_panel_inactive": "Slides inactives",
"slideshow_slide_panel_empty": "Actuellement, il n'y a aucune slide. %link% maintenant.",
"slideshow_slide_panel_th_name": "Nom",
"slideshow_slide_panel_th_duration": "Durée",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_enabled": "Activé",
"slideshow_slide_panel_th_activity": "Options",
"slideshow_slide_form_add_title": "Ajouter d'une slide",
"slideshow_slide_form_add_submit": "Ajouter",
"slideshow_slide_form_edit_title": "Modification d'une slide",
"slideshow_slide_form_edit_submit": "Enregistrer",
"slideshow_slide_form_label_name": "Nom",
"slideshow_slide_form_label_location": "Chemin",
"slideshow_slide_form_label_type": "Type",
"slideshow_slide_form_label_type_url": "URL",
"slideshow_slide_form_label_type_video": "Vidéo",
"slideshow_slide_form_label_type_picture": "Image",
"slideshow_slide_form_label_object": "Objet",
"slideshow_slide_form_label_duration": "Durée",
"slideshow_slide_form_label_duration_unit": "secondes",
"slideshow_slide_form_button_cancel": "Annuler",
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
"fleet_page_title": "Appareils",
"fleet_screen_button_add": "Ajouter un écran",
"fleet_screen_panel_active": "Écrans actifs",
"fleet_screen_panel_inactive": "Écrans inactifs",
"fleet_screen_panel_empty": "Actuellement, il n'y a pas d'écrans. %link% maintenant.",
"fleet_screen_panel_th_name": "Nom",
"fleet_screen_panel_th_address": "Adresse",
"fleet_screen_panel_th_enabled": "Activé",
"fleet_screen_panel_th_activity": "Options",
"fleet_screen_form_add_title": "Ajout d'un écran",
"fleet_screen_form_add_submit": "Ajouter",
"fleet_screen_form_edit_title": "Modification d'un écran",
"fleet_screen_form_edit_submit": "Enregistrer",
"fleet_screen_form_label_name": "Nom",
"fleet_screen_form_label_address": "Adresse",
"fleet_screen_form_button_cancel": "Annuler",
"js_fleet_screen_delete_confirmation": "Êtes-vous sûr ?",
"sysinfo_page_title": "Système",
"sysinfo_panel_title": "Informations",

View File

@ -7,18 +7,18 @@ import subprocess
import sys
from enum import Enum
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from werkzeug.utils import secure_filename
from flask import Flask, send_from_directory
from config import config
from src.SlideManager import SlideManager
from src.model.Slide import Slide
from src.model.SlideType import SlideType
from src.utils import str_to_enum
from src.ScreenManager import ScreenManager
from src.controller.PlayerController import PlayerController
from src.controller.SlideshowController import SlideshowController
from src.controller.FleetController import FleetController
from src.controller.SysinfoController import SysinfoController
# <config>
PLAYER_URL = 'http://localhost:{}'.format(config['port'])
screen_manager = ScreenManager()
slide_manager = SlideManager()
with open('./lang/{}.json'.format(config['lang']), 'r') as file:
LANGDICT = json.load(file)
@ -69,110 +69,21 @@ if config['lx_file']:
# </xenv>
# <utils>
def get_ip_address():
try:
result = subprocess.run(
["ip", "-4", "route", "get", "8.8.8.8"],
capture_output=True,
text=True
)
ip_address = result.stdout.split()[6]
return ip_address
except Exception as e:
print(f"Error obtaining IP address: {e}")
return 'Unknown'
# </utils>
# <web>
@app.context_processor
def inject_global_vars():
return dict(
FLEET_MODE=config['fleet_enabled'],
LANG=config['lang'],
STATIC_PREFIX='/data/www/'
)
@app.route('/')
def index():
return render_template('player/player.jinja.html', items=json.dumps(slide_manager.to_dict(slide_manager.get_enabled_slides())))
@app.route('/playlist')
def playlist():
return jsonify(slide_manager.to_dict(slide_manager.get_enabled_slides()))
PlayerController(app, LANGDICT, slide_manager)
SlideshowController(app, LANGDICT, slide_manager)
FleetController(app, LANGDICT, screen_manager)
SysinfoController(app, LANGDICT)
@app.route('/slide/default')
def slide_default():
return render_template('player/default.jinja.html', ipaddr=get_ip_address())
@app.route('/manage')
def manage():
return render_template(
'manager/manage.jinja.html',
l=LANGDICT,
enabled_slides=slide_manager.get_enabled_slides(),
disabled_slides=slide_manager.get_disabled_slides(),
)
@app.route('/manage/sysinfo')
def manage_sysinfo():
return render_template(
'manager/sysinfo.jinja.html',
ipaddr=get_ip_address(),
l=LANGDICT,
)
@app.route('/manage/slide/add', methods=['GET', 'POST'])
def manage_slide_add():
slide = Slide(
name=request.form['name'],
type=str_to_enum(request.form['type'], SlideType),
duration=request.form['duration'],
)
if slide.has_file():
if 'object' not in request.files:
return redirect(request.url)
object = request.files['object']
if object.filename == '':
return redirect(request.url)
if object:
object_name = secure_filename(object.filename)
object_path = os.path.join(app.config['UPLOAD_FOLDER'], object_name)
object.save(object_path)
slide.location = object_path
else:
slide.location = request.form['object']
slide_manager.add_form(slide)
return redirect(url_for('manage'))
@app.route('/manage/slide/edit', methods=['POST'])
def manage_slide_edit():
slide_manager.update_form(request.form['id'], request.form['name'], request.form['duration'])
return redirect(url_for('manage'))
@app.route('/manage/slide/toggle', methods=['POST'])
def manage_slide_toggle():
data = request.get_json()
slide_manager.update_enabled(data.get('id'), data.get('enabled'))
return jsonify({'status': 'ok'})
@app.route('/manage/slide/delete', methods=['DELETE'])
def manage_slide_delete():
data = request.get_json()
slide_manager.delete(data.get('id'))
return jsonify({'status': 'ok'})
@app.route('/manage/slide/position', methods=['POST'])
def manage_slide_position():
data = request.get_json()
slide_manager.update_positions(data)
return jsonify({'status': 'ok'})
@app.errorhandler(404)
def not_found(e):

66
src/ScreenManager.py Normal file
View File

@ -0,0 +1,66 @@
from typing import Dict, Optional, List, Tuple, Union
from src.model.Screen import Screen
from pysondb import PysonDB
class ScreenManager:
DB_FILE = "data/db/fleet.json"
def __init__(self):
self._db = PysonDB(self.DB_FILE)
@staticmethod
def hydrate_object(raw_screen: dict, id: Optional[str] = None) -> Screen:
if id:
raw_screen['id'] = id
return Screen(**raw_screen)
@staticmethod
def hydrate_dict(raw_screens: dict) -> List[Screen]:
return [ScreenManager.hydrate_object(raw_screen, raw_id) for raw_id, raw_screen in raw_screens.items()]
@staticmethod
def hydrate_list(raw_screens: list) -> List[Screen]:
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)
def get_all(self, sort: bool = False) -> List[Screen]:
raw_screens = self._db.get_all()
if isinstance(raw_screens, dict):
if sort:
return sorted(ScreenManager.hydrate_dict(raw_screens), key=lambda x: x.position)
return ScreenManager.hydrate_dict(raw_screens)
return ScreenManager.hydrate_list(sorted(raw_screens, key=lambda x: x['position']) if sort else raw_screens)
def get_enabled_screens(self) -> List[Screen]:
return [screen for screen in self.get_all(sort=True) if screen.enabled]
def get_disabled_screens(self) -> List[Screen]:
return [screen for screen in self.get_all(sort=True) if not screen.enabled]
def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, {"enabled": enabled, "position": 999})
def update_positions(self, positions: list) -> None:
for screen_id, screen_position in positions.items():
self._db.update_by_id(screen_id, {"position": screen_position})
def update_form(self, id: str, name: str, address: int) -> None:
self._db.update_by_id(id, {"name": name, "address": address})
def add_form(self, screen: Screen) -> None:
db_screen = screen.to_dict()
del db_screen['id']
self._db.add(db_screen)
def delete(self, id: str) -> None:
self._db.delete_by_id(id)
def to_dict(self, screens: List[Screen]) -> dict:
return [screen.to_dict() for screen in screens]

View File

@ -1,4 +1,4 @@
import json
import os
from typing import Dict, Optional, List, Tuple, Union
from src.model.Slide import Slide
@ -6,15 +6,15 @@ from src.utils import str_to_enum
from pysondb import PysonDB
class SlideManager():
class SlideManager:
DB_FILE = "data/slideshow.json"
DB_FILE = "data/db/slideshow.json"
def __init__(self):
self._db = PysonDB(self.DB_FILE)
@staticmethod
def hydrate_object(raw_slide: dict, id: Union[int, str] = None) -> Slide:
def hydrate_object(raw_slide: dict, id: str = None) -> Slide:
if id:
raw_slide['id'] = id
@ -28,6 +28,9 @@ class SlideManager():
def hydrate_list(raw_slides: list) -> List[Slide]:
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)
def get_all(self, sort: bool = False) -> List[Slide]:
raw_slides = self._db.get_all()
@ -44,7 +47,7 @@ class SlideManager():
def get_disabled_slides(self) -> List[Slide]:
return [slide for slide in self.get_all(sort=True) if not slide.enabled]
def update_enabled(self, id: int, enabled: bool) -> None:
def update_enabled(self, id: str, enabled: bool) -> None:
self._db.update_by_id(id, {"enabled": enabled, "position": 999})
def update_positions(self, positions: list) -> None:
@ -59,8 +62,13 @@ class SlideManager():
del db_slide['id']
self._db.add(db_slide)
def delete(self, id: int) -> None:
self._db.delete_by_id(id)
def delete(self, id: str) -> None:
slide = self.get(id)
if slide:
if slide.has_file():
os.unlink(slide.location)
self._db.delete_by_id(id)
def to_dict(self, slides: List[Slide]) -> dict:
return [slide.to_dict() for slide in slides]

View File

@ -0,0 +1,55 @@
import json
from flask import Flask, render_template, redirect, request, url_for, jsonify
from src.model.Screen import Screen
class FleetController:
def __init__(self, app, l, screen_manager):
self._app = app
self._l = l
self._screen_manager = screen_manager
self.register()
def register(self):
self._app.add_url_rule('/fleet', 'fleet', self.fleet, methods=['GET'])
self._app.add_url_rule('/fleet/screen/add', 'fleet_screen_add', self.fleet_screen_add, methods=['POST'])
self._app.add_url_rule('/fleet/screen/edit', 'fleet_screen_edit', self.fleet_screen_edit, methods=['POST'])
self._app.add_url_rule('/fleet/screen/toggle', 'fleet_screen_toggle', self.fleet_screen_toggle, methods=['POST'])
self._app.add_url_rule('/fleet/screen/delete', 'fleet_screen_delete', self.fleet_screen_delete, methods=['DELETE'])
self._app.add_url_rule('/fleet/screen/position', 'fleet_screen_position', self.fleet_screen_position, methods=['POST'])
def fleet(self):
return render_template(
'fleet/fleet.jinja.html',
l=self._l,
enabled_screens=self._screen_manager.get_enabled_screens(),
disabled_screens=self._screen_manager.get_disabled_screens(),
)
def fleet_screen_add(self):
self._screen_manager.add_form(Screen(
name=request.form['name'],
address=request.form['address'],
))
return redirect(url_for('fleet'))
def fleet_screen_edit(self):
self._screen_manager.update_form(request.form['id'], request.form['name'], request.form['address'])
return redirect(url_for('fleet'))
def fleet_screen_toggle(self):
data = request.get_json()
self._screen_manager.update_enabled(data.get('id'), data.get('enabled'))
return jsonify({'status': 'ok'})
def fleet_screen_delete(self):
data = request.get_json()
self._screen_manager.delete(data.get('id'))
return jsonify({'status': 'ok'})
def fleet_screen_position(self):
data = request.get_json()
self._screen_manager.update_positions(data)
return jsonify({'status': 'ok'})

View File

@ -0,0 +1,30 @@
import json
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from src.utils import get_ip_address
class PlayerController:
def __init__(self, app, l, slide_manager):
self._app = app
self._l = l
self._slide_manager = slide_manager
self.register()
def register(self):
self._app.add_url_rule('/', 'player', self.player, methods=['GET'])
self._app.add_url_rule('/player/default', 'player_default', self.player_default, methods=['GET'])
self._app.add_url_rule('/player/playlist', 'player_playlist', self.player_playlist, methods=['GET'])
def player(self):
return render_template(
'player/player.jinja.html',
items=json.dumps(self._slide_manager.to_dict(self._slide_manager.get_enabled_slides()))
)
def player_default(self):
return render_template('player/default.jinja.html', ipaddr=get_ip_address())
def player_playlist(self):
return jsonify(self._slide_manager.to_dict(self._slide_manager.get_enabled_slides()))

View File

@ -0,0 +1,80 @@
import json
import os
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from werkzeug.utils import secure_filename
from src.model.Slide import Slide
from src.model.SlideType import SlideType
from src.utils import str_to_enum
class SlideshowController:
def __init__(self, app, l, slide_manager):
self._app = app
self._l = l
self._slide_manager = slide_manager
self.register()
def register(self):
self._app.add_url_rule('/slideshow', 'slideshow', self.slideshow, methods=['GET'])
self._app.add_url_rule('/slideshow/slide/add', 'slideshow_slide_add', self.slideshow_slide_add, methods=['POST'])
self._app.add_url_rule('/slideshow/slide/edit', 'slideshow_slide_edit', self.slideshow_slide_edit, methods=['POST'])
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'])
def slideshow(self):
return render_template(
'slideshow/slideshow.jinja.html',
l=self._l,
enabled_slides=self._slide_manager.get_enabled_slides(),
disabled_slides=self._slide_manager.get_disabled_slides(),
)
def slideshow_slide_add(self):
slide = Slide(
name=request.form['name'],
type=str_to_enum(request.form['type'], SlideType),
duration=request.form['duration'],
)
if slide.has_file():
if 'object' not in request.files:
return redirect(request.url)
object = request.files['object']
if object.filename == '':
return redirect(request.url)
if object:
object_name = secure_filename(object.filename)
object_path = os.path.join(self._app.config['UPLOAD_FOLDER'], object_name)
object.save(object_path)
slide.location = object_path
else:
slide.location = request.form['object']
self._slide_manager.add_form(slide)
return redirect(url_for('slideshow'))
def slideshow_slide_edit(self):
self._slide_manager.update_form(request.form['id'], request.form['name'], request.form['duration'])
return redirect(url_for('slideshow'))
def slideshow_slide_toggle(self):
data = request.get_json()
self._slide_manager.update_enabled(data.get('id'), data.get('enabled'))
return jsonify({'status': 'ok'})
def slideshow_slide_delete(self):
data = request.get_json()
self._slide_manager.delete(data.get('id'))
return jsonify({'status': 'ok'})
def slideshow_slide_position(self):
data = request.get_json()
self._slide_manager.update_positions(data)
return jsonify({'status': 'ok'})

View File

@ -0,0 +1,22 @@
import json
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from src.utils import get_ip_address
class SysinfoController:
def __init__(self, app, l):
self._app = app
self._l = l
self.register()
def register(self):
self._app.add_url_rule('/sysinfo', 'sysinfo', self.sysinfo, methods=['GET'])
def sysinfo(self):
return render_template(
'sysinfo/sysinfo.jinja.html',
ipaddr=get_ip_address(),
l=self._l,
)

70
src/model/Screen.py Normal file
View File

@ -0,0 +1,70 @@
import json
from typing import Optional, Union
class Screen:
def __init__(self, address: str = '', enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None):
self._id = id if id else None
self._address = address
self._enabled = enabled
self._name = name
self._position = position
@property
def id(self) -> Union[int, str]:
return self._id
@property
def address(self) -> str:
return self._address
@address.setter
def address(self, value: str):
self._address = value
@property
def enabled(self) -> bool:
return self._enabled
@enabled.setter
def enabled(self, value: bool):
self._enabled = value
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
self._name = value
@property
def position(self) -> int:
return self._position
@position.setter
def position(self, value: int):
self._position = value
def __str__(self) -> str:
return f"Slide(" \
f"id='{self.id}',\n" \
f"name='{self.name}',\n" \
f"enabled='{self.enabled}',\n" \
f"position='{self.position}',\n" \
f"address='{self.address}',\n" \
f")"
def to_json(self) -> str:
return json.dumps(self.to_dict())
def to_dict(self) -> dict:
return {
"name": self.name,
"id": self.id,
"enabled": self.enabled,
"position": self.position,
"address": self.address,
}

View File

@ -7,10 +7,8 @@ from src.utils import str_to_enum
class Slide:
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: Union[int, str] = 999, id: Optional[int] = None):
if id:
self._id = id
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None):
self._id = id if id else None
self._location = location
self._duration = duration
self._type = str_to_enum(type, SlideType) if isinstance(type, str) else type
@ -19,7 +17,7 @@ class Slide:
self._position = position
@property
def id(self) -> Union[int, str]:
def id(self) -> Optional[str]:
return self._id
@property

View File

@ -4,3 +4,16 @@ def str_to_enum(str_val: str, enum_class):
if enum_item.value == str_val:
return enum_item
raise ValueError(f"{str_val} is not a valid {enum_class.__name__} item")
def get_ip_address():
try:
result = subprocess.run(
["ip", "-4", "route", "get", "8.8.8.8"],
capture_output=True,
text=True
)
ip_address = result.stdout.split()[6]
return ip_address
except Exception as e:
print(f"Error obtaining IP address: {e}")
return 'Unknown'

View File

@ -24,18 +24,25 @@
</h1>
<menu>
<ul>
<li class="{{ 'active' if request.url_rule.endpoint == 'manage' }}">
<a href="{{ url_for('manage') }}">
<i class="fa-regular fa-clock"></i> {{ l.manage_page_title }}
<li class="{{ 'active' if request.url_rule.endpoint == 'slideshow' }}">
<a href="{{ url_for('slideshow') }}">
<i class="fa-regular fa-clock"></i> {{ l.slideshow_page_title }}
</a>
</li>
{# <li class="{{ 'active' if request.url_rule.endpoint == 'manage_settings' }}">#}
{% if FLEET_MODE %}
<li class="{{ 'active' if request.url_rule.endpoint == 'fleet' }}">
<a href="{{ url_for('fleet') }}">
<i class="fa fa-tv"></i> {{ l.fleet_page_title }}
</a>
</li>
{% endif %}
{# <li class="{{ 'active' if request.url_rule.endpoint == 'settings' }}">#}
{# <a href="#">#}
{# <i class="fa-solid fa-gear"></i> {{ l.settings_page_title }}#}
{# </a>#}
{# </li>#}
<li class="{{ 'active' if request.url_rule.endpoint == 'manage_sysinfo' }}">
<a href="{{ url_for('manage_sysinfo') }}">
<li class="{{ 'active' if request.url_rule.endpoint == 'sysinfo' }}">
<a href="{{ url_for('sysinfo') }}">
<i class="fa-solid fa-list-check"></i> {{ l.sysinfo_page_title }}
</a>
</li>
@ -54,7 +61,8 @@
{% endblock %}
</div>
<script>
var l = {'js_manage_slide_delete_confirmation': '{{ l.manage_slide_delete_confirmation }}'};
var l = {'js_slideshow_slide_delete_confirmation': '{{ l.slideshow_slide_delete_confirmation }}'};
var l = {'js_fleet_screen_delete_confirmation': '{{ l.js_fleet_screen_delete_confirmation }}'};
</script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
{% block add_js %}{% endblock %}

View File

@ -0,0 +1,52 @@
<table class="{{ tclass }}-screens">
<thead>
<tr>
<th>{{ l.fleet_screen_panel_th_name }}</th>
<th class="tac">{{ l.fleet_screen_panel_th_address }}</th>
<th class="tac">{{ l.fleet_screen_panel_th_enabled }}</th>
<th class="tac">{{ l.fleet_screen_panel_th_activity }}</th>
</tr>
</thead>
<tbody>
<tr class="empty-tr {% if screens|length != 0 %}hidden{% endif %}">
<td colspan="4">
{{ l.fleet_screen_panel_empty|replace(
'%link%',
('<a href="javascript:void(0);" class="item-add">'~l.fleet_screen_button_add~'</a>')|safe
) }}
</td>
</tr>
{% for screen in screens %}
<tr class="screen-item" data-level="{{ screen.id }}" data-entity="{{ screen.to_json() }}">
<td class="infos">
<div class="inner">
<a href="javascript:void(0);" class="item-sort screen-sort">
<i class="fa fa-sort icon-left"></i>
</a>
<i class="fa fa-tv icon-left"></i>
{{ screen.name }}
</div>
</td>
<td class="tac">
{{ screen.address }}
</td>
<td class="tac">
<label class="pure-material-switch">
<input type="checkbox" {% if screen.enabled %}checked="checked"{% endif %}><span></span>
</label>
</td>
<td class="actions tac">
<a href="javascript:void(0);" class="item-edit screen-edit">
<i class="fa fa-pencil"></i>
</a>
<a href="{{ screen.address }}" class="item-download screen-download" target="_blank">
<i class="fa fa-eye"></i>
</a>
<a href="javascript:void(0);" class="item-delete screen-delete">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,52 @@
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.fleet_page_title }}
{% endblock %}
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/tablednd-fixed.js"></script>
<script src="{{ STATIC_PREFIX }}js/fleet.js"></script>
{% endblock %}
{% block page %}
<div class="toolbar">
<h2>{{ l.fleet_page_title }}</h2>
<div class="toolbar-actions">
<button class="purple screen-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.fleet_screen_button_add }}</button>
</div>
</div>
<div class="panel">
<div class="panel-body">
<h3>{{ l.fleet_screen_panel_active }}</h3>
{% with tclass='active', screens=enabled_screens %}
{% include 'fleet/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="panel panel-inactive">
<div class="panel-body">
<h3>{{ l.fleet_screen_panel_inactive }}</h3>
{% with tclass='inactive', screens=disabled_screens %}
{% include 'fleet/component/table.jinja.html' %}
{% endwith %}
</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 'fleet/modal/add.jinja.html' %}
{% include 'fleet/modal/edit.jinja.html' %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
<div class="modal modal-screen-add">
<h2>
{{ l.fleet_screen_form_add_title }}
</h2>
<form action="/fleet/screen/add" method="POST" enctype="multipart/form-data">
<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"/>
</div>
</div>
<div class="form-group">
<label for="screen-add-address">{{ l.fleet_screen_form_label_address }}</label>
<div class="widget">
<input type="text" name="address" id="screen-add-address" required="required"/>
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.fleet_screen_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-plus icon-left"></i> {{ l.fleet_screen_form_add_submit }}
</button>
</div>
</form>
</div>

View File

@ -0,0 +1,32 @@
<div class="modal modal-screen-edit hidden">
<h2>
{{ l.fleet_screen_form_edit_submit }}
</h2>
<form action="/fleet/screen/edit" method="POST">
<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"/>
</div>
</div>
<div class="form-group">
<label for="screen-edit-address">{{ l.fleet_screen_form_label_address }}</label>
<div class="widget">
<input type="text" name="address" id="screen-edit-address" required="required"/>
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.fleet_screen_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-save icon-left"></i>{{ l.fleet_screen_form_edit_submit }}
</button>
</div>
</form>
</div>

View File

@ -14,10 +14,10 @@
</head>
<body>
<div id="FirstSlide" class="slide" style="visibility: hidden;">
<iframe src="/slide/default"></iframe>
<iframe src="/player/default"></iframe>
</div>
<div id="SecondSlide" style="visibility: visible;">
<iframe src="/slide/default"></iframe>
<iframe src="/player/default"></iframe>
</div>
<script type="text/javascript">
var items = {{items | safe}};
@ -26,7 +26,7 @@
var curUrl = 0;
var nextReady = true;
var itemCheck = setInterval(function () {
fetch('playlist').then(function(response) {
fetch('player/playlist').then(function(response) {
if (response.ok) {
return response.json();
}

View File

@ -1,18 +1,18 @@
<table class="{{ tclass }}-slides">
<thead>
<tr>
<th>{{ l.manage_slide_panel_th_name }}</th>
<th class="tac">{{ l.manage_slide_panel_th_duration }}</th>
<th class="tac">{{ l.manage_slide_panel_th_enabled }}</th>
<th class="tac">{{ l.manage_slide_panel_th_activity }}</th>
<th>{{ l.slideshow_slide_panel_th_name }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_duration }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_enabled }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_activity }}</th>
</tr>
</thead>
<tbody>
<tr class="empty-tr {% if slides|length != 0 %}hidden{% endif %}">
<td colspan="4">
{{ l.manage_slide_panel_empty|replace(
{{ l.slideshow_slide_panel_empty|replace(
'%link%',
('<a href="javascript:void(0);" class="slide-add">'~l.manage_slide_button_add~'</a>')|safe
('<a href="javascript:void(0);" class="item-add">'~l.slideshow_slide_button_add~'</a>')|safe
) }}
</td>
</tr>
@ -20,7 +20,7 @@
<tr class="slide-item" data-level="{{ slide.id }}" data-entity="{{ slide.to_json() }}">
<td class="infos">
<div class="inner">
<a href="javascript:void(0);" class="slide-sort">
<a href="javascript:void(0);" class="item-sort slide-sort">
<i class="fa fa-sort icon-left"></i>
</a>
{% set icon_type = 'globe' %}
@ -35,7 +35,7 @@
</div>
</td>
<td class="tac">
{{ slide.duration }} {{ l.manage_slide_panel_th_duration_unit }}
{{ slide.duration }} {{ l.slideshow_slide_panel_th_duration_unit }}
</td>
<td class="tac">
<label class="pure-material-switch">
@ -43,13 +43,13 @@
</label>
</td>
<td class="actions tac">
<a href="javascript:void(0);" class="slide-edit">
<a href="javascript:void(0);" class="item-edit slide-edit">
<i class="fa fa-pencil"></i>
</a>
<a href="{{ slide.location }}" class="slide-download" target="_blank">
<a href="{{ slide.location }}" class="item-download slide-download" target="_blank">
<i class="fa fa-eye"></i>
</a>
<a href="javascript:void(0);" class="slide-delete">
<a href="javascript:void(0);" class="item-delete slide-delete">
<i class="fa fa-trash"></i>
</a>
</td>

View File

@ -1,28 +1,28 @@
<div class="modal modal-slide-add">
<h2>
{{ l.manage_slide_form_add_title }}
{{ l.slideshow_slide_form_add_title }}
</h2>
<form action="/manage/slide/add" method="POST" enctype="multipart/form-data">
<form action="/slideshow/slide/add" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="slide-add-name">{{ l.manage_slide_form_label_name }}</label>
<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"/>
</div>
</div>
<div class="form-group">
<label for="slide-add-type">{{ l.manage_slide_form_label_type }}</label>
<label for="slide-add-type">{{ l.slideshow_slide_form_label_type }}</label>
<div class="widget">
<select name="type" id="slide-add-type">
<option value="url" data-input="text">{{ l.manage_slide_form_label_type_url }}</option>
<option value="video" data-input="upload">{{ l.manage_slide_form_label_type_video }}</option>
<option value="picture" data-input="upload">{{ l.manage_slide_form_label_type_picture }}</option>
<option value="url" data-input="text">{{ l.slideshow_slide_form_label_type_url }}</option>
<option value="video" data-input="upload">{{ l.slideshow_slide_form_label_type_video }}</option>
<option value="picture" data-input="upload">{{ l.slideshow_slide_form_label_type_picture }}</option>
</select>
</div>
</div>
<div class="form-group object-input">
<label for="slide-add-duration">{{ l.manage_slide_form_label_object }}</label>
<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="file" name="object" id="slide-add-object-input-upload"
@ -31,19 +31,19 @@
</div>
<div class="form-group">
<label for="slide-add-duration">{{ l.manage_slide_form_label_duration }}</label>
<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"/>
<span>{{ l.manage_slide_form_label_duration_unit }}</span>
<span>{{ l.slideshow_slide_form_label_duration_unit }}</span>
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.manage_slide_form_button_cancel }}
{{ l.slideshow_slide_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-plus icon-left"></i> {{ l.manage_slide_form_add_submit }}
<i class="fa fa-plus icon-left"></i> {{ l.slideshow_slide_form_add_submit }}
</button>
</div>
</form>

View File

@ -1,49 +1,49 @@
<div class="modal modal-slide-edit hidden">
<h2>
{{ l.manage_slide_form_edit_submit }}
{{ l.slideshow_slide_form_edit_submit }}
</h2>
<form action="/manage/slide/edit" method="POST">
<form action="/slideshow/slide/edit" method="POST">
<input type="hidden" name="id" id="slide-edit-id"/>
<div class="form-group">
<label for="slide-edit-name">{{ l.manage_slide_form_label_name }}</label>
<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"/>
</div>
</div>
<div class="form-group">
<label for="slide-edit-type">{{ l.manage_slide_form_label_type }}</label>
<label for="slide-edit-type">{{ l.slideshow_slide_form_label_type }}</label>
<div class="widget">
<select id="slide-edit-type" name="type" disabled="disabled">
<option value="url" data-input="text">{{ l.manage_slide_form_label_type_url }}</option>
<option value="video" data-input="upload">{{ l.manage_slide_form_label_type_video }}</option>
<option value="picture" data-input="upload">{{ l.manage_slide_form_label_type_picture }}</option>
<option value="url" data-input="text">{{ l.slideshow_slide_form_label_type_url }}</option>
<option value="video" data-input="upload">{{ l.slideshow_slide_form_label_type_video }}</option>
<option value="picture" data-input="upload">{{ l.slideshow_slide_form_label_type_picture }}</option>
</select>
</div>
</div>
<div class="form-group">
<label for="slide-edit-location">{{ l.manage_slide_form_label_location }}</label>
<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"/>
</div>
</div>
<div class="form-group">
<label for="slide-edit-duration">{{ l.manage_slide_form_label_duration }}</label>
<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"/>
<span>{{ l.manage_slide_form_label_duration_unit }}</span>
<span>{{ l.slideshow_slide_form_label_duration_unit }}</span>
</div>
</div>
<div class="actions">
<button type="button" class="modal-close">
{{ l.manage_slide_form_button_cancel }}
{{ l.slideshow_slide_form_button_cancel }}
</button>
<button type="submit" class="green">
<i class="fa fa-save icon-left"></i>{{ l.manage_slide_form_edit_submit }}
<i class="fa fa-save icon-left"></i>{{ l.slideshow_slide_form_edit_submit }}
</button>
</div>
</form>

View File

@ -1,38 +1,38 @@
{% extends 'manager/base.jinja.html' %}
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.manage_page_title }}
{{ l.slideshow_page_title }}
{% endblock %}
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/tablednd-fixed.js"></script>
<script src="{{ STATIC_PREFIX }}js/manage.js"></script>
<script src="{{ STATIC_PREFIX }}js/slideshow.js"></script>
{% endblock %}
{% block page %}
<div class="toolbar">
<h2>{{ l.manage_page_title }}</h2>
<h2>{{ l.slideshow_page_title }}</h2>
<div class="toolbar-actions">
<button class="purple slide-add"><i class="fa fa-plus icon-left"></i>{{ l.manage_slide_button_add }}</button>
<button class="purple slide-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.slideshow_slide_button_add }}</button>
</div>
</div>
<div class="panel">
<div class="panel-body">
<h3>{{ l.manage_slide_panel_active }}</h3>
<h3>{{ l.slideshow_slide_panel_active }}</h3>
{% with tclass='active', slides=enabled_slides %}
{% include 'manager/manage-table.jinja.html' %}
{% include 'slideshow/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="panel panel-inactive">
<div class="panel-body">
<h3>{{ l.manage_slide_panel_inactive }}</h3>
<h3>{{ l.slideshow_slide_panel_inactive }}</h3>
{% with tclass='inactive', slides=disabled_slides %}
{% include 'manager/manage-table.jinja.html' %}
{% include 'slideshow/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
@ -44,8 +44,8 @@
<i class="fa fa-close"></i>
</a>
<div class="modals-inner">
{% include 'manager/modal/slide-add.jinja.html' %}
{% include 'manager/modal/slide-edit.jinja.html' %}
{% include 'slideshow/modal/add.jinja.html' %}
{% include 'slideshow/modal/edit.jinja.html' %}
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
{% extends 'manager/base.jinja.html' %}
{% extends 'base.jinja.html' %}
{% block page_title %}