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/ out/
data/uploads/* data/uploads/*
!data/uploads/reclame.jpg !data/uploads/reclame.jpg
data/slideshow.json data/db/slideshow.json
config.py config.py
*.lock *.lock
__pycache__/ __pycache__/

View File

@ -3,5 +3,6 @@ config = {
"port": 5000, # Application port "port": 5000, # Application port
"reverse_proxy_mode": False, # True if you want to use nginx on port 80 "reverse_proxy_mode": False, # True if you want to use nginx on port 80
"lang": "en", # Language for manage view "fr" or "en" "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; padding: 10px;
} }
.panel td a.slide-sort { .panel td a.item.sort {
cursor: move; cursor: move;
} }
.panel td a.slide-name { .panel td a.item-name {
color: white; color: white;
} }
.panel-inactive td a.slide-name { .panel-inactive td a.item-name {
color: #AAA; color: #AAA;
} }
.panel td a.slide-name:hover { .panel td a.item-name:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -253,12 +253,12 @@ button.purple:hover {
border-color: #0eef5f; border-color: #0eef5f;
} }
.panel td.actions a.slide-edit:hover { .panel td.actions a.item-edit:hover {
color: #bc48ff; color: #bc48ff;
border-color: #bc48ff; border-color: #bc48ff;
} }
.panel td.actions a.slide-delete:hover { .panel td.actions a.item-delete:hover {
color: #ef0e0e; color: #ef0e0e;
border-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,7 +3,7 @@ jQuery(document).ready(function ($) {
const $tableInactive = $('table.inactive-slides'); const $tableInactive = $('table.inactive-slides');
const $modalsRoot = $('.modals'); 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'); return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
}; };
@ -30,19 +30,19 @@ jQuery(document).ready(function ($) {
const updatePositions = function (table, row) { const updatePositions = function (table, row) {
const positions = {}; const positions = {};
$('.slide-item').each(function(index) { $('.slide-item').each(function (index) {
positions[getId($(this))] = index; positions[getId($(this))] = index;
}); });
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: '/manage/slide/position', url: '/slideshow/slide/position',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
data: JSON.stringify(positions), data: JSON.stringify(positions),
}); });
}; };
const main = function() { const main = function () {
$("table").tableDnD({ $("table").tableDnD({
dragHandle: 'td a.slide-sort', dragHandle: 'td a.slide-sort',
onDrop: updatePositions onDrop: updatePositions
@ -51,7 +51,7 @@ jQuery(document).ready(function ($) {
$(document).on('change', 'input[type=checkbox]', function () { $(document).on('change', 'input[type=checkbox]', function () {
$.ajax({ $.ajax({
url: 'manage/slide/toggle', url: 'slideshow/slide/toggle',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this)), enabled: $(this).is(':checked')}), data: JSON.stringify({id: getId($(this)), enabled: $(this).is(':checked')}),
method: 'POST', method: 'POST',
@ -104,13 +104,13 @@ jQuery(document).ready(function ($) {
}); });
$(document).on('click', '.slide-delete', 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)'); const $tr = $(this).parents('tr:eq(0)');
$tr.remove(); $tr.remove();
updateTable(); updateTable();
$.ajax({ $.ajax({
method: 'DELETE', method: 'DELETE',
url: '/manage/slide/delete', url: '/slideshow/slide/delete',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
data: JSON.stringify({id: getId($(this))}), data: JSON.stringify({id: getId($(this))}),
}); });

View File

@ -1,29 +1,47 @@
{ {
"manage_page_title": "Schedule Overview", "slideshow_page_title": "Schedule Overview",
"manage_slide_button_add": "Add a slide", "slideshow_slide_button_add": "Add a slide",
"manage_slide_panel_active": "Active slides", "slideshow_slide_panel_active": "Active slides",
"manage_slide_panel_inactive": "Inactive slides", "slideshow_slide_panel_inactive": "Inactive slides",
"manage_slide_panel_empty": "Currently, there are no slides. %link% now.", "slideshow_slide_panel_empty": "Currently, there are no slides. %link% now.",
"manage_slide_panel_th_name": "Name", "slideshow_slide_panel_th_name": "Name",
"manage_slide_panel_th_duration": "Duration", "slideshow_slide_panel_th_duration": "Duration",
"manage_slide_panel_th_duration_unit": "sec", "slideshow_slide_panel_th_duration_unit": "sec",
"manage_slide_panel_th_enabled": "Enabled", "slideshow_slide_panel_th_enabled": "Enabled",
"manage_slide_panel_th_activity": "Activity", "slideshow_slide_panel_th_activity": "Activity",
"manage_slide_form_add_title": "Add Slide", "slideshow_slide_form_add_title": "Add Slide",
"manage_slide_form_add_submit": "Add", "slideshow_slide_form_add_submit": "Add",
"manage_slide_form_edit_title": "Edit Slide", "slideshow_slide_form_edit_title": "Edit Slide",
"manage_slide_form_edit_submit": "Save", "slideshow_slide_form_edit_submit": "Save",
"manage_slide_form_label_name": "Name", "slideshow_slide_form_label_name": "Name",
"manage_slide_form_label_location": "Location", "slideshow_slide_form_label_location": "Location",
"manage_slide_form_label_type": "Type", "slideshow_slide_form_label_type": "Type",
"manage_slide_form_label_type_url": "URL", "slideshow_slide_form_label_type_url": "URL",
"manage_slide_form_label_type_video": "Video", "slideshow_slide_form_label_type_video": "Video",
"manage_slide_form_label_type_picture": "Picture", "slideshow_slide_form_label_type_picture": "Picture",
"manage_slide_form_label_object": "Object", "slideshow_slide_form_label_object": "Object",
"manage_slide_form_label_duration": "Duration", "slideshow_slide_form_label_duration": "Duration",
"manage_slide_form_label_duration_unit": "seconds", "slideshow_slide_form_label_duration_unit": "seconds",
"manage_slide_form_button_cancel": "Cancel", "slideshow_slide_form_button_cancel": "Cancel",
"js_manage_slide_delete_confirmation": "Are you sure?", "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_page_title": "System infos",
"sysinfo_panel_title": "Infos", "sysinfo_panel_title": "Infos",
@ -31,7 +49,5 @@
"sysinfo_panel_th_value": "Value", "sysinfo_panel_th_value": "Value",
"sysinfo_panel_td_ipaddr": "IP Address", "sysinfo_panel_td_ipaddr": "IP Address",
"settings_page_title": "Settings" "settings_page_title": "Settings"
} }

View File

@ -1,29 +1,47 @@
{ {
"manage_page_title": "Vue Planning", "slideshow_page_title": "Vue Planning",
"manage_slide_button_add": "Ajouter une slide", "slideshow_slide_button_add": "Ajouter une slide",
"manage_slide_panel_active": "Slides actives", "slideshow_slide_panel_active": "Slides actives",
"manage_slide_panel_inactive": "Slides inactives", "slideshow_slide_panel_inactive": "Slides inactives",
"manage_slide_panel_empty": "Actuellement, il n'y a aucune slide. %link% maintenant.", "slideshow_slide_panel_empty": "Actuellement, il n'y a aucune slide. %link% maintenant.",
"manage_slide_panel_th_name": "Nom", "slideshow_slide_panel_th_name": "Nom",
"manage_slide_panel_th_duration": "Durée", "slideshow_slide_panel_th_duration": "Durée",
"manage_slide_panel_th_duration_unit": "sec", "slideshow_slide_panel_th_duration_unit": "sec",
"manage_slide_panel_th_enabled": "Activé", "slideshow_slide_panel_th_enabled": "Activé",
"manage_slide_panel_th_activity": "Options", "slideshow_slide_panel_th_activity": "Options",
"manage_slide_form_add_title": "Ajouter d'une slide", "slideshow_slide_form_add_title": "Ajouter d'une slide",
"manage_slide_form_add_submit": "Ajouter", "slideshow_slide_form_add_submit": "Ajouter",
"manage_slide_form_edit_title": "Modification d'une slide", "slideshow_slide_form_edit_title": "Modification d'une slide",
"manage_slide_form_edit_submit": "Enregistrer", "slideshow_slide_form_edit_submit": "Enregistrer",
"manage_slide_form_label_name": "Nom", "slideshow_slide_form_label_name": "Nom",
"manage_slide_form_label_location": "Chemin", "slideshow_slide_form_label_location": "Chemin",
"manage_slide_form_label_type": "Type", "slideshow_slide_form_label_type": "Type",
"manage_slide_form_label_type_url": "URL", "slideshow_slide_form_label_type_url": "URL",
"manage_slide_form_label_type_video": "Vidéo", "slideshow_slide_form_label_type_video": "Vidéo",
"manage_slide_form_label_type_picture": "Image", "slideshow_slide_form_label_type_picture": "Image",
"manage_slide_form_label_object": "Objet", "slideshow_slide_form_label_object": "Objet",
"manage_slide_form_label_duration": "Durée", "slideshow_slide_form_label_duration": "Durée",
"manage_slide_form_label_duration_unit": "secondes", "slideshow_slide_form_label_duration_unit": "secondes",
"manage_slide_form_button_cancel": "Annuler", "slideshow_slide_form_button_cancel": "Annuler",
"js_manage_slide_delete_confirmation": "Êtes-vous sûr ?", "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_page_title": "Système",
"sysinfo_panel_title": "Informations", "sysinfo_panel_title": "Informations",

View File

@ -7,18 +7,18 @@ import subprocess
import sys import sys
from enum import Enum from flask import Flask, send_from_directory
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from werkzeug.utils import secure_filename
from config import config from config import config
from src.SlideManager import SlideManager from src.SlideManager import SlideManager
from src.model.Slide import Slide from src.ScreenManager import ScreenManager
from src.model.SlideType import SlideType from src.controller.PlayerController import PlayerController
from src.utils import str_to_enum from src.controller.SlideshowController import SlideshowController
from src.controller.FleetController import FleetController
from src.controller.SysinfoController import SysinfoController
# <config> # <config>
PLAYER_URL = 'http://localhost:{}'.format(config['port']) PLAYER_URL = 'http://localhost:{}'.format(config['port'])
screen_manager = ScreenManager()
slide_manager = SlideManager() slide_manager = SlideManager()
with open('./lang/{}.json'.format(config['lang']), 'r') as file: with open('./lang/{}.json'.format(config['lang']), 'r') as file:
LANGDICT = json.load(file) LANGDICT = json.load(file)
@ -69,110 +69,21 @@ if config['lx_file']:
# </xenv> # </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> # <web>
@app.context_processor @app.context_processor
def inject_global_vars(): def inject_global_vars():
return dict( return dict(
FLEET_MODE=config['fleet_enabled'],
LANG=config['lang'], LANG=config['lang'],
STATIC_PREFIX='/data/www/' 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') PlayerController(app, LANGDICT, slide_manager)
def playlist(): SlideshowController(app, LANGDICT, slide_manager)
return jsonify(slide_manager.to_dict(slide_manager.get_enabled_slides())) 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) @app.errorhandler(404)
def not_found(e): 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 typing import Dict, Optional, List, Tuple, Union
from src.model.Slide import Slide from src.model.Slide import Slide
@ -6,15 +6,15 @@ from src.utils import str_to_enum
from pysondb import PysonDB from pysondb import PysonDB
class SlideManager(): class SlideManager:
DB_FILE = "data/slideshow.json" DB_FILE = "data/db/slideshow.json"
def __init__(self): def __init__(self):
self._db = PysonDB(self.DB_FILE) self._db = PysonDB(self.DB_FILE)
@staticmethod @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: if id:
raw_slide['id'] = id raw_slide['id'] = id
@ -28,6 +28,9 @@ class SlideManager():
def hydrate_list(raw_slides: list) -> List[Slide]: def hydrate_list(raw_slides: list) -> List[Slide]:
return [SlideManager.hydrate_object(raw_slide) for raw_slide in raw_slides] 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]: def get_all(self, sort: bool = False) -> List[Slide]:
raw_slides = self._db.get_all() raw_slides = self._db.get_all()
@ -44,7 +47,7 @@ class SlideManager():
def get_disabled_slides(self) -> List[Slide]: def get_disabled_slides(self) -> List[Slide]:
return [slide for slide in self.get_all(sort=True) if not slide.enabled] 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}) self._db.update_by_id(id, {"enabled": enabled, "position": 999})
def update_positions(self, positions: list) -> None: def update_positions(self, positions: list) -> None:
@ -59,7 +62,12 @@ class SlideManager():
del db_slide['id'] del db_slide['id']
self._db.add(db_slide) self._db.add(db_slide)
def delete(self, id: int) -> None: 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) self._db.delete_by_id(id)
def to_dict(self, slides: List[Slide]) -> dict: def to_dict(self, slides: List[Slide]) -> dict:

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: 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): 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):
if id: self._id = id if id else None
self._id = id
self._location = location self._location = location
self._duration = duration self._duration = duration
self._type = str_to_enum(type, SlideType) if isinstance(type, str) else type self._type = str_to_enum(type, SlideType) if isinstance(type, str) else type
@ -19,7 +17,7 @@ class Slide:
self._position = position self._position = position
@property @property
def id(self) -> Union[int, str]: def id(self) -> Optional[str]:
return self._id return self._id
@property @property

View File

@ -4,3 +4,16 @@ def str_to_enum(str_val: str, enum_class):
if enum_item.value == str_val: if enum_item.value == str_val:
return enum_item return enum_item
raise ValueError(f"{str_val} is not a valid {enum_class.__name__} 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> </h1>
<menu> <menu>
<ul> <ul>
<li class="{{ 'active' if request.url_rule.endpoint == 'manage' }}"> <li class="{{ 'active' if request.url_rule.endpoint == 'slideshow' }}">
<a href="{{ url_for('manage') }}"> <a href="{{ url_for('slideshow') }}">
<i class="fa-regular fa-clock"></i> {{ l.manage_page_title }} <i class="fa-regular fa-clock"></i> {{ l.slideshow_page_title }}
</a> </a>
</li> </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="#">#} {# <a href="#">#}
{# <i class="fa-solid fa-gear"></i> {{ l.settings_page_title }}#} {# <i class="fa-solid fa-gear"></i> {{ l.settings_page_title }}#}
{# </a>#} {# </a>#}
{# </li>#} {# </li>#}
<li class="{{ 'active' if request.url_rule.endpoint == 'manage_sysinfo' }}"> <li class="{{ 'active' if request.url_rule.endpoint == 'sysinfo' }}">
<a href="{{ url_for('manage_sysinfo') }}"> <a href="{{ url_for('sysinfo') }}">
<i class="fa-solid fa-list-check"></i> {{ l.sysinfo_page_title }} <i class="fa-solid fa-list-check"></i> {{ l.sysinfo_page_title }}
</a> </a>
</li> </li>
@ -54,7 +61,8 @@
{% endblock %} {% endblock %}
</div> </div>
<script> <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>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></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 %} {% 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> </head>
<body> <body>
<div id="FirstSlide" class="slide" style="visibility: hidden;"> <div id="FirstSlide" class="slide" style="visibility: hidden;">
<iframe src="/slide/default"></iframe> <iframe src="/player/default"></iframe>
</div> </div>
<div id="SecondSlide" style="visibility: visible;"> <div id="SecondSlide" style="visibility: visible;">
<iframe src="/slide/default"></iframe> <iframe src="/player/default"></iframe>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var items = {{items | safe}}; var items = {{items | safe}};
@ -26,7 +26,7 @@
var curUrl = 0; var curUrl = 0;
var nextReady = true; var nextReady = true;
var itemCheck = setInterval(function () { var itemCheck = setInterval(function () {
fetch('playlist').then(function(response) { fetch('player/playlist').then(function(response) {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }

View File

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

View File

@ -1,28 +1,28 @@
<div class="modal modal-slide-add"> <div class="modal modal-slide-add">
<h2> <h2>
{{ l.manage_slide_form_add_title }} {{ l.slideshow_slide_form_add_title }}
</h2> </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"> <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"> <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> </div>
<div class="form-group"> <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"> <div class="widget">
<select name="type" id="slide-add-type"> <select name="type" id="slide-add-type">
<option value="url" data-input="text">{{ l.manage_slide_form_label_type_url }}</option> <option value="url" data-input="text">{{ l.slideshow_slide_form_label_type_url }}</option>
<option value="video" data-input="upload">{{ l.manage_slide_form_label_type_video }}</option> <option value="video" data-input="upload">{{ l.slideshow_slide_form_label_type_video }}</option>
<option value="picture" data-input="upload">{{ l.manage_slide_form_label_type_picture }}</option> <option value="picture" data-input="upload">{{ l.slideshow_slide_form_label_type_picture }}</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group object-input"> <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"> <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" <input type="file" name="object" id="slide-add-object-input-upload"
@ -31,19 +31,19 @@
</div> </div>
<div class="form-group"> <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"> <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.manage_slide_form_label_duration_unit }}</span> <span>{{ l.slideshow_slide_form_label_duration_unit }}</span>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
<button type="button" class="modal-close"> <button type="button" class="modal-close">
{{ l.manage_slide_form_button_cancel }} {{ l.slideshow_slide_form_button_cancel }}
</button> </button>
<button type="submit" class="green"> <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> </button>
</div> </div>
</form> </form>

View File

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

View File

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

View File

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