crud for slide in gui
This commit is contained in:
parent
ab7cc46de3
commit
bf5a67a05f
@ -1,7 +1,7 @@
|
||||
# Obscreen
|
||||
|
||||
## About
|
||||
Use a RaspberryPi to show a full-screen Slideshow (Kiosk-mode)
|
||||
Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
config = {
|
||||
"config": False,
|
||||
"port": 5000,
|
||||
"reverse_proxy_mode": False,
|
||||
"lang": "en",
|
||||
"lxfile": '/home/pi/.config/lxsession/LXDE-pi/autostart'
|
||||
"config": False, # Enable autoreload for html/jinja files
|
||||
"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"
|
||||
"lxfile": '/home/pi/.config/lxsession/LXDE-pi/autostart' # Path to lx autostart file
|
||||
}
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 0,
|
||||
"location": "data/uploads/sample.jpg",
|
||||
"delay": 10,
|
||||
"type": "picture"
|
||||
"duration": 10,
|
||||
"type": "picture",
|
||||
"enabled": true,
|
||||
"name": "Picture Sample",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"location": "https://unix.org",
|
||||
"delay": 20,
|
||||
"type": "url"
|
||||
"duration": 20,
|
||||
"type": "url",
|
||||
"enabled": true,
|
||||
"name": "URL Sample",
|
||||
"position": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
535
data/www/css/manage.css
Normal file
535
data/www/css/manage.css
Normal file
@ -0,0 +1,535 @@
|
||||
* {
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
background-color: #0f0035;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
background-color: #0f0035;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tac {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 70%;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
min-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
padding: 0 25px 0 25px;
|
||||
}
|
||||
|
||||
header .logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header .logo img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
header menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
header menu ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header menu ul li {
|
||||
margin: 0 15px;
|
||||
|
||||
}
|
||||
|
||||
header menu ul li a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header menu ul li a:hover,
|
||||
header menu ul li.active a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 25px 0 25px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
padding: 0 25px 0 0;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-actions {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ffe333;
|
||||
color: #270035;
|
||||
padding: 10px 30px;
|
||||
text-decoration: none;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
transition: .2s linear all;
|
||||
}
|
||||
|
||||
button.purple {
|
||||
font-weight: bold;
|
||||
border: 1px solid #692fbd;
|
||||
color: white;
|
||||
background: #692fbd;
|
||||
background: linear-gradient(90deg, #bc48ff 0%, #692fbd 100%);
|
||||
}
|
||||
|
||||
button.green {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
border: 1px solid #0eef5f;
|
||||
background: linear-gradient(90deg, #2fde6f 0%, #13c251 100%);
|
||||
}
|
||||
|
||||
button.normal:hover {
|
||||
color: white;
|
||||
border-color: #692fbd;
|
||||
background: #692fbd;
|
||||
background: linear-gradient(90deg, #bc48ff 0%, #692fbd 100%);
|
||||
}
|
||||
|
||||
button.green:hover,
|
||||
button.purple:hover {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
|
||||
.panel {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 5px;
|
||||
padding: 0 25px 80px 25px;
|
||||
margin: 10px 25px;
|
||||
border-left: 5px solid #0eef5f;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.panel h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.panel-inactive {
|
||||
background: white;
|
||||
color: #AAA;
|
||||
border-color: #BBB;
|
||||
}
|
||||
|
||||
.panel-inactive h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.panel table {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.panel th {
|
||||
border-bottom: 1px solid #fff;
|
||||
border-collapse: collapse;
|
||||
padding: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.panel-inactive th {
|
||||
border-color: #EEE;
|
||||
|
||||
}
|
||||
|
||||
.panel td {
|
||||
border-collapse: collapse;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.panel td a.slide-sort {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.panel td a.slide-name {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-inactive td a.slide-name {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
.panel td a.slide-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.panel td.actions a {
|
||||
background: white;
|
||||
color: #333;
|
||||
border: 1px solid #AAA;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
width: 35px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.panel td.actions a:hover {
|
||||
color: #0eef5f;
|
||||
border-color: #0eef5f;
|
||||
}
|
||||
|
||||
.panel td.actions a.slide-edit:hover {
|
||||
color: #bc48ff;
|
||||
border-color: #bc48ff;
|
||||
}
|
||||
|
||||
.panel td.actions a.slide-delete:hover {
|
||||
color: #ef0e0e;
|
||||
border-color: #ef0e0e;
|
||||
}
|
||||
|
||||
.panel td.infos {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 400px;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel td.infos .inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel a {
|
||||
color: #0eef5f;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.panel a:hover {
|
||||
color: #0bc44e;
|
||||
}
|
||||
|
||||
.icon-right {
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.icon-left {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.pure-material-switch {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pure-material-switch > input {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: -8px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: rgba(0, 0, 0, 0.38);
|
||||
outline: none;
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s 0.1s, transform 0.2s 0.1s;
|
||||
}
|
||||
|
||||
.pure-material-switch > span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pure-material-switch > span::before {
|
||||
content: "";
|
||||
float: right;
|
||||
display: inline-block;
|
||||
margin: 5px 0 5px 10px;
|
||||
border-radius: 7px;
|
||||
width: 36px;
|
||||
height: 14px;
|
||||
background-color: rgba(0, 0, 0, 0.38);
|
||||
vertical-align: top;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
.pure-material-switch > span::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 16px;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.pure-material-switch > input:checked {
|
||||
right: -10px;
|
||||
background-color: rgb(14, 239, 95);
|
||||
}
|
||||
|
||||
.pure-material-switch > input:checked + span::before {
|
||||
background-color: rgba(14, 239, 95, 0.6);
|
||||
}
|
||||
|
||||
.pure-material-switch > input:checked + span::after {
|
||||
background-color: rgb(14, 239, 95);
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.pure-material-switch:hover > input {
|
||||
opacity: 0.04;
|
||||
}
|
||||
|
||||
.pure-material-switch > input:focus {
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.pure-material-switch:hover > input:focus {
|
||||
opacity: 0.16;
|
||||
}
|
||||
|
||||
.pure-material-switch > input:active {
|
||||
opacity: 1;
|
||||
transform: scale(0);
|
||||
transition: transform 0s, opacity 0s;
|
||||
}
|
||||
|
||||
.pure-material-switch > input:active + span::before {
|
||||
background-color: rgba(14, 239, 95, 0.6);
|
||||
}
|
||||
|
||||
.pure-material-switch > input:checked:active + span::before {
|
||||
background-color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.pure-material-switch > input:disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pure-material-switch > input:disabled + span {
|
||||
color: rgb(0, 0, 0);
|
||||
opacity: 0.38;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pure-material-switch > input:disabled + span::before {
|
||||
background-color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.pure-material-switch > input:checked:disabled + span::before {
|
||||
background-color: rgba(14, 239, 95, 0.6);
|
||||
}
|
||||
|
||||
.modals {
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modals-outer {
|
||||
min-width: 30%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modals-outer .modal-close {
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-top: -100px;
|
||||
}
|
||||
|
||||
.modals-inner {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modals-inner .modal h2 {
|
||||
border-bottom: 1px solid #DDD;
|
||||
padding: 15px 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
form .form-group {
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
form .form-group label {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
text-align: right;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
form .form-group .widget {
|
||||
flex: 3;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
form .form-group input,
|
||||
form .form-group select,
|
||||
form .form-group textarea {
|
||||
flex: 1;
|
||||
padding: 10px 5px 10px 5px;
|
||||
border: 1px solid #EEE;
|
||||
border-radius: 4px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
form .form-group span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
form .actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
form .actions button.green,
|
||||
form .actions button {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
form .actions button.green:hover {
|
||||
background: white;
|
||||
color: rgb(14, 239, 95);
|
||||
border-color: rgb(14, 239, 95)
|
||||
}
|
||||
|
||||
form .actions button.modal-close {
|
||||
color: #999;
|
||||
border-color: #999;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form .actions button.modal-close:hover {
|
||||
color: #555;
|
||||
border-color: #555;
|
||||
}
|
||||
126
data/www/js/manage.js
Normal file
126
data/www/js/manage.js
Normal file
@ -0,0 +1,126 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
var $tableActive = $('table.active-slides');
|
||||
var $tableInactive = $('table.inactive-slides');
|
||||
var $modalsRoot = $('.modals');
|
||||
|
||||
var getId = function($el) {
|
||||
return $el.is('tr') ? $el.attr('data-level') : $el.parents('tr:eq(0)').attr('data-level');
|
||||
};
|
||||
|
||||
var updateTable = 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();
|
||||
}
|
||||
|
||||
var showModal = function (modalClass) {
|
||||
$modalsRoot.removeClass('hidden').find('form').trigger('reset');
|
||||
$modalsRoot.find('.modal').addClass('hidden');
|
||||
$modalsRoot.find('.modal.' + modalClass).removeClass('hidden');
|
||||
};
|
||||
|
||||
var hideModal = function () {
|
||||
$modalsRoot.addClass('hidden').find('form').trigger('reset');
|
||||
};
|
||||
|
||||
var updatePositions = function (table, row) {
|
||||
var positions = {};
|
||||
$('.slide-item').each(function(index) {
|
||||
positions[getId($(this))] = index;
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/manage/slide/position',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify(positions),
|
||||
});
|
||||
};
|
||||
|
||||
var main = function() {
|
||||
$("table").tableDnD({
|
||||
dragHandle: 'td a.slide-sort',
|
||||
onDrop: updatePositions
|
||||
});
|
||||
};
|
||||
|
||||
$(document).on('change', 'input[type=checkbox]', function () {
|
||||
$.ajax({
|
||||
url: 'manage/slide/toggle',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify({id: getId($(this)), enabled: $(this).is(':checked')}),
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
var $tr = $(this).parents('tr:eq(0)').remove().clone();
|
||||
|
||||
if ($(this).is(':checked')) {
|
||||
$tableActive.append($tr);
|
||||
} else {
|
||||
console.log($tableInactive)
|
||||
$tableInactive.append($tr);
|
||||
}
|
||||
|
||||
updateTable();
|
||||
});
|
||||
|
||||
$(document).on('change', '#slide-add-type', function () {
|
||||
var value = $(this).val();
|
||||
var inputType = $(this).find('option').filter(function (i, el) {
|
||||
return $(el).val() === value;
|
||||
}).data('input');
|
||||
|
||||
$('.slide-add-object-input')
|
||||
.addClass('hidden')
|
||||
.prop('disabled', true)
|
||||
.filter('#slide-add-object-input-' + inputType)
|
||||
.removeClass('hidden')
|
||||
.prop('disabled', false)
|
||||
;
|
||||
});
|
||||
|
||||
$(document).on('click', '.modal-close', function () {
|
||||
hideModal();
|
||||
});
|
||||
|
||||
$(document).on('click', '.slide-add', function () {
|
||||
showModal('modal-slide-add');
|
||||
});
|
||||
|
||||
$(document).on('click', '.slide-edit', function () {
|
||||
var slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
|
||||
console.log(slide)
|
||||
showModal('modal-slide-edit');
|
||||
$('#slide-edit-name').val(slide.name);
|
||||
$('#slide-edit-type').val(slide.type);
|
||||
$('#slide-edit-duration').val(slide.duration);
|
||||
$('#slide-edit-id').val(slide.id);
|
||||
});
|
||||
|
||||
$(document).on('click', '.slide-delete', function () {
|
||||
if (confirm('Are you sure ?')) {
|
||||
var $tr = $(this).parents('tr:eq(0)');
|
||||
$tr.remove();
|
||||
updateTable();
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: '/manage/slide/delete',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify({id: getId($(this))}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).keyup(function (e) {
|
||||
if (e.key === "Escape") {
|
||||
hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
main();
|
||||
});
|
||||
1
data/www/js/tablednd-fixed.js
Normal file
1
data/www/js/tablednd-fixed.js
Normal file
File diff suppressed because one or more lines are too long
62
obscreen.py
62
obscreen.py
@ -7,21 +7,14 @@ import subprocess
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory
|
||||
from pysondb import db
|
||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
|
||||
from config import config
|
||||
from src.SlideManager import SlideManager
|
||||
|
||||
|
||||
# <classes>
|
||||
class ItemType(Enum):
|
||||
PICTURE = 'picture'
|
||||
VIDEO = 'video'
|
||||
URL = 'url'
|
||||
# </classes>
|
||||
|
||||
# <config>
|
||||
PLAYER_URL = 'http://localhost:{}'.format(config['port'])
|
||||
DB = db.getDb("data/slideshow.json")
|
||||
slide_manager = SlideManager()
|
||||
with open('./lang/{}.json'.format(config['lang']), 'r') as file:
|
||||
LANGDICT = json.load(file)
|
||||
# </config>
|
||||
@ -82,7 +75,11 @@ def get_ip_address():
|
||||
# <web>
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('player.jinja.html', items=json.dumps(DB.getAll()))
|
||||
return render_template('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()))
|
||||
|
||||
@app.route('/slide/default')
|
||||
def slide_default():
|
||||
@ -90,13 +87,50 @@ def slide_default():
|
||||
|
||||
@app.route('/manage')
|
||||
def manage():
|
||||
return render_template('manage.jinja.html', ipaddr=get_ip_address(), l=LANGDICT)
|
||||
return render_template(
|
||||
'manage.jinja.html',
|
||||
ipaddr=get_ip_address(),
|
||||
l=LANGDICT,
|
||||
enabled_slides=slide_manager.get_enabled_slides(),
|
||||
disabled_slides=slide_manager.get_disabled_slides()
|
||||
)
|
||||
|
||||
@app.route('/manage/slide/add', methods=['POST'])
|
||||
def manage_slide_add():
|
||||
name = request.form['name']
|
||||
print(name)
|
||||
print(request.form)
|
||||
response = {'message': f'Bonjour {name}, votre formulaire a été reçu !'}
|
||||
return jsonify(response)
|
||||
|
||||
@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):
|
||||
return send_from_directory('data', '404.html'), 404
|
||||
return send_from_directory('views', 'error404.html'), 404
|
||||
# </web>
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=config['port'])
|
||||
app.run(host=config['bind'] if 'bind' in config else '0.0.0.0', port=config['port'])
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
flask
|
||||
pysondb
|
||||
uuid
|
||||
|
||||
54
src/SlideManager.py
Normal file
54
src/SlideManager.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
|
||||
from typing import Dict, Optional, List, Tuple
|
||||
from src.model.Slide import Slide
|
||||
from pysondb import db
|
||||
|
||||
class SlideManager():
|
||||
|
||||
DB_FILE = "data/slideshow.json"
|
||||
|
||||
def __init__(self):
|
||||
self._db = db.getDb(self.DB_FILE)
|
||||
|
||||
@staticmethod
|
||||
def hydrate_object(raw_slide) -> Slide:
|
||||
return Slide(**raw_slide)
|
||||
|
||||
@staticmethod
|
||||
def hydrate_list(raw_slides) -> List[Slide]:
|
||||
return [SlideManager.hydrate_object(raw_slide) for raw_slide in raw_slides]
|
||||
|
||||
def get_all(self, sort: bool = False) -> List[Slide]:
|
||||
raw_slides = self._db.getAll()
|
||||
return SlideManager.hydrate_list(sorted(raw_slides, key=lambda x: x['position']) if sort else raw_slides)
|
||||
|
||||
def get_enabled_slides(self) -> List[Slide]:
|
||||
return [slide for slide in self.get_all(sort=True) if slide.enabled]
|
||||
|
||||
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:
|
||||
self._db.updateById(id, {"enabled": enabled, "position": 999})
|
||||
|
||||
def update_positions(self, positions: list) -> None:
|
||||
for slide_id, slide_position in positions.items():
|
||||
self._db.updateById(slide_id, {"position": slide_position})
|
||||
|
||||
def update_form(self, id: str, name: str, duration: int) -> None:
|
||||
self._db.updateById(id, {"name": name, "duration": duration})
|
||||
|
||||
def reindent(self) -> None:
|
||||
with open(self.DB_FILE, 'r') as file:
|
||||
data = json.load(file)
|
||||
|
||||
with open(self.DB_FILE, 'w', encoding='utf-8') as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
|
||||
def delete(self, id: int) -> None:
|
||||
self._db.deleteById(id)
|
||||
self.reindent()
|
||||
|
||||
def to_dict(self, slides: List[Slide]) -> dict:
|
||||
return [slide.to_dict() for slide in slides]
|
||||
94
src/model/Slide.py
Normal file
94
src/model/Slide.py
Normal file
@ -0,0 +1,94 @@
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from typing import Optional
|
||||
from src.model import SlideType
|
||||
|
||||
|
||||
class Slide:
|
||||
|
||||
def __init__(self, location: str, duration: int, type: SlideType, enabled: bool, name: str, position: int = 999, id: Optional[int] = None):
|
||||
self._id = uuid.uuid4().int if id is None else id
|
||||
self._location = location
|
||||
self._duration = duration
|
||||
self._type = type
|
||||
self._enabled = enabled
|
||||
self._name = name
|
||||
self._position = position
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def location(self) -> str:
|
||||
return self._location
|
||||
|
||||
@location.setter
|
||||
def location(self, value: str):
|
||||
self._location = value
|
||||
|
||||
@property
|
||||
def type(self) -> SlideType:
|
||||
return self._type
|
||||
|
||||
@type.setter
|
||||
def type(self, value: SlideType):
|
||||
self._type = value
|
||||
|
||||
@property
|
||||
def duration(self) -> int:
|
||||
return self._duration
|
||||
|
||||
@duration.setter
|
||||
def duration(self, value: int):
|
||||
self._duration = 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"type='{self.type}',\n" \
|
||||
f"enabled='{self.enabled}',\n" \
|
||||
f"duration='{self.duration}',\n" \
|
||||
f"position='{self.position}',\n" \
|
||||
f"location='{self.location}',\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,
|
||||
"type": self.type,
|
||||
"duration": self.duration,
|
||||
"location": self.location,
|
||||
}
|
||||
6
src/model/SlideType.py
Normal file
6
src/model/SlideType.py
Normal file
@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
class ItemType(Enum):
|
||||
PICTURE = 'picture'
|
||||
VIDEO = 'video'
|
||||
URL = 'url'
|
||||
File diff suppressed because one or more lines are too long
@ -1,52 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<title>
|
||||
Obscreen
|
||||
</title>
|
||||
<title>Obscreen</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="google" content="notranslate">
|
||||
<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;
|
||||
}
|
||||
|
||||
.slide {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.slide, iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.slide iframe {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.slide img {
|
||||
height: 100%;
|
||||
}
|
||||
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: white; display: flex; flex-direction: row; justify-content: center; align-items: center; }
|
||||
.slide { display: flex; flex-direction: row; justify-content: center; align-items: center; background: black; }
|
||||
.slide, iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; padding-top: 0; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; }
|
||||
.slide iframe { background: white; }
|
||||
.slide img { height: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -57,114 +20,128 @@
|
||||
<iframe src="/slide/default"></iframe>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var items = {{items | safe}};
|
||||
var delay = 3000 / 1;
|
||||
var curUrl = 0;
|
||||
var nextReady = true;
|
||||
preloadIntoFirstSlide();
|
||||
var items = {{items | safe}};
|
||||
var duration = 3000 / 1;
|
||||
var playlistCheck = 10 * 1000; // 10 seconds check
|
||||
var curUrl = 0;
|
||||
var nextReady = true;
|
||||
var itemCheck = setInterval(function () {
|
||||
fetch('playlist').then(function(response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
}).then(function(data) {
|
||||
items = data;
|
||||
});
|
||||
}, playlistCheck);
|
||||
|
||||
function loadContent(element, callbackReady) {
|
||||
switch (items[curUrl].type) {
|
||||
case 'url':
|
||||
loadUrl(element, callbackReady);
|
||||
break;
|
||||
case 'picture':
|
||||
loadPicture(element, callbackReady);
|
||||
break;
|
||||
case 'video':
|
||||
loadVideo(element, callbackReady);
|
||||
break;
|
||||
default:
|
||||
loadUrl(element, callbackReady);
|
||||
break;
|
||||
}
|
||||
function main() {
|
||||
preloadIntoFirstSlide();
|
||||
}
|
||||
|
||||
function loadUrl(element, callbackReady) {
|
||||
element.innerHTML = `<iframe src="${items[curUrl].location}"></iframe>`;
|
||||
callbackReady(function () {});
|
||||
}
|
||||
function loadContent(element, callbackReady) {
|
||||
switch (items[curUrl].type) {
|
||||
case 'url':
|
||||
loadUrl(element, callbackReady);
|
||||
break;
|
||||
case 'picture':
|
||||
loadPicture(element, callbackReady);
|
||||
break;
|
||||
case 'video':
|
||||
loadVideo(element, callbackReady);
|
||||
break;
|
||||
default:
|
||||
loadUrl(element, callbackReady);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function loadPicture(element, callbackReady) {
|
||||
element.innerHTML = `<img src="${items[curUrl].location}" alt="" />`;
|
||||
callbackReady(function () {});
|
||||
}
|
||||
function loadUrl(element, callbackReady) {
|
||||
element.innerHTML = `<iframe src="${items[curUrl].location}"></iframe>`;
|
||||
callbackReady(function () {});
|
||||
}
|
||||
|
||||
function loadVideo(element, callbackReady) {
|
||||
element.innerHTML = `<video><source src=${items[curUrl].location} type="video/mp4" /></video>`;
|
||||
var video = element.querySelector('video');
|
||||
items[curUrl].handle = video;
|
||||
video.addEventListener('loadedmetadata', function () {
|
||||
items[curUrl].delay = Math.ceil(video.duration);
|
||||
});
|
||||
function loadPicture(element, callbackReady) {
|
||||
element.innerHTML = `<img src="${items[curUrl].location}" alt="" />`;
|
||||
callbackReady(function () {});
|
||||
}
|
||||
|
||||
callbackReady(function () {
|
||||
nextReady = false;
|
||||
video.play();
|
||||
video.addEventListener('ended', function () {
|
||||
nextReady = true;
|
||||
});
|
||||
setTimeout(function() {
|
||||
nextReady = true;
|
||||
}, Math.ceil(items[curUrl].delay * 1.5));
|
||||
});
|
||||
}
|
||||
function loadVideo(element, callbackReady) {
|
||||
element.innerHTML = `<video><source src=${items[curUrl].location} type="video/mp4" /></video>`;
|
||||
var video = element.querySelector('video');
|
||||
video.addEventListener('loadedmetadata', function () {
|
||||
items[curUrl].duration = Math.ceil(video.duration);
|
||||
});
|
||||
|
||||
function preloadIntoFirstSlide() {
|
||||
console.log('preloadIntoFirstSlide', items[curUrl]);
|
||||
var element = document.getElementById('FirstSlide');
|
||||
var callbackReady = function (onSlideStart) {
|
||||
var move = function () {
|
||||
if (nextReady) {
|
||||
moveToFirstSlide();
|
||||
onSlideStart();
|
||||
} else {
|
||||
setTimeout(move, 1000);
|
||||
}
|
||||
}
|
||||
callbackReady(function () {
|
||||
nextReady = false;
|
||||
video.play();
|
||||
video.addEventListener('ended', function () {
|
||||
nextReady = true;
|
||||
});
|
||||
setTimeout(function () {
|
||||
nextReady = true;
|
||||
}, Math.ceil(items[curUrl].duration * 1.5));
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(move, delay);
|
||||
};
|
||||
function preloadIntoFirstSlide() {
|
||||
//console.log('preloadIntoFirstSlide', items[curUrl]);
|
||||
var element = document.getElementById('FirstSlide');
|
||||
var callbackReady = function (onSlideStart) {
|
||||
var move = function () {
|
||||
if (nextReady) {
|
||||
moveToFirstSlide();
|
||||
onSlideStart();
|
||||
} else {
|
||||
setTimeout(move, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(move, duration);
|
||||
};
|
||||
|
||||
loadContent(element, callbackReady);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToFirstSlide() {
|
||||
console.log('moveToFirstSlide', items[curUrl]);
|
||||
delay = items[curUrl].delay * 1000;
|
||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
||||
document.querySelector('#FirstSlide').style.visibility = 'visible';
|
||||
document.querySelector('#SecondSlide').style.visibility = 'hidden';
|
||||
preloadIntoSecondSlide();
|
||||
}
|
||||
function moveToFirstSlide() {
|
||||
//console.log('moveToFirstSlide', items[curUrl]);
|
||||
duration = items[curUrl].duration * 1000;
|
||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
||||
document.querySelector('#FirstSlide').style.visibility = 'visible';
|
||||
document.querySelector('#SecondSlide').style.visibility = 'hidden';
|
||||
preloadIntoSecondSlide();
|
||||
}
|
||||
|
||||
function preloadIntoSecondSlide() {
|
||||
console.log('preloadIntoSecondSlide', items[curUrl]);
|
||||
var element = document.getElementById('SecondSlide');
|
||||
var callbackReady = function (onSlideStart) {
|
||||
var move = function () {
|
||||
if (nextReady) {
|
||||
moveToSecondSlide();
|
||||
onSlideStart();
|
||||
} else {
|
||||
setTimeout(move, 1000);
|
||||
}
|
||||
}
|
||||
function preloadIntoSecondSlide() {
|
||||
//console.log('preloadIntoSecondSlide', items[curUrl]);
|
||||
var element = document.getElementById('SecondSlide');
|
||||
var callbackReady = function (onSlideStart) {
|
||||
var move = function () {
|
||||
if (nextReady) {
|
||||
moveToSecondSlide();
|
||||
onSlideStart();
|
||||
} else {
|
||||
setTimeout(move, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(move, delay);
|
||||
};
|
||||
setTimeout(move, duration);
|
||||
};
|
||||
|
||||
loadContent(element, callbackReady);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToSecondSlide() {
|
||||
console.log('moveToSecondSlide', items[curUrl]);
|
||||
delay = items[curUrl].delay * 1000;
|
||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
||||
document.querySelector('#FirstSlide').style.visibility = 'hidden';
|
||||
document.querySelector('#SecondSlide').style.visibility = 'visible';
|
||||
preloadIntoFirstSlide();
|
||||
}
|
||||
function moveToSecondSlide() {
|
||||
//console.log('moveToSecondSlide', items[curUrl]);
|
||||
duration = items[curUrl].duration * 1000;
|
||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
||||
document.querySelector('#FirstSlide').style.visibility = 'hidden';
|
||||
document.querySelector('#SecondSlide').style.visibility = 'visible';
|
||||
preloadIntoFirstSlide();
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user