diff --git a/.gitignore b/.gitignore index a75c825..7c51c14 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ out/ data/uploads/* !data/uploads/reclame.jpg -data/slideshow.json +data/db/slideshow.json config.py *.lock __pycache__/ diff --git a/config.py.dist b/config.py.dist index 092ab0b..a749cd7 100644 --- a/config.py.dist +++ b/config.py.dist @@ -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 } diff --git a/data/db/fleet.json b/data/db/fleet.json new file mode 100644 index 0000000..0190a7e --- /dev/null +++ b/data/db/fleet.json @@ -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" + } + } +} \ No newline at end of file diff --git a/data/slideshow.json.dist b/data/db/slideshow.json.dist similarity index 100% rename from data/slideshow.json.dist rename to data/db/slideshow.json.dist diff --git a/data/www/css/main.css b/data/www/css/main.css index 31af028..352a3b3 100644 --- a/data/www/css/main.css +++ b/data/www/css/main.css @@ -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; } diff --git a/data/www/js/fleet.js b/data/www/js/fleet.js new file mode 100644 index 0000000..0331497 --- /dev/null +++ b/data/www/js/fleet.js @@ -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(); +}); \ No newline at end of file diff --git a/data/www/js/manage.js b/data/www/js/slideshow.js similarity index 90% rename from data/www/js/manage.js rename to data/www/js/slideshow.js index eb38599..ec56592 100644 --- a/data/www/js/manage.js +++ b/data/www/js/slideshow.js @@ -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))}), }); diff --git a/lang/en.json b/lang/en.json index 7794e12..e47433a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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" } diff --git a/lang/fr.json b/lang/fr.json index 2aab7e0..5554479 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -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", diff --git a/obscreen.py b/obscreen.py index ec58254..49f9551 100755 --- a/obscreen.py +++ b/obscreen.py @@ -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 # 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']: # -# -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' -# - - # @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): diff --git a/src/ScreenManager.py b/src/ScreenManager.py new file mode 100644 index 0000000..09edafc --- /dev/null +++ b/src/ScreenManager.py @@ -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] diff --git a/src/SlideManager.py b/src/SlideManager.py index 47affbe..fa9e491 100644 --- a/src/SlideManager.py +++ b/src/SlideManager.py @@ -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] diff --git a/src/controller/FleetController.py b/src/controller/FleetController.py new file mode 100644 index 0000000..f274a9a --- /dev/null +++ b/src/controller/FleetController.py @@ -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'}) diff --git a/src/controller/PlayerController.py b/src/controller/PlayerController.py new file mode 100644 index 0000000..ad7a9b5 --- /dev/null +++ b/src/controller/PlayerController.py @@ -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())) diff --git a/src/controller/SlideshowController.py b/src/controller/SlideshowController.py new file mode 100644 index 0000000..71dd3d9 --- /dev/null +++ b/src/controller/SlideshowController.py @@ -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'}) diff --git a/src/controller/SysinfoController.py b/src/controller/SysinfoController.py new file mode 100644 index 0000000..e89eb70 --- /dev/null +++ b/src/controller/SysinfoController.py @@ -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, + ) diff --git a/src/model/Screen.py b/src/model/Screen.py new file mode 100644 index 0000000..3ce0658 --- /dev/null +++ b/src/model/Screen.py @@ -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, + } diff --git a/src/model/Slide.py b/src/model/Slide.py index af36e40..ff43f89 100644 --- a/src/model/Slide.py +++ b/src/model/Slide.py @@ -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 diff --git a/src/utils.py b/src/utils.py index d9046ae..396c3d6 100644 --- a/src/utils.py +++ b/src/utils.py @@ -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' \ No newline at end of file diff --git a/views/manager/base.jinja.html b/views/base.jinja.html similarity index 74% rename from views/manager/base.jinja.html rename to views/base.jinja.html index b05a902..36164c5 100755 --- a/views/manager/base.jinja.html +++ b/views/base.jinja.html @@ -24,18 +24,25 @@
    -
  • - - {{ l.manage_page_title }} +
  • + + {{ l.slideshow_page_title }}
  • -{#
  • #} + {% if FLEET_MODE %} +
  • + + {{ l.fleet_page_title }} + +
  • + {% endif %} +{#
  • #} {# #} {# {{ l.settings_page_title }}#} {# #} {#
  • #} -
  • - +
  • + {{ l.sysinfo_page_title }}
  • @@ -54,7 +61,8 @@ {% endblock %} {% block add_js %}{% endblock %} diff --git a/views/fleet/component/table.jinja.html b/views/fleet/component/table.jinja.html new file mode 100644 index 0000000..2b050d6 --- /dev/null +++ b/views/fleet/component/table.jinja.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + {% for screen in screens %} + + + + + + + {% endfor %} + +
    {{ l.fleet_screen_panel_th_name }}{{ l.fleet_screen_panel_th_address }}{{ l.fleet_screen_panel_th_enabled }}{{ l.fleet_screen_panel_th_activity }}
    + {{ l.fleet_screen_panel_empty|replace( + '%link%', + (''~l.fleet_screen_button_add~'')|safe + ) }} +
    +
    + + + + + {{ screen.name }} +
    +
    + {{ screen.address }} + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/views/fleet/fleet.jinja.html b/views/fleet/fleet.jinja.html new file mode 100644 index 0000000..42a730a --- /dev/null +++ b/views/fleet/fleet.jinja.html @@ -0,0 +1,52 @@ +{% extends 'base.jinja.html' %} + + +{% block page_title %} + {{ l.fleet_page_title }} +{% endblock %} + +{% block add_js %} + + +{% endblock %} + +{% block page %} +
    +

    {{ l.fleet_page_title }}

    + +
    + +
    +
    +
    +
    +

    {{ l.fleet_screen_panel_active }}

    + + {% with tclass='active', screens=enabled_screens %} + {% include 'fleet/component/table.jinja.html' %} + {% endwith %} +
    +
    +
    +
    +

    {{ l.fleet_screen_panel_inactive }}

    + + {% with tclass='inactive', screens=disabled_screens %} + {% include 'fleet/component/table.jinja.html' %} + {% endwith %} +
    +
    + + + +{% endblock %} diff --git a/views/fleet/modal/add.jinja.html b/views/fleet/modal/add.jinja.html new file mode 100644 index 0000000..770257c --- /dev/null +++ b/views/fleet/modal/add.jinja.html @@ -0,0 +1,30 @@ + \ No newline at end of file diff --git a/views/fleet/modal/edit.jinja.html b/views/fleet/modal/edit.jinja.html new file mode 100644 index 0000000..66999f0 --- /dev/null +++ b/views/fleet/modal/edit.jinja.html @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/views/player/player.jinja.html b/views/player/player.jinja.html index b5d935a..b623992 100755 --- a/views/player/player.jinja.html +++ b/views/player/player.jinja.html @@ -14,10 +14,10 @@
    - +
    - + {% endblock %} {% block page %}
    -

    {{ l.manage_page_title }}

    +

    {{ l.slideshow_page_title }}

    - +
    -

    {{ l.manage_slide_panel_active }}

    +

    {{ l.slideshow_slide_panel_active }}

    {% with tclass='active', slides=enabled_slides %} - {% include 'manager/manage-table.jinja.html' %} + {% include 'slideshow/component/table.jinja.html' %} {% endwith %}
    -

    {{ l.manage_slide_panel_inactive }}

    +

    {{ l.slideshow_slide_panel_inactive }}

    {% with tclass='inactive', slides=disabled_slides %} - {% include 'manager/manage-table.jinja.html' %} + {% include 'slideshow/component/table.jinja.html' %} {% endwith %}
    @@ -44,8 +44,8 @@
    - {% 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' %}
    diff --git a/views/manager/sysinfo.jinja.html b/views/sysinfo/sysinfo.jinja.html similarity index 95% rename from views/manager/sysinfo.jinja.html rename to views/sysinfo/sysinfo.jinja.html index ead0c8a..b4a7cd6 100644 --- a/views/manager/sysinfo.jinja.html +++ b/views/sysinfo/sysinfo.jinja.html @@ -1,4 +1,4 @@ -{% extends 'manager/base.jinja.html' %} +{% extends 'base.jinja.html' %} {% block page_title %}