add playlist management
This commit is contained in:
parent
dc0c975a36
commit
a7c986e1b4
@ -13,10 +13,10 @@ Use a RaspberryPi (Lite OS) to show a full-screen slideshow (Kiosk-mode)
|
||||
### Features:
|
||||
- Dead simple chromium webview
|
||||
- Clear GUI
|
||||
- Fleet view to manage many devices easily
|
||||
- Very few dependencies
|
||||
- SQLite database
|
||||
- Plugin system
|
||||
- Feature flags to enable complex use cases (Fleet/User/Playlist management)
|
||||
- No stupid pricing plan
|
||||
- No cloud
|
||||
- No telemetry
|
||||
@ -25,6 +25,6 @@ Use a RaspberryPi (Lite OS) to show a full-screen slideshow (Kiosk-mode)
|
||||
|
||||
# Two setups available
|
||||
|
||||
### 🔴 [I want to power RaspberryPi and automatically see my slideshow on a screen connected to it and manage the slideshow](docs/setup-run-on-rpi.md)
|
||||
### 🔵 [I want to start browser and setup playlist url manually on my device and just want a slideshow manager](docs/setup-run-headless.md)
|
||||
### 🔴 [I want to power a RaspberryPi and automatically see my slideshow on a screen connected to it and manage the slideshow](docs/setup-run-on-rpi.md)
|
||||
### 🔵 [I just want a slideshow manager and I'll deal with screen and browser myself](docs/setup-run-headless.md)
|
||||
|
||||
|
||||
@ -59,6 +59,14 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.container.expand {
|
||||
min-width: 100%;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
@ -346,11 +354,17 @@ button.purple:hover {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel td.infos .inner {
|
||||
.panel td .inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel td div.badge {
|
||||
margin-right: 5px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.panel a {
|
||||
@ -718,4 +732,102 @@ a.badge:hover {
|
||||
|
||||
.badge.anonymous {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.explorer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.explorer .left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.explorer .right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel.panel-menu {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
margin-right: 0;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.panel.panel-menu ul {
|
||||
flex: 1;
|
||||
max-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel.panel-menu ul li {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.panel.panel-menu ul li a {
|
||||
padding: 5px 15px 5px 15px;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.panel.panel-menu ul li:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.panel.panel-menu ul li.active {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
border: 1px solid rgba(255,255,255,.2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.explorer .panel-menu {
|
||||
border-color: #692fbd;
|
||||
}
|
||||
.explorer .panel-active {
|
||||
background: white;
|
||||
color: #AAA;
|
||||
}
|
||||
.explorer .panel-active h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
.explorer .panel-inactive {
|
||||
background: white;
|
||||
color: #AAA;
|
||||
border-color: #BBB;
|
||||
}
|
||||
|
||||
|
||||
.explorer .panel.panel-active th {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
@ -92,7 +92,6 @@ jQuery(document).ready(function ($) {
|
||||
$(document).on('click', '.user-delete', function () {
|
||||
if (confirm(l.js_auth_user_delete_confirmation)) {
|
||||
const $tr = $(this).parents('tr:eq(0)');
|
||||
updateTable();
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: '/auth/user/delete',
|
||||
@ -100,6 +99,7 @@ jQuery(document).ready(function ($) {
|
||||
data: JSON.stringify({id: getId($(this))}),
|
||||
success: function(data) {
|
||||
$tr.remove();
|
||||
updateTable();
|
||||
},
|
||||
error: function(data) {
|
||||
$('.alert-error').html(data.responseJSON.message).removeClass('hidden');
|
||||
@ -16,7 +16,7 @@ jQuery(document).ready(function ($) {
|
||||
}
|
||||
}).tableDnDUpdate();
|
||||
updatePositions();
|
||||
}
|
||||
};
|
||||
|
||||
const showModal = function (modalClass) {
|
||||
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
|
||||
96
data/www/js/playlist/playlists.js
Normal file
96
data/www/js/playlist/playlists.js
Normal file
@ -0,0 +1,96 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
const $tableActive = $('table.active-playlists');
|
||||
const $tableInactive = $('table.inactive-playlists');
|
||||
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.playlist-item:visible').length === 0) {
|
||||
$(this).find('tr.empty-tr').removeClass('hidden');
|
||||
} else {
|
||||
$(this).find('tr.empty-tr').addClass('hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 main = function () {
|
||||
|
||||
};
|
||||
|
||||
$(document).on('change', 'input[type=checkbox]', function () {
|
||||
$.ajax({
|
||||
url: '/playlist/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('click', '.modal-close', function () {
|
||||
hideModal();
|
||||
});
|
||||
|
||||
$(document).on('click', '.playlist-add', function () {
|
||||
showModal('modal-playlist-add');
|
||||
$('.modal-playlist-add input:eq(0)').focus().select();
|
||||
});
|
||||
|
||||
$(document).on('click', '.playlist-edit', function () {
|
||||
const playlist = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
|
||||
showModal('modal-playlist-edit');
|
||||
$('.modal-playlist-edit input:visible:eq(0)').focus().select();
|
||||
$('#playlist-edit-name').val(playlist.name);
|
||||
$('#playlist-edit-id').val(playlist.id);
|
||||
});
|
||||
|
||||
$(document).on('click', '.playlist-delete', function () {
|
||||
if (confirm(l.js_playlist_delete_confirmation)) {
|
||||
const $tr = $(this).parents('tr:eq(0)');
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: '/playlist/delete',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify({id: getId($(this))}),
|
||||
success: function(data) {
|
||||
$tr.remove();
|
||||
updateTable();
|
||||
},
|
||||
error: function(data) {
|
||||
$('.alert-error').html(data.responseJSON.message).removeClass('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).keyup(function (e) {
|
||||
if (e.key === "Escape") {
|
||||
hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
main();
|
||||
});
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
> #### 👈 [back to readme](/README.md)
|
||||
|
||||
#### 🔵 You want to start browser and setup playlist url manually on your device and just want a slideshow manager ? You're in the right place.
|
||||
#### 🔵 You just want a slideshow manager and you'll deal with screen and browser yourself ? You're in the right place.
|
||||
|
||||
---
|
||||
## 📡 Run the manager
|
||||
@ -73,16 +73,16 @@ python ./obscreen.py
|
||||
|
||||
#### Start server forever with systemctl
|
||||
```bash
|
||||
cat "$(pwd)/system/obscreen-manager.service" | sed "s#/home/pi#$HOME#g" | sed "s#=pi#=$USER#g" | sudo tee /etc/systemd/system/obscreen-manager.service
|
||||
cat "$(pwd)/system/obscreen-composer.service" | sed "s#/home/pi#$HOME#g" | sed "s#=pi#=$USER#g" | sudo tee /etc/systemd/system/obscreen-composer.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable obscreen-manager.service
|
||||
sudo systemctl start obscreen-manager.service
|
||||
sudo systemctl enable obscreen-composer.service
|
||||
sudo systemctl start obscreen-composer.service
|
||||
```
|
||||
|
||||
#### Troubleshoot
|
||||
```bash
|
||||
# Watch logs with following command
|
||||
sudo journalctl -u obscreen-manager -f
|
||||
sudo journalctl -u obscreen-composer -f
|
||||
```
|
||||
---
|
||||
## 👌 Usage
|
||||
|
||||
@ -87,16 +87,16 @@ python ./obscreen.py
|
||||
|
||||
#### Start server forever with systemctl
|
||||
```bash
|
||||
cat "$(pwd)/system/obscreen-manager.service" | sed "s#/home/pi#$HOME#g" | sed "s#=pi#=$USER#g" | sudo tee /etc/systemd/system/obscreen-manager.service
|
||||
cat "$(pwd)/system/obscreen-composer.service" | sed "s#/home/pi#$HOME#g" | sed "s#=pi#=$USER#g" | sudo tee /etc/systemd/system/obscreen-composer.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable obscreen-manager.service
|
||||
sudo systemctl start obscreen-manager.service
|
||||
sudo systemctl enable obscreen-composer.service
|
||||
sudo systemctl start obscreen-composer.service
|
||||
```
|
||||
|
||||
#### Troubleshoot
|
||||
```bash
|
||||
# Watch logs with following command
|
||||
sudo journalctl -u obscreen-manager -f
|
||||
sudo journalctl -u obscreen-composer -f
|
||||
```
|
||||
---
|
||||
## 🏁 Finally
|
||||
|
||||
25
lang/en.json
25
lang/en.json
@ -3,6 +3,8 @@
|
||||
"slideshow_goto_player": "Go to player",
|
||||
"slideshow_refresh_player": "Refresh player",
|
||||
"slideshow_refresh_player_success": "A player refresh has been schedueld, it should happen soon enough (%time% seconds maximum)",
|
||||
"slideshow_playlist_panel_title": "Playlists",
|
||||
"slideshow_playlist_panel_item_default": "Default Playlist",
|
||||
"slideshow_slide_button_add": "Add a slide",
|
||||
"slideshow_slide_panel_active": "Active slides",
|
||||
"slideshow_slide_panel_inactive": "Inactive slides",
|
||||
@ -37,7 +39,25 @@
|
||||
"slideshow_slide_form_button_cancel": "Cancel",
|
||||
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
||||
|
||||
"fleet_page_title": "Devices",
|
||||
"playlist_page_title": "Playlists",
|
||||
"playlist_button_add": "Add a playlist",
|
||||
"playlist_panel_active": "Active playlists",
|
||||
"playlist_panel_inactive": "Inactive playlists",
|
||||
"playlist_panel_empty": "Currently, there are no playlists. %link% now.",
|
||||
"playlist_panel_th_name": "Name",
|
||||
"playlist_panel_th_duration": "Duration",
|
||||
"playlist_panel_th_enabled": "Enabled",
|
||||
"playlist_panel_th_activity": "Options",
|
||||
"playlist_form_add_title": "Add Slide",
|
||||
"playlist_form_add_submit": "Add",
|
||||
"playlist_form_edit_title": "Edit Slide",
|
||||
"playlist_form_edit_submit": "Save",
|
||||
"playlist_form_label_name": "Name",
|
||||
"playlist_form_button_cancel": "Cancel",
|
||||
"js_playlist_delete_confirmation": "Are you sure?",
|
||||
"playlist_delete_has_slides": "Playlist has slides, please remove them before and retry",
|
||||
|
||||
"fleet_page_title": "Composers",
|
||||
"fleet_screen_button_add": "Add a screen",
|
||||
"fleet_screen_button_fleetview": "Fleet view",
|
||||
"fleet_screen_panel_active": "Active screens",
|
||||
@ -89,7 +109,8 @@
|
||||
"settings_variable_form_label_value": "Value",
|
||||
"settings_variable_form_button_cancel": "Cancel",
|
||||
"settings_variable_desc_lang": "Server language",
|
||||
"settings_variable_desc_fleet_enabled": "Enable fleet screen management view",
|
||||
"settings_variable_desc_playlist_enabled": "Enable playlist management",
|
||||
"settings_variable_desc_fleet_composer_enabled": "Enable fleet composer management",
|
||||
"settings_variable_desc_auth_enabled": "Enable auth management",
|
||||
"settings_variable_desc_edition_auth_enabled": "Default user credentials will be admin/admin",
|
||||
"settings_variable_desc_external_url": "External url (i.e: https://screen-01.company.com or http://10.10.3.100)",
|
||||
|
||||
27
lang/fr.json
27
lang/fr.json
@ -3,6 +3,8 @@
|
||||
"slideshow_goto_player": "Voir le lecteur",
|
||||
"slideshow_refresh_player": "Rafraîchir le lecteur",
|
||||
"slideshow_refresh_player_success": "Un rafraîchissement du lecteur a été programmé, il devrait avoir lieu sous peu (%time% secondes maximum)",
|
||||
"slideshow_playlist_panel_title": "Playlists",
|
||||
"slideshow_playlist_panel_item_default": "Playlist par défaut",
|
||||
"slideshow_slide_button_add": "Ajouter une slide",
|
||||
"slideshow_slide_panel_active": "Slides actives",
|
||||
"slideshow_slide_panel_inactive": "Slides inactives",
|
||||
@ -15,7 +17,7 @@
|
||||
"slideshow_slide_panel_th_activity": "Options",
|
||||
"slideshow_slide_panel_td_cron_scheduled_loop": "En boucle",
|
||||
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Mauvaise valeur cron",
|
||||
"slideshow_slide_form_add_title": "Ajouter d'une slide",
|
||||
"slideshow_slide_form_add_title": "Ajout d'une slide",
|
||||
"slideshow_slide_form_add_submit": "Ajouter",
|
||||
"slideshow_slide_form_edit_title": "Modification d'une slide",
|
||||
"slideshow_slide_form_edit_submit": "Enregistrer",
|
||||
@ -37,7 +39,25 @@
|
||||
"slideshow_slide_form_button_cancel": "Annuler",
|
||||
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
||||
|
||||
"fleet_page_title": "Appareils",
|
||||
"playlist_page_title": "Listes de lecture",
|
||||
"playlist_button_add": "Ajouter une liste de lecture",
|
||||
"playlist_panel_active": "Listes de lecture actives",
|
||||
"playlist_panel_inactive": "Listes de lecture inactives",
|
||||
"playlist_panel_empty": "Actuellement, il n'y a pas de liste de lecture. %link% maintenant.",
|
||||
"playlist_panel_th_name": "Nom",
|
||||
"playlist_panel_th_duration": "Durée",
|
||||
"playlist_panel_th_enabled": "Activé",
|
||||
"playlist_panel_th_activity": "Options",
|
||||
"playlist_form_add_title": "Ajout d'une liste de lecture",
|
||||
"playlist_form_add_submit": "Ajouter",
|
||||
"playlist_form_edit_title": "Modification d'une liste de lecture",
|
||||
"playlist_form_edit_submit": "Enregistrer",
|
||||
"playlist_form_label_name": "Nom",
|
||||
"playlist_form_button_cancel": "Annuler",
|
||||
"js_playlist_delete_confirmation": "Êtes-vous sûr ?",
|
||||
"playlist_delete_has_slides": "La liste de lecture contient des sldies, supprimez-les avant et réessayez",
|
||||
|
||||
"fleet_page_title": "Composeurs",
|
||||
"fleet_screen_button_add": "Ajouter un écran",
|
||||
"fleet_screen_button_fleetview": "Vue flotte",
|
||||
"fleet_screen_panel_active": "Écrans actifs",
|
||||
@ -89,7 +109,8 @@
|
||||
"settings_variable_form_label_value": "Valeur",
|
||||
"settings_variable_form_button_cancel": "Annuler",
|
||||
"settings_variable_desc_lang": "Langage de l'application",
|
||||
"settings_variable_desc_fleet_enabled": "Activer la gestion de flotte des écrans",
|
||||
"settings_variable_desc_playlist_enabled": "Activer la gestion des playlists",
|
||||
"settings_variable_desc_fleet_composer_enabled": "Activer la gestion de flotte des composeurs",
|
||||
"settings_variable_desc_auth_enabled": "Activer la gestion de l'authentification",
|
||||
"settings_variable_desc_edition_auth_enabled": "Les identifiants de l'utilisateur par défaut seront admin/admin",
|
||||
"settings_variable_desc_external_url": "URL externe (i.e: https://screen-01.company.com or http://10.10.3.100)",
|
||||
|
||||
@ -9,14 +9,22 @@ from src.interface.ObController import ObController
|
||||
|
||||
class AuthController(ObController):
|
||||
|
||||
def guard_auth(self, f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not self._model_store.variable().map().get('auth_enabled').as_bool():
|
||||
return redirect(url_for('manage'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/login', 'login', self.login, methods=['GET', 'POST'])
|
||||
self._app.add_url_rule('/logout', 'logout', self.logout, methods=['GET'])
|
||||
self._app.add_url_rule('/auth/user/list', 'auth_user_list', self._auth(self.auth_user_list), methods=['GET'])
|
||||
self._app.add_url_rule('/auth/user/add', 'auth_user_add', self._auth(self.auth_user_add), methods=['POST'])
|
||||
self._app.add_url_rule('/auth/user/edit', 'auth_user_edit', self._auth(self.auth_user_edit), methods=['POST'])
|
||||
self._app.add_url_rule('/auth/user/toggle', 'auth_user_toggle', self._auth(self.auth_user_toggle), methods=['POST'])
|
||||
self._app.add_url_rule('/auth/user/delete', 'auth_user_delete', self._auth(self.auth_user_delete), methods=['DELETE'])
|
||||
self._app.add_url_rule('/auth/user/list', 'auth_user_list', self.guard_auth(self._auth(self.auth_user_list)), methods=['GET'])
|
||||
self._app.add_url_rule('/auth/user/add', 'auth_user_add', self.guard_auth(self._auth(self.auth_user_add)), methods=['POST'])
|
||||
self._app.add_url_rule('/auth/user/edit', 'auth_user_edit', self.guard_auth(self._auth(self.auth_user_edit)), methods=['POST'])
|
||||
self._app.add_url_rule('/auth/user/toggle', 'auth_user_toggle', self.guard_auth(self._auth(self.auth_user_toggle)), methods=['POST'])
|
||||
self._app.add_url_rule('/auth/user/delete', 'auth_user_delete', self.guard_auth(self._auth(self.auth_user_delete)), methods=['DELETE'])
|
||||
|
||||
def login(self):
|
||||
login_error = None
|
||||
|
||||
@ -8,14 +8,22 @@ from src.interface.ObController import ObController
|
||||
|
||||
class FleetController(ObController):
|
||||
|
||||
def guard_fleet(self, f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not self._model_store.variable().map().get('fleet_composer_enabled').as_bool():
|
||||
return redirect(url_for('manage'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/fleet', 'fleet', self._auth(self.fleet), methods=['GET'])
|
||||
self._app.add_url_rule('/fleet/screen/list', 'fleet_screen_list', self._auth(self.fleet_screen_list), methods=['GET'])
|
||||
self._app.add_url_rule('/fleet/screen/add', 'fleet_screen_add', self._auth(self.fleet_screen_add), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet/screen/edit', 'fleet_screen_edit', self._auth(self.fleet_screen_edit), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet/screen/toggle', 'fleet_screen_toggle', self._auth(self.fleet_screen_toggle), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet/screen/delete', 'fleet_screen_delete', self._auth(self.fleet_screen_delete), methods=['DELETE'])
|
||||
self._app.add_url_rule('/fleet/screen/position', 'fleet_screen_position', self._auth(self.fleet_screen_position), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet', 'fleet', self.guard_fleet(self._auth(self.fleet)), methods=['GET'])
|
||||
self._app.add_url_rule('/fleet/screen/list', 'fleet_screen_list', self.guard_fleet(self._auth(self.fleet_screen_list)), methods=['GET'])
|
||||
self._app.add_url_rule('/fleet/screen/add', 'fleet_screen_add', self.guard_fleet(self._auth(self.fleet_screen_add)), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet/screen/edit', 'fleet_screen_edit', self.guard_fleet(self._auth(self.fleet_screen_edit)), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet/screen/toggle', 'fleet_screen_toggle', self.guard_fleet(self._auth(self.fleet_screen_toggle)), methods=['POST'])
|
||||
self._app.add_url_rule('/fleet/screen/delete', 'fleet_screen_delete', self.guard_fleet(self._auth(self.fleet_screen_delete)), methods=['DELETE'])
|
||||
self._app.add_url_rule('/fleet/screen/position', 'fleet_screen_position', self.guard_fleet(self._auth(self.fleet_screen_position)), methods=['POST'])
|
||||
|
||||
def fleet(self):
|
||||
return render_template(
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import json
|
||||
|
||||
from typing import Optional
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
|
||||
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.interface.ObController import ObController
|
||||
from src.utils import get_ip_address, get_safe_cron_descriptor
|
||||
@ -8,8 +10,8 @@ from src.utils import get_ip_address, get_safe_cron_descriptor
|
||||
|
||||
class PlayerController(ObController):
|
||||
|
||||
def _get_playlist(self) -> dict:
|
||||
enabled_slides = self._model_store.slide().get_enabled_slides()
|
||||
def _get_playlist(self, playlist_id: Optional[int] = 0) -> dict:
|
||||
enabled_slides = self._model_store.slide().get_slides(enabled=True, playlist_id=playlist_id)
|
||||
slides = self._model_store.slide().to_dict(enabled_slides)
|
||||
|
||||
playlist_loop = []
|
||||
@ -32,13 +34,21 @@ class PlayerController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/', 'player', self.player, methods=['GET'])
|
||||
self._app.add_url_rule('/use/<playlist_slug_or_id>', 'player_use', 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'])
|
||||
self._app.add_url_rule('/player/playlist/use/<playlist_slug_or_id>', 'player_playlist_use', self.player_playlist, methods=['GET'])
|
||||
|
||||
def player(self, playlist_slug_or_id: str = ''):
|
||||
current_playlist = self._model_store.playlist().get_one_by("slug = ? OR id = ?", {
|
||||
"slug": playlist_slug_or_id,
|
||||
"id": playlist_slug_or_id
|
||||
})
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
|
||||
def player(self):
|
||||
return render_template(
|
||||
'player/player.jinja.html',
|
||||
items=json.dumps(self._get_playlist()),
|
||||
items=json.dumps(self._get_playlist(playlist_id=playlist_id)),
|
||||
default_slide_duration=self._model_store.variable().get_one_by_name('default_slide_duration'),
|
||||
polling_interval=self._model_store.variable().get_one_by_name('polling_interval'),
|
||||
slide_animation_enabled=self._model_store.variable().get_one_by_name('slide_animation_enabled'),
|
||||
@ -54,5 +64,11 @@ class PlayerController(ObController):
|
||||
ipaddr=ipaddr if ipaddr else self._model_store.lang().map().get('common_unknown_ipaddr')
|
||||
)
|
||||
|
||||
def player_playlist(self):
|
||||
return jsonify(self._get_playlist())
|
||||
def player_playlist(self, playlist_slug_or_id: str = ''):
|
||||
current_playlist = self._model_store.playlist().get_one_by("slug = ? OR id = ?", {
|
||||
"slug": playlist_slug_or_id,
|
||||
"id": playlist_slug_or_id
|
||||
})
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
|
||||
return jsonify(self._get_playlist(playlist_id=playlist_id))
|
||||
|
||||
68
src/controller/PlaylistController.py
Normal file
68
src/controller/PlaylistController.py
Normal file
@ -0,0 +1,68 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, render_template, redirect, request, url_for, jsonify
|
||||
from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.interface.ObController import ObController
|
||||
|
||||
|
||||
class PlaylistController(ObController):
|
||||
|
||||
def guard_playlist(self, f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not self._model_store.variable().map().get('playlist_enabled').as_bool():
|
||||
return redirect(url_for('manage'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/playlist/list', 'playlist_list', self.guard_playlist(self._auth(self.playlist_list)), methods=['GET'])
|
||||
self._app.add_url_rule('/playlist/add', 'playlist_add', self.guard_playlist(self._auth(self.playlist_add)), methods=['POST'])
|
||||
self._app.add_url_rule('/playlist/edit', 'playlist_edit', self.guard_playlist(self._auth(self.playlist_edit)), methods=['POST'])
|
||||
self._app.add_url_rule('/playlist/toggle', 'playlist_toggle', self.guard_playlist(self._auth(self.playlist_toggle)), methods=['POST'])
|
||||
self._app.add_url_rule('/playlist/delete', 'playlist_delete', self.guard_playlist(self._auth(self.playlist_delete)), methods=['DELETE'])
|
||||
|
||||
def playlist(self):
|
||||
return render_template(
|
||||
'playlist/playlist.jinja.html',
|
||||
playlists=self._model_store.playlist().get_enabled_playlists(),
|
||||
)
|
||||
|
||||
def playlist_list(self):
|
||||
durations = self._model_store.playlist().get_durations_by_playlists()
|
||||
return render_template(
|
||||
'playlist/list.jinja.html',
|
||||
enabled_playlists=self._model_store.playlist().get_enabled_playlists(with_default=True),
|
||||
disabled_playlists=self._model_store.playlist().get_disabled_playlists(),
|
||||
durations=durations
|
||||
)
|
||||
|
||||
def playlist_add(self):
|
||||
playlist = Playlist(
|
||||
name=request.form['name'],
|
||||
)
|
||||
|
||||
self._model_store.playlist().add_form(playlist)
|
||||
|
||||
return redirect(url_for('playlist_list'))
|
||||
|
||||
def playlist_edit(self):
|
||||
self._model_store.playlist().update_form(
|
||||
id=request.form['id'],
|
||||
name=request.form['name'],
|
||||
)
|
||||
return redirect(url_for('playlist_list'))
|
||||
|
||||
def playlist_toggle(self):
|
||||
data = request.get_json()
|
||||
self._model_store.playlist().update_enabled(data.get('id'), data.get('enabled'))
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
def playlist_delete(self):
|
||||
data = request.get_json()
|
||||
id = data.get('id')
|
||||
if self._model_store.slide().count_slides_for_playlist(id) > 0:
|
||||
return jsonify({'status': 'error', 'message': self.t('playlist_delete_has_slides')}), 400
|
||||
self._model_store.playlist().delete(id)
|
||||
return jsonify({'status': 'ok'})
|
||||
@ -33,7 +33,7 @@ class SettingsController(ObController):
|
||||
if variable.name == 'slide_upload_limit':
|
||||
self.reload_web_server()
|
||||
|
||||
if variable.name == 'fleet_enabled':
|
||||
if variable.name == 'fleet_composer_enabled':
|
||||
self.reload_web_server()
|
||||
|
||||
if variable.name == 'auth_enabled':
|
||||
|
||||
@ -8,7 +8,7 @@ from src.service.ModelStore import ModelStore
|
||||
from src.model.entity.Slide import Slide
|
||||
from src.model.enum.SlideType import SlideType
|
||||
from src.interface.ObController import ObController
|
||||
from src.utils import str_to_enum, get_optional_string
|
||||
from src.utils import str_to_enum, get_optional_string, randomize_filename
|
||||
|
||||
|
||||
class SlideshowController(ObController):
|
||||
@ -16,6 +16,7 @@ class SlideshowController(ObController):
|
||||
def register(self):
|
||||
self._app.add_url_rule('/manage', 'manage', self.manage, methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow', 'slideshow_slide_list', self._auth(self.slideshow), methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow/playlist/set/<playlist_id>', 'slideshow_slide_list_playlist_use', self._auth(self.slideshow), methods=['GET'])
|
||||
self._app.add_url_rule('/slideshow/slide/add', 'slideshow_slide_add', self._auth(self.slideshow_slide_add), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/slide/edit', 'slideshow_slide_edit', self._auth(self.slideshow_slide_edit), methods=['POST'])
|
||||
self._app.add_url_rule('/slideshow/slide/toggle', 'slideshow_slide_toggle', self._auth(self.slideshow_slide_toggle), methods=['POST'])
|
||||
@ -26,11 +27,15 @@ class SlideshowController(ObController):
|
||||
def manage(self):
|
||||
return redirect(url_for('slideshow_slide_list'))
|
||||
|
||||
def slideshow(self):
|
||||
def slideshow(self, playlist_id: int = 0):
|
||||
current_playlist = self._model_store.playlist().get(playlist_id)
|
||||
playlist_id = current_playlist.id if current_playlist else None
|
||||
return render_template(
|
||||
'slideshow/list.jinja.html',
|
||||
enabled_slides=self._model_store.slide().get_enabled_slides(),
|
||||
disabled_slides=self._model_store.slide().get_disabled_slides(),
|
||||
current_playlist=current_playlist,
|
||||
playlists=self._model_store.playlist().get_enabled_playlists(),
|
||||
enabled_slides=self._model_store.slide().get_slides(playlist_id=playlist_id, enabled=True),
|
||||
disabled_slides=self._model_store.slide().get_slides(playlist_id=playlist_id, enabled=False),
|
||||
var_last_restart=self._model_store.variable().get_one_by_name('last_restart'),
|
||||
var_external_url=self._model_store.variable().get_one_by_name('external_url'),
|
||||
enum_slide_type=SlideType
|
||||
@ -41,6 +46,7 @@ class SlideshowController(ObController):
|
||||
name=request.form['name'],
|
||||
type=str_to_enum(request.form['type'], SlideType),
|
||||
duration=request.form['duration'],
|
||||
playlist=request.form['playlist'] if 'playlist' in request.form else None,
|
||||
cron_schedule=get_optional_string(request.form['cron_schedule']),
|
||||
cron_schedule_end=get_optional_string(request.form['cron_schedule_end']),
|
||||
)
|
||||
@ -55,7 +61,7 @@ class SlideshowController(ObController):
|
||||
return redirect(request.url)
|
||||
|
||||
if object:
|
||||
object_name = secure_filename(object.filename)
|
||||
object_name = randomize_filename(object.filename)
|
||||
object_path = os.path.join(self._app.config['UPLOAD_FOLDER'], object_name)
|
||||
object.save(object_path)
|
||||
slide.location = object_path
|
||||
@ -65,10 +71,13 @@ class SlideshowController(ObController):
|
||||
self._model_store.slide().add_form(slide)
|
||||
self._post_update()
|
||||
|
||||
if slide.playlist:
|
||||
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist))
|
||||
|
||||
return redirect(url_for('slideshow_slide_list'))
|
||||
|
||||
def slideshow_slide_edit(self):
|
||||
self._model_store.slide().update_form(
|
||||
slide = self._model_store.slide().update_form(
|
||||
id=request.form['id'],
|
||||
name=request.form['name'],
|
||||
duration=request.form['duration'],
|
||||
@ -77,6 +86,10 @@ class SlideshowController(ObController):
|
||||
location=request.form['location'] if 'location' in request.form else None
|
||||
)
|
||||
self._post_update()
|
||||
|
||||
if slide.playlist:
|
||||
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist))
|
||||
|
||||
return redirect(url_for('slideshow_slide_list'))
|
||||
|
||||
def slideshow_slide_toggle(self):
|
||||
|
||||
@ -63,7 +63,7 @@ class SysinfoController(ObController):
|
||||
os.execl(python, python, *sys.argv)
|
||||
else:
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "restart", 'obscreen-manager'], check=True, timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.run(["sudo", "systemctl", "restart", 'obscreen-composer'], check=True, timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
pass
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import sqlite3
|
||||
import logging
|
||||
@ -7,6 +8,7 @@ from sqlite3 import Cursor
|
||||
from src.utils import wrap_if, is_wrapped_by
|
||||
from typing import Optional, Dict
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
|
||||
DB_FILE: str = "data/db/obscreen.db"
|
||||
@ -28,10 +30,19 @@ class DatabaseManager:
|
||||
self._conn.row_factory = sqlite3.Row
|
||||
|
||||
def open(self, table_name: str, table_model: list):
|
||||
self.execute_write_query('''CREATE TABLE IF NOT EXISTS {} (
|
||||
new_table_definition = '''CREATE TABLE IF NOT EXISTS {} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
{}
|
||||
)'''.format(table_name, ", ".join(table_model)))
|
||||
)'''.format(table_name, ", ".join(table_model))
|
||||
self.execute_write_query(new_table_definition)
|
||||
|
||||
old_table_definition = self.execute_read_query("select sql from sqlite_master where tbl_name = ?", (table_name,))
|
||||
old_table_definition = old_table_definition[0]['sql']
|
||||
|
||||
delta_queries = self.generate_delta_queries(old_table_definition, new_table_definition)
|
||||
|
||||
for delta_query in delta_queries:
|
||||
self.execute_write_query(delta_query)
|
||||
|
||||
return self
|
||||
|
||||
@ -95,18 +106,19 @@ class DatabaseManager:
|
||||
query="select * from {} {}".format(table_name, "ORDER BY {} ASC".format(sort) if sort else "")
|
||||
)
|
||||
|
||||
def get_by_query(self, table_name: str, query: str = "1=1", sort: Optional[str] = None) -> list:
|
||||
def get_by_query(self, table_name: str, query: str = "1=1", sort: Optional[str] = None, values: dict = {}) -> list:
|
||||
return self.execute_read_query(
|
||||
query="select * from {} where {} {}".format(
|
||||
table_name,
|
||||
query,
|
||||
"ORDER BY {} ASC".format(sort) if sort else ""
|
||||
)
|
||||
),
|
||||
params=tuple(v for v in values.values())
|
||||
)
|
||||
|
||||
def get_one_by_query(self, table_name: str, query: str = "1=1", sort: Optional[str] = None) -> list:
|
||||
def get_one_by_query(self, table_name: str, query: str = "1=1", sort: Optional[str] = None, values: dict = {}) -> list:
|
||||
query = "select * from {} where {} {}".format(table_name, query, "ORDER BY {} ASC".format(sort) if sort else "")
|
||||
lines = self.execute_read_query(query=query)
|
||||
lines = self.execute_read_query(query=query, params=tuple(v for v in values.values()))
|
||||
count = len(lines)
|
||||
|
||||
if count > 1:
|
||||
@ -142,3 +154,55 @@ class DatabaseManager:
|
||||
|
||||
def delete_by_id(self, table_name: str, id: int) -> None:
|
||||
self.execute_write_query("DELETE FROM {} WHERE id = ?".format(table_name), params=(id,))
|
||||
|
||||
@staticmethod
|
||||
def parse_create_table_query(query: str):
|
||||
table_name_pattern = re.compile(r'CREATE TABLE\s+(IF NOT EXISTS\s+)?["]?(\w+)["]?', re.IGNORECASE)
|
||||
columns_pattern = re.compile(r'\((.*)\)', re.DOTALL)
|
||||
|
||||
table_name_match = table_name_pattern.search(query)
|
||||
columns_match = columns_pattern.search(query)
|
||||
|
||||
if not table_name_match or not columns_match:
|
||||
raise ValueError("Invalid CREATE TABLE query.")
|
||||
|
||||
table_name = table_name_match.group(2)
|
||||
columns_part = columns_match.group(1)
|
||||
|
||||
# Split columns_part by commas but ignore commas inside parentheses
|
||||
columns = re.split(r',\s*(?![^()]*\))', columns_part)
|
||||
|
||||
# Extract column names and their definitions
|
||||
column_definitions = {}
|
||||
for column in columns:
|
||||
column_parts = column.strip().split(maxsplit=1)
|
||||
column_name = column_parts[0]
|
||||
column_definition = column.strip()
|
||||
column_definitions[column_name] = column_definition
|
||||
|
||||
return table_name, column_definitions
|
||||
|
||||
|
||||
@staticmethod
|
||||
def generate_delta_queries(old_query: str, new_query: str) -> list:
|
||||
old_table_name, old_columns = DatabaseManager.parse_create_table_query(old_query)
|
||||
new_table_name, new_columns = DatabaseManager.parse_create_table_query(new_query)
|
||||
|
||||
if old_table_name != new_table_name:
|
||||
raise ValueError("Table names do not match.")
|
||||
|
||||
old_column_names = set(old_columns.keys())
|
||||
new_column_names = set(new_columns.keys())
|
||||
|
||||
columns_to_add = new_column_names - old_column_names
|
||||
columns_to_remove = old_column_names - new_column_names
|
||||
|
||||
delta_queries = []
|
||||
|
||||
for column in columns_to_add:
|
||||
delta_queries.append(f'ALTER TABLE {old_table_name} ADD COLUMN {new_columns[column]}')
|
||||
|
||||
for column in columns_to_remove:
|
||||
delta_queries.append(f'ALTER TABLE {old_table_name} DROP COLUMN {column}')
|
||||
|
||||
return delta_queries
|
||||
|
||||
152
src/manager/PlaylistManager.py
Normal file
152
src/manager/PlaylistManager.py
Normal file
@ -0,0 +1,152 @@
|
||||
import os
|
||||
|
||||
from typing import Dict, Optional, List, Tuple, Union
|
||||
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.utils import get_optional_string, get_yt_video_id, slugify
|
||||
from src.manager.DatabaseManager import DatabaseManager
|
||||
from src.manager.LangManager import LangManager
|
||||
from src.manager.UserManager import UserManager
|
||||
from src.service.ModelManager import ModelManager
|
||||
|
||||
|
||||
class PlaylistManager(ModelManager):
|
||||
|
||||
TABLE_NAME = "playlist"
|
||||
TABLE_MODEL = [
|
||||
"name CHAR(255)",
|
||||
"slug CHAR(255)",
|
||||
"enabled INTEGER DEFAULT 0",
|
||||
"created_by CHAR(255)",
|
||||
"updated_by CHAR(255)",
|
||||
"created_at INTEGER",
|
||||
"updated_at INTEGER"
|
||||
]
|
||||
|
||||
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager):
|
||||
super().__init__(lang_manager, database_manager, user_manager)
|
||||
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
|
||||
|
||||
def hydrate_object(self, raw_playlist: dict, id: int = None) -> Playlist:
|
||||
if id:
|
||||
raw_playlist['id'] = id
|
||||
|
||||
[raw_playlist, user_tracker_edits] = self.user_manager.initialize_user_trackers(raw_playlist)
|
||||
|
||||
if len(user_tracker_edits) > 0:
|
||||
self._db.update_by_id(self.TABLE_NAME, raw_playlist['id'], user_tracker_edits)
|
||||
|
||||
return Playlist(**raw_playlist)
|
||||
|
||||
def hydrate_list(self, raw_playlists: list) -> List[Playlist]:
|
||||
return [self.hydrate_object(raw_playlist) for raw_playlist in raw_playlists]
|
||||
|
||||
def get(self, id: Optional[int]) -> Optional[Playlist]:
|
||||
if not id:
|
||||
return None
|
||||
|
||||
object = self._db.get_by_id(self.TABLE_NAME, id)
|
||||
return self.hydrate_object(object, id) if object else None
|
||||
|
||||
def get_by(self, query, sort: Optional[str] = None, values: dict = {}) -> List[Playlist]:
|
||||
return self.hydrate_list(self._db.get_by_query(self.TABLE_NAME, query=query, sort=sort, values=values))
|
||||
|
||||
def get_one_by(self, query, values: dict = {}) -> Optional[Playlist]:
|
||||
object = self._db.get_one_by_query(self.TABLE_NAME, query=query, values=values)
|
||||
|
||||
if not object:
|
||||
return None
|
||||
|
||||
return self.hydrate_object(object)
|
||||
|
||||
def get_durations_by_playlists(self):
|
||||
durations = self._db.execute_read_query("select playlist, sum(duration) as total_duration from slideshow where cron_schedule is null group by playlist")
|
||||
map = {}
|
||||
for duration in durations:
|
||||
map[duration['playlist']] = duration['total_duration']
|
||||
return map
|
||||
|
||||
def get_all(self) -> List[Playlist]:
|
||||
return self.hydrate_list(self._db.get_all(self.TABLE_NAME))
|
||||
|
||||
def get_enabled_playlists(self, with_default: bool = False) -> List[Playlist]:
|
||||
playlists = self.get_by(query="enabled = 1")
|
||||
|
||||
if not with_default:
|
||||
return playlists
|
||||
|
||||
return [Playlist(id=None, name=self.t('slideshow_playlist_panel_item_default'))] + playlists
|
||||
|
||||
def get_disabled_playlists(self) -> List[Playlist]:
|
||||
return self.get_by(query="enabled = 0")
|
||||
|
||||
def update_enabled(self, id: int, enabled: bool) -> None:
|
||||
self._db.update_by_id(self.TABLE_NAME, id, {"enabled": enabled})
|
||||
|
||||
def forget_user(self, user_id: int):
|
||||
playlists = self.get_by("created_by = '{}' or updated_by = '{}'".format(user_id, user_id))
|
||||
edits_playlists = self.user_manager.forget_user(playlists, user_id)
|
||||
|
||||
for playlist_id, edits in edits_playlists.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, playlist_id, edits)
|
||||
|
||||
def pre_add(self, playlist: Dict) -> Dict:
|
||||
playlist["slug"] = slugify(playlist["name"])
|
||||
self.user_manager.track_user_on_create(playlist)
|
||||
self.user_manager.track_user_on_update(playlist)
|
||||
return playlist
|
||||
|
||||
def pre_update(self, playlist: Dict) -> Dict:
|
||||
playlist["slug"] = slugify(playlist["name"])
|
||||
self.user_manager.track_user_on_update(playlist)
|
||||
return playlist
|
||||
|
||||
def pre_delete(self, playlist_id: str) -> str:
|
||||
return playlist_id
|
||||
|
||||
def post_add(self, playlist_id: str) -> str:
|
||||
return playlist_id
|
||||
|
||||
def post_update(self, playlist_id: str) -> str:
|
||||
return playlist_id
|
||||
|
||||
def post_updates(self):
|
||||
pass
|
||||
|
||||
def post_delete(self, playlist_id: str) -> str:
|
||||
return playlist_id
|
||||
|
||||
def update_form(self, id: int, name: str) -> None:
|
||||
playlist = self.get(id)
|
||||
|
||||
if not playlist:
|
||||
return
|
||||
|
||||
form = {
|
||||
"name": name
|
||||
}
|
||||
|
||||
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))
|
||||
self.post_update(id)
|
||||
|
||||
def add_form(self, playlist: Union[Playlist, Dict]) -> None:
|
||||
form = playlist
|
||||
|
||||
if not isinstance(playlist, dict):
|
||||
form = playlist.to_dict()
|
||||
del form['id']
|
||||
|
||||
self._db.add(self.TABLE_NAME, self.pre_add(form))
|
||||
self.post_add(playlist.id)
|
||||
|
||||
def delete(self, id: int) -> None:
|
||||
playlist = self.get(id)
|
||||
|
||||
if playlist:
|
||||
self.pre_delete(id)
|
||||
self._db.delete_by_id(self.TABLE_NAME, id)
|
||||
self.post_delete(id)
|
||||
|
||||
def to_dict(self, playlists: List[Playlist]) -> List[Dict]:
|
||||
return [playlist.to_dict() for playlist in playlists]
|
||||
|
||||
@ -12,7 +12,7 @@ class ScreenManager(ModelManager):
|
||||
TABLE_NAME = "fleet"
|
||||
TABLE_MODEL = [
|
||||
"name CHAR(255)",
|
||||
"enabled INTEGER",
|
||||
"enabled INTEGER DEFAULT 0",
|
||||
"position INTEGER",
|
||||
"host CHAR(255)",
|
||||
"port INTEGER"
|
||||
|
||||
@ -3,6 +3,7 @@ import os
|
||||
from typing import Dict, Optional, List, Tuple, Union
|
||||
|
||||
from src.model.entity.Slide import Slide
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.model.enum.SlideType import SlideType
|
||||
from src.utils import get_optional_string, get_yt_video_id
|
||||
from src.manager.DatabaseManager import DatabaseManager
|
||||
@ -17,7 +18,8 @@ class SlideManager(ModelManager):
|
||||
TABLE_MODEL = [
|
||||
"name CHAR(255)",
|
||||
"type CHAR(30)",
|
||||
"enabled INTEGER",
|
||||
"enabled INTEGER DEFAULT 0",
|
||||
"playlist INTEGER",
|
||||
"duration INTEGER",
|
||||
"position INTEGER",
|
||||
"location TEXT",
|
||||
@ -72,11 +74,14 @@ class SlideManager(ModelManager):
|
||||
for slide_id, edits in edits_slides.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, slide_id, edits)
|
||||
|
||||
def get_enabled_slides(self) -> List[Slide]:
|
||||
return self.get_by(query="enabled = 1", sort="position")
|
||||
def get_slides(self, playlist_id: Optional[int] = None, enabled: bool = True) -> List[Slide]:
|
||||
query = "enabled = {}".format("1" if enabled else "0")
|
||||
if playlist_id:
|
||||
query = "{} {}".format(query, "AND playlist = {}".format(playlist_id))
|
||||
else:
|
||||
query = "{} {}".format(query, "AND playlist is NULL")
|
||||
|
||||
def get_disabled_slides(self) -> List[Slide]:
|
||||
return self.get_by(query="enabled = 0", sort="position")
|
||||
return self.get_by(query=query, sort="position")
|
||||
|
||||
def pre_add(self, slide: Dict) -> Dict:
|
||||
self.user_manager.track_user_on_create(slide)
|
||||
@ -110,7 +115,7 @@ class SlideManager(ModelManager):
|
||||
for slide_id, slide_position in positions.items():
|
||||
self._db.update_by_id(self.TABLE_NAME, slide_id, {"position": slide_position})
|
||||
|
||||
def update_form(self, id: int, name: str, duration: int, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', location: Optional[str] = None) -> None:
|
||||
def update_form(self, id: int, name: str, duration: int, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', location: Optional[str] = None) -> Slide:
|
||||
slide = self.get(id)
|
||||
|
||||
if not slide:
|
||||
@ -131,6 +136,7 @@ class SlideManager(ModelManager):
|
||||
|
||||
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))
|
||||
self.post_update(id)
|
||||
return self.get(id)
|
||||
|
||||
def add_form(self, slide: Union[Slide, Dict]) -> None:
|
||||
form = slide
|
||||
@ -162,3 +168,5 @@ class SlideManager(ModelManager):
|
||||
def to_dict(self, slides: List[Slide]) -> List[Dict]:
|
||||
return [slide.to_dict() for slide in slides]
|
||||
|
||||
def count_slides_for_playlist(self, id: int) -> int:
|
||||
return len(self.get_slides(playlist_id=id))
|
||||
@ -15,7 +15,7 @@ class UserManager:
|
||||
TABLE_MODEL = [
|
||||
"username CHAR(255)",
|
||||
"password CHAR(255)",
|
||||
"enabled INTEGER"
|
||||
"enabled INTEGER DEFAULT 1"
|
||||
]
|
||||
|
||||
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, on_user_delete):
|
||||
|
||||
@ -104,7 +104,8 @@ class VariableManager(ModelManager):
|
||||
### General
|
||||
{"name": "lang", "section": self.t(VariableSection.GENERAL), "value": "en", "type": VariableType.SELECT_SINGLE, "editable": True, "description": self.t('settings_variable_desc_lang'), "selectables": self.t(ApplicationLanguage), "refresh_player": False},
|
||||
{"name": "auth_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_auth_enabled'), "description_edition": self.t('settings_variable_desc_edition_auth_enabled'), "refresh_player": False},
|
||||
{"name": "fleet_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_fleet_enabled'), "refresh_player": False},
|
||||
{"name": "fleet_composer_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_fleet_composer_enabled'), "refresh_player": False},
|
||||
{"name": "playlist_enabled", "section": self.t(VariableSection.GENERAL), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_playlist_enabled'), "refresh_player": False},
|
||||
{"name": "external_url", "section": self.t(VariableSection.GENERAL), "value": "", "type": VariableType.STRING, "editable": True, "description": self.t('settings_variable_desc_external_url'), "refresh_player": False},
|
||||
{"name": "slide_upload_limit", "section": self.t(VariableSection.GENERAL), "value": 32, "unit": VariableUnit.MEGABYTE, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_slide_upload_limit'), "refresh_player": False},
|
||||
|
||||
|
||||
111
src/model/entity/Playlist.py
Normal file
111
src/model/entity/Playlist.py
Normal file
@ -0,0 +1,111 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
class Playlist:
|
||||
|
||||
def __init__(self, name: str = 'Untitled', slug: str = 'untitled', id: Optional[int] = None, enabled: bool = False, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
|
||||
self._id = id if id else None
|
||||
self._name = name
|
||||
self._slug = slug
|
||||
self._enabled = enabled
|
||||
self._created_by = created_by if created_by else None
|
||||
self._updated_by = updated_by if updated_by else None
|
||||
self._created_at = int(created_at if created_at else time.time())
|
||||
self._updated_at = int(updated_at if updated_at else time.time())
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[int]:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return bool(self._enabled)
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value: bool):
|
||||
self._enabled = bool(value)
|
||||
|
||||
@property
|
||||
def created_by(self) -> str:
|
||||
return self._created_by
|
||||
|
||||
@created_by.setter
|
||||
def created_by(self, value: str):
|
||||
self._created_by = value
|
||||
|
||||
@property
|
||||
def updated_by(self) -> str:
|
||||
return self._updated_by
|
||||
|
||||
@updated_by.setter
|
||||
def updated_by(self, value: str):
|
||||
self._updated_by = value
|
||||
|
||||
@property
|
||||
def created_at(self) -> int:
|
||||
return self._created_at
|
||||
|
||||
@created_at.setter
|
||||
def created_at(self, value: int):
|
||||
self._created_at = value
|
||||
|
||||
@property
|
||||
def updated_at(self) -> int:
|
||||
return self._updated_at
|
||||
|
||||
@updated_at.setter
|
||||
def updated_at(self, value: int):
|
||||
self._updated_at = value
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value: str):
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
return self._slug
|
||||
|
||||
@slug.setter
|
||||
def slug(self, value: str):
|
||||
self._slug = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Playlist(" \
|
||||
f"id='{self.id}',\n" \
|
||||
f"name='{self.name}',\n" \
|
||||
f"nameslug='{self.slug}',\n" \
|
||||
f"enabled='{self.enabled}',\n" \
|
||||
f"created_by='{self.created_by}',\n" \
|
||||
f"updated_by='{self.updated_by}',\n" \
|
||||
f"created_at='{self.created_at}',\n" \
|
||||
f"updated_at='{self.updated_at}',\n" \
|
||||
f")"
|
||||
|
||||
def to_json(self, edits: dict = {}) -> str:
|
||||
obj = self.to_dict()
|
||||
|
||||
for k,v in edits.items():
|
||||
obj[k] = v
|
||||
|
||||
return json.dumps(obj)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
playlist = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"slug": self.slug,
|
||||
"enabled": self.enabled,
|
||||
"created_by": self.created_by,
|
||||
"updated_by": self.updated_by,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
return playlist
|
||||
@ -8,9 +8,10 @@ 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: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
|
||||
def __init__(self, location: str = '', playlist: Optional[int] = None, duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
|
||||
self._id = id if id else None
|
||||
self._location = location
|
||||
self._playlist = playlist
|
||||
self._duration = duration
|
||||
self._type = str_to_enum(type, SlideType) if isinstance(type, str) else type
|
||||
self._enabled = enabled
|
||||
@ -75,6 +76,14 @@ class Slide:
|
||||
def type(self, value: SlideType):
|
||||
self._type = value
|
||||
|
||||
@property
|
||||
def playlist(self) -> Optional[int]:
|
||||
return self._playlist
|
||||
|
||||
@playlist.setter
|
||||
def playlist(self, value: Optional[int]):
|
||||
self._playlist = value
|
||||
|
||||
@property
|
||||
def cron_schedule(self) -> Optional[str]:
|
||||
return self._cron_schedule
|
||||
@ -136,6 +145,7 @@ class Slide:
|
||||
f"updated_by='{self.updated_by}',\n" \
|
||||
f"created_at='{self.created_at}',\n" \
|
||||
f"updated_at='{self.updated_at}',\n" \
|
||||
f"playlist='{self.playlist}',\n" \
|
||||
f"cron_schedule='{self.cron_schedule}',\n" \
|
||||
f"cron_schedule_end='{self.cron_schedule_end}',\n" \
|
||||
f")"
|
||||
@ -161,6 +171,7 @@ class Slide:
|
||||
"updated_by": self.updated_by,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"playlist": self.playlist,
|
||||
"cron_schedule": self.cron_schedule,
|
||||
"cron_schedule_end": self.cron_schedule_end,
|
||||
}
|
||||
|
||||
@ -15,6 +15,11 @@ class HookType(Enum):
|
||||
H_FLEET_CSS = 'h_fleet_css'
|
||||
H_FLEET_JAVASCRIPT = 'h_fleet_javascript'
|
||||
|
||||
H_PLAYLIST_TOOLBAR_ACTIONS_START = 'h_playlist_toolbar_actions_start'
|
||||
H_PLAYLIST_TOOLBAR_ACTIONS_END = 'h_playlist_toolbar_actions_end'
|
||||
H_PLAYLIST_CSS = 'h_playlist_css'
|
||||
H_PLAYLIST_JAVASCRIPT = 'h_playlist_javascript'
|
||||
|
||||
H_AUTH_TOOLBAR_ACTIONS_START = 'h_auth_toolbar_actions_start'
|
||||
H_AUTH_TOOLBAR_ACTIONS_END = 'h_auth_toolbar_actions_end'
|
||||
H_AUTH_CSS = 'h_auth_css'
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from src.manager.PlaylistManager import PlaylistManager
|
||||
from src.manager.SlideManager import SlideManager
|
||||
from src.manager.ScreenManager import ScreenManager
|
||||
from src.manager.UserManager import UserManager
|
||||
@ -26,6 +27,7 @@ class ModelStore:
|
||||
|
||||
# Model
|
||||
self._screen_manager = ScreenManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
|
||||
self._playlist_manager = PlaylistManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
|
||||
self._slide_manager = SlideManager(lang_manager=self._lang_manager, database_manager=self._database_manager, user_manager=self._user_manager)
|
||||
self._variable_manager.reload()
|
||||
|
||||
@ -44,6 +46,9 @@ class ModelStore:
|
||||
def slide(self) -> SlideManager:
|
||||
return self._slide_manager
|
||||
|
||||
def playlist(self) -> PlaylistManager:
|
||||
return self._playlist_manager
|
||||
|
||||
def screen(self) -> ScreenManager:
|
||||
return self._screen_manager
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ from src.model.hook.HookRegistration import HookRegistration
|
||||
from src.model.hook.StaticHookRegistration import StaticHookRegistration
|
||||
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
|
||||
from src.constant.WebDirConstant import WebDirConstant
|
||||
from src.utils import get_safe_cron_descriptor, is_validate_cron_date_time
|
||||
from src.utils import get_safe_cron_descriptor, is_validate_cron_date_time, seconds_to_hhmmss
|
||||
|
||||
|
||||
class TemplateRenderer:
|
||||
@ -26,14 +26,16 @@ class TemplateRenderer:
|
||||
globals = dict(
|
||||
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
|
||||
SECRET_KEY=self._model_store.config().map().get('secret_key'),
|
||||
FLEET_ENABLED=self._model_store.variable().map().get('fleet_enabled').as_bool(),
|
||||
FLEET_COMPOSER_ENABLED=self._model_store.variable().map().get('fleet_composer_enabled').as_bool(),
|
||||
AUTH_ENABLED=self._model_store.variable().map().get('auth_enabled').as_bool(),
|
||||
PLAYLIST_ENABLED=self._model_store.variable().map().get('playlist_enabled').as_bool(),
|
||||
track_created=self._model_store.user().track_user_created,
|
||||
track_updated=self._model_store.user().track_user_updated,
|
||||
VERSION=self._model_store.config().map().get('version'),
|
||||
LANG=self._model_store.variable().map().get('lang').as_string(),
|
||||
HOOK=self._render_hook,
|
||||
cron_descriptor=self.cron_descriptor,
|
||||
seconds_to_hhmmss=seconds_to_hhmmss,
|
||||
is_validate_cron_date_time=is_validate_cron_date_time,
|
||||
l=self._model_store.lang().map(),
|
||||
t=self._model_store.lang().translate,
|
||||
|
||||
@ -12,6 +12,7 @@ from src.service.TemplateRenderer import TemplateRenderer
|
||||
from src.controller.PlayerController import PlayerController
|
||||
from src.controller.SlideshowController import SlideshowController
|
||||
from src.controller.FleetController import FleetController
|
||||
from src.controller.PlaylistController import PlaylistController
|
||||
from src.controller.AuthController import AuthController
|
||||
from src.controller.SysinfoController import SysinfoController
|
||||
from src.controller.SettingsController import SettingsController
|
||||
@ -106,6 +107,7 @@ class WebServer:
|
||||
SettingsController(self, self._app, auth_required, self._model_store, self._template_renderer)
|
||||
SysinfoController(self, self._app, auth_required, self._model_store, self._template_renderer)
|
||||
FleetController(self, self._app, auth_required, self._model_store, self._template_renderer)
|
||||
PlaylistController(self, self._app, auth_required, self._model_store, self._template_renderer)
|
||||
AuthController(self, self._app, auth_required, self._model_store, self._template_renderer)
|
||||
|
||||
def _setup_web_globals(self) -> None:
|
||||
|
||||
21
src/utils.py
21
src/utils.py
@ -1,7 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
import subprocess
|
||||
import unicodedata
|
||||
import platform
|
||||
|
||||
|
||||
@ -200,3 +202,22 @@ def get_yt_video_id(url: str) -> str:
|
||||
YouTube video id.
|
||||
"""
|
||||
return regex_search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", url, group=1)
|
||||
|
||||
|
||||
def slugify(value):
|
||||
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
|
||||
value = re.sub('[^\w\s-]', '', value).strip().lower()
|
||||
return re.sub('[-\s]+', '-', value)
|
||||
|
||||
|
||||
def seconds_to_hhmmss(seconds):
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
secs = seconds % 60
|
||||
return f"{hours:02}:{minutes:02}:{secs:02}"
|
||||
|
||||
|
||||
def randomize_filename(old_filename: str) -> str:
|
||||
new_uuid = str(uuid.uuid4())
|
||||
_, extension = os.path.splitext(old_filename)
|
||||
return f"{new_uuid}{extension}"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Obscreen Manager
|
||||
Description=Obscreen Composer
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@ -19,7 +19,9 @@
|
||||
<tr class="user-item" data-level="{{ user.id }}" data-entity="{{ user.to_json() }}">
|
||||
<td class="infos">
|
||||
<div class="inner">
|
||||
<div class="badge"><i class="fa fa-key icon-left"></i> {{ user.id }}</div>
|
||||
<i class="fa fa-user icon-left"></i>
|
||||
|
||||
{{ user.username }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/auth.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/auth/users.js"></script>
|
||||
{{ HOOK(H_AUTH_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
{{ HOOK(H_ROOT_CSS) }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="container {% block container_class %}{% endblock %}">
|
||||
{% set fleet_mode = request.args.get('fleet_mode') == '1' %}
|
||||
|
||||
{% block header %}
|
||||
@ -56,7 +56,14 @@
|
||||
<i class="fa-regular fa-clock"></i> {{ l.slideshow_page_title }}
|
||||
</a>
|
||||
</li>
|
||||
{% if FLEET_ENABLED %}
|
||||
{% if PLAYLIST_ENABLED %}
|
||||
<li class="{{ 'active' if request.url_rule.endpoint == 'playlist_list' }}">
|
||||
<a href="{{ url_for('playlist_list') }}">
|
||||
<i class="fa fa-bars-staggered"></i> {{ l.playlist_page_title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if FLEET_COMPOSER_ENABLED %}
|
||||
<li class="{{ 'active' if request.url_rule.endpoint == 'fleet_screen_list' }}">
|
||||
<a href="{{ url_for('fleet_screen_list') }}">
|
||||
<i class="fa fa-tv"></i> {{ l.fleet_page_title }}
|
||||
@ -115,7 +122,8 @@
|
||||
<script>
|
||||
var secret_key = '{{ SECRET_KEY }}';
|
||||
var l = {
|
||||
'js_slideshow_slide_delete_confirmation': '{{ l.slideshow_slide_delete_confirmation }}',
|
||||
'js_playlist_delete_confirmation': '{{ l.js_playlist_delete_confirmation }}',
|
||||
'js_slideshow_slide_delete_confirmation': '{{ l.js_slideshow_slide_delete_confirmation }}',
|
||||
'js_fleet_screen_delete_confirmation': '{{ l.js_fleet_screen_delete_confirmation }}',
|
||||
'js_auth_user_delete_confirmation': '{{ l.js_auth_user_delete_confirmation }}',
|
||||
'js_sysinfo_restart_confirmation': '{{ l.js_sysinfo_restart_confirmation }}',
|
||||
|
||||
@ -24,6 +24,9 @@
|
||||
<a href="javascript:void(0);" class="item-sort screen-sort">
|
||||
<i class="fa fa-sort icon-left"></i>
|
||||
</a>
|
||||
|
||||
<div class="badge"><i class="fa fa-key icon-left"></i> {{ screen.id }}</div>
|
||||
|
||||
<i class="fa fa-tv icon-left"></i>
|
||||
{{ screen.name }}
|
||||
</div>
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/tablednd-fixed.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/fleet.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/tablednd-fixed.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/fleet/screens.js"></script>
|
||||
{{ HOOK(H_FLEET_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
|
||||
{% if slide_animation_enabled.eval() %}
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/animate.min.css" />
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/animate.min.css" />
|
||||
{% endif %}
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: white; display: flex; flex-direction: row; justify-content: center; align-items: center; }
|
||||
@ -15,7 +15,7 @@
|
||||
.slide iframe { background: white; }
|
||||
.slide img { height: 100%; }
|
||||
</style>
|
||||
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/is-cron-now.js"></script>
|
||||
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/lib/is-cron-now.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="FirstSlide" class="slide" style="z-index: 1000;">
|
||||
@ -69,12 +69,13 @@
|
||||
};
|
||||
|
||||
var cronTick = function() {
|
||||
|
||||
|
||||
|
||||
if ((new Date()).getSeconds() != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('Cron Tick');
|
||||
|
||||
for (var i = 0; i < items.cron.length; i++) {
|
||||
var item = items.cron[i];
|
||||
|
||||
@ -98,13 +99,56 @@
|
||||
loadContent(curSlide, callbackReady, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function main() {
|
||||
preloadSlide('SecondSlide', items.loop[curItemIndex])
|
||||
cronState.interval = setInterval(cronTick, 1000);
|
||||
}
|
||||
|
||||
function preloadSlide(slide, item) {
|
||||
var element = document.getElementById(slide);
|
||||
var callbackReady = function (onSlideStart) {
|
||||
var move = function () {
|
||||
if (nextReady && !cronState.active) {
|
||||
moveToSlide(slide, item);
|
||||
onSlideStart();
|
||||
} else {
|
||||
setTimeout(move, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(move, duration);
|
||||
};
|
||||
|
||||
loadContent(element, callbackReady, item);
|
||||
}
|
||||
|
||||
function moveToSlide(slide, item) {
|
||||
curSlide = document.getElementById(slide);
|
||||
previousSlide = curSlide == firstSlide ? secondSlide : firstSlide;
|
||||
|
||||
duration = item.duration * 1000;
|
||||
curItemIndex = (curItemIndex + 1) === items.loop.length ? 0 : curItemIndex + 1;
|
||||
|
||||
curSlide.style.zIndex = 1000;
|
||||
previousSlide.style.zIndex = 500;
|
||||
|
||||
if (animate) {
|
||||
curSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
|
||||
curSlide.onanimationend = () => {
|
||||
curSlide.classList.remove(animate_transitions[0], animate_speed);
|
||||
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
|
||||
};
|
||||
previousSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
|
||||
previousSlide.onanimationend = () => {
|
||||
previousSlide.classList.remove(animate_transitions[1], animate_speed);
|
||||
};
|
||||
} else {
|
||||
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
function loadContent(element, callbackReady, item) {
|
||||
switch (item.type) {
|
||||
case 'url':
|
||||
@ -164,49 +208,6 @@
|
||||
callbackReady(onSlideStart);
|
||||
}
|
||||
|
||||
function preloadSlide(slide, item) {
|
||||
var element = document.getElementById(slide);
|
||||
var callbackReady = function (onSlideStart) {
|
||||
var move = function () {
|
||||
if (nextReady && !cronState.active) {
|
||||
moveToSlide(slide, item);
|
||||
onSlideStart();
|
||||
} else {
|
||||
setTimeout(move, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(move, duration);
|
||||
};
|
||||
|
||||
loadContent(element, callbackReady, item);
|
||||
}
|
||||
|
||||
function moveToSlide(slide, item) {
|
||||
curSlide = document.getElementById(slide);
|
||||
previousSlide = curSlide == firstSlide ? secondSlide : firstSlide;
|
||||
|
||||
duration = item.duration * 1000;
|
||||
curItemIndex = (curItemIndex + 1) === items.loop.length ? 0 : curItemIndex + 1;
|
||||
|
||||
curSlide.style.zIndex = 1000;
|
||||
previousSlide.style.zIndex = 500;
|
||||
|
||||
if (animate) {
|
||||
curSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
|
||||
curSlide.onanimationend = () => {
|
||||
curSlide.classList.remove(animate_transitions[0], animate_speed);
|
||||
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
|
||||
};
|
||||
previousSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
|
||||
previousSlide.onanimationend = () => {
|
||||
previousSlide.classList.remove(animate_transitions[1], animate_speed);
|
||||
};
|
||||
} else {
|
||||
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
56
views/playlist/component/table.jinja.html
Normal file
56
views/playlist/component/table.jinja.html
Normal file
@ -0,0 +1,56 @@
|
||||
<table class="{{ tclass }}-playlists">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ l.playlist_panel_th_name }}</th>
|
||||
<th class="tac">{{ l.playlist_panel_th_enabled }}</th>
|
||||
<th class="tac">{{ l.playlist_panel_th_duration }}</th>
|
||||
<th class="tac">{{ l.playlist_panel_th_activity }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="empty-tr {% if playlists|length != 0 %}hidden{% endif %}">
|
||||
<td colspan="4">
|
||||
{{ l.playlist_panel_empty|replace(
|
||||
'%link%',
|
||||
('<a href="javascript:void(0);" class="item-add playlist-add">'~l.playlist_button_add~'</a>')|safe
|
||||
) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% for playlist in playlists %}
|
||||
<tr class="playlist-item" data-level="{{ playlist.id }}" data-entity="{{ playlist.to_json() }}">
|
||||
<td class="infos">
|
||||
<div class="inner">
|
||||
{% if playlist.id %}
|
||||
<div class="badge"><i class="fa fa-key icon-left"></i> {{ playlist.id }}</div>
|
||||
{% else %}
|
||||
<div class="badge"><i class="fa fa-lock"></i></div>
|
||||
{% endif %}
|
||||
<i class="fa fa-bars-staggered icon-left"></i>
|
||||
|
||||
{{ playlist.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="tac">
|
||||
{% if playlist.id %}
|
||||
<label class="pure-material-switch">
|
||||
<input type="checkbox" {% if playlist.enabled %}checked="checked"{% endif %}><span></span>
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="tac">
|
||||
{{ seconds_to_hhmmss(durations[playlist.id]) }}
|
||||
</td>
|
||||
<td class="actions tac">
|
||||
{% if playlist.id %}
|
||||
<a href="javascript:void(0);" class="item-edit playlist-edit">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="item-delete playlist-delete">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
61
views/playlist/list.jinja.html
Normal file
61
views/playlist/list.jinja.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% extends 'base.jinja.html' %}
|
||||
|
||||
{% block page_title %}
|
||||
{{ l.playlist_page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
{{ HOOK(H_PLAYLIST_CSS) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/tablednd-fixed.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/playlist/playlists.js"></script>
|
||||
{{ HOOK(H_PLAYLIST_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="toolbar">
|
||||
<h2>{{ l.playlist_page_title }}</h2>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_START) }}
|
||||
<button class="purple playlist-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.playlist_button_add }}</button>
|
||||
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_END) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-error hidden">
|
||||
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<h3>{{ l.playlist_panel_active }}</h3>
|
||||
|
||||
{% with tclass='active', playlists=enabled_playlists %}
|
||||
{% include 'playlist/component/table.jinja.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-inactive">
|
||||
<div class="panel-body">
|
||||
<h3>{{ l.playlist_panel_inactive }}</h3>
|
||||
|
||||
{% with tclass='inactive', playlists=disabled_playlists %}
|
||||
{% include 'playlist/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 'playlist/modal/add.jinja.html' %}
|
||||
{% include 'playlist/modal/edit.jinja.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
views/playlist/modal/add.jinja.html
Normal file
25
views/playlist/modal/add.jinja.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="modal modal-playlist-add modal-playlist">
|
||||
<h2>
|
||||
{{ l.playlist_form_add_title }}
|
||||
</h2>
|
||||
|
||||
<form action="/playlist/add" method="POST" enctype="multipart/form-data">
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playlist-add-name">{{ l.playlist_form_label_name }}</label>
|
||||
<div class="widget">
|
||||
<input name="name" type="text" id="playlist-add-name" required="required" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-normal modal-close">
|
||||
{{ l.playlist_form_button_cancel }}
|
||||
</button>
|
||||
<button type="submit" class="green">
|
||||
<i class="fa fa-plus icon-left"></i> {{ l.playlist_form_add_submit }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
27
views/playlist/modal/edit.jinja.html
Normal file
27
views/playlist/modal/edit.jinja.html
Normal file
@ -0,0 +1,27 @@
|
||||
<div class="modal modal-playlist-edit modal-playlist hidden">
|
||||
<h2>
|
||||
{{ l.playlist_form_edit_title }}
|
||||
</h2>
|
||||
|
||||
|
||||
<form action="/playlist/edit" method="POST">
|
||||
|
||||
<input type="hidden" name="id" id="playlist-edit-id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playlist-edit-name">{{ l.playlist_form_label_name }}</label>
|
||||
<div class="widget">
|
||||
<input type="text" name="name" id="playlist-edit-name" required="required" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-normal modal-close">
|
||||
{{ l.playlist_form_button_cancel }}
|
||||
</button>
|
||||
<button type="submit" class="green">
|
||||
<i class="fa fa-save icon-left"></i>{{ l.playlist_form_edit_submit }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -5,18 +5,20 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/flatpickr.min.css" />
|
||||
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css" />
|
||||
{{ HOOK(H_SLIDESHOW_CSS) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script src="{{ STATIC_PREFIX }}js/flatpickr.min.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/tablednd-fixed.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/flatpickr.min.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/lib/tablednd-fixed.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/slideshow/slides.js"></script>
|
||||
<script src="{{ STATIC_PREFIX }}js/restart.js"></script>
|
||||
{{ HOOK(H_SLIDESHOW_JAVASCRIPT) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block container_class %}{% if PLAYLIST_ENABLED %}expand{% endif %}{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="toolbar">
|
||||
<h2>{{ l.slideshow_page_title }}</h2>
|
||||
@ -28,7 +30,7 @@
|
||||
{{ HOOK(H_FLEETMODE_SLIDESHOW_TOOLBAR_ACTIONS) }}
|
||||
{% endif %}
|
||||
|
||||
<a href="/" target="_blank" class="btn" title="{{ l.slideshow_goto_player }}">
|
||||
<a href="{% if current_playlist %}{{ url_for('player_use', playlist_slug_or_id=current_playlist.slug) }}{% else %}{{ url_for('player') }}{% endif %}" target="_blank" class="btn" title="{{ l.slideshow_goto_player }}">
|
||||
<i class="fa fa-play"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('slideshow_player_refresh') }}" class="btn" title="{{ l.slideshow_refresh_player }}">
|
||||
@ -36,6 +38,7 @@
|
||||
</a>
|
||||
|
||||
<button class="purple slide-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.slideshow_slide_button_add }}</button>
|
||||
|
||||
{{ HOOK(H_SLIDESHOW_TOOLBAR_ACTIONS_END) }}
|
||||
</div>
|
||||
</div>
|
||||
@ -47,22 +50,57 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<h3>{{ l.slideshow_slide_panel_active }}</h3>
|
||||
|
||||
{% with tclass='active', slides=enabled_slides %}
|
||||
{% include 'slideshow/component/table.jinja.html' %}
|
||||
{% endwith %}
|
||||
<div class="explorer">
|
||||
{% if PLAYLIST_ENABLED %}
|
||||
<div class="left">
|
||||
<div class="panel panel-menu">
|
||||
<div class="panel-body">
|
||||
<h3>
|
||||
{{ l.slideshow_playlist_panel_title }}
|
||||
</h3>
|
||||
<ul>
|
||||
<li class="{% if not current_playlist %}active{% endif %}">
|
||||
<a href="{{ url_for('slideshow_slide_list') }}" class="select-playlist">
|
||||
{% if not current_playlist %}
|
||||
<i class="fa fa-play icon-left"></i>
|
||||
{% endif %}
|
||||
{{ l.slideshow_playlist_panel_item_default }}
|
||||
</a>
|
||||
</li>
|
||||
{% for playlist in playlists %}
|
||||
<li class="{% if current_playlist %}active{% endif %}">
|
||||
<a href="{{ url_for('slideshow_slide_list_playlist_use', playlist_id=playlist.id) }}" class="select-playlist">
|
||||
{% if current_playlist %}
|
||||
<i class="fa fa-play icon-left"></i>
|
||||
{% endif %}
|
||||
{{ playlist.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-inactive">
|
||||
<div class="panel-body">
|
||||
<h3>{{ l.slideshow_slide_panel_inactive }}</h3>
|
||||
{% endif %}
|
||||
<div class="right">
|
||||
<div class="panel {% if PLAYLIST_ENABLED %}panel-active{% endif %}">
|
||||
<div class="panel-body">
|
||||
<h3>{{ l.slideshow_slide_panel_active }}</h3>
|
||||
|
||||
{% with tclass='inactive', slides=disabled_slides %}
|
||||
{% include 'slideshow/component/table.jinja.html' %}
|
||||
{% endwith %}
|
||||
{% with tclass='active', slides=enabled_slides %}
|
||||
{% include 'slideshow/component/table.jinja.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-inactive">
|
||||
<div class="panel-body">
|
||||
<h3>{{ l.slideshow_slide_panel_inactive }}</h3>
|
||||
|
||||
{% with tclass='inactive', slides=disabled_slides %}
|
||||
{% include 'slideshow/component/table.jinja.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,6 +9,10 @@
|
||||
{{ l.slideshow_slide_form_section_content }}
|
||||
</h3>
|
||||
|
||||
{% if current_playlist %}
|
||||
<input name="playlist" type="text" id="slide-add-playlist" value="{{ current_playlist.id }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="slide-add-name">{{ l.slideshow_slide_form_label_name }}</label>
|
||||
<div class="widget">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user