This commit is contained in:
jr-k 2024-07-15 23:43:18 +02:00
parent e2ba157fdc
commit d3ef3c16a2
56 changed files with 1949 additions and 1610 deletions

File diff suppressed because one or more lines are too long

BIN
data/www/img/logo3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -105,7 +105,7 @@ jQuery(function ($) {
route = $(this).attr('data-entity-route') + '?id=' + $item.attr('data-id');
}
if (confirm(l.common_are_you_sure)) {
if (confirm(l.js_common_are_you_sure)) {
document.location.href = route;
}
});
@ -134,8 +134,14 @@ jQuery(function ($) {
selectEpxlrLink(verticalNeighbors.above.find('.explr-link'));
} else if (e.key === "ArrowDown" && verticalNeighbors.below) {
selectEpxlrLink(verticalNeighbors.below.find('.explr-link'));
} else if (e.key === "Backspace") {
if ($('.explr-item-delete:visible').length) {
$('.explr-item-delete:visible').click();
}
}
} else if (e.key.indexOf('Arrow') === 0) {
selectEpxlrLink($('.explr-dirview li:visible:eq(0)').find('.explr-link'));
}
});
// Explorer item selection

View File

@ -122,5 +122,17 @@ jQuery(document).ready(function ($) {
$firstInputText.focus();
}
}
$(document).on('click', '.copy-link', function (e) {
e.preventDefault();
const $input = $('#' + $(this).attr('data-target-id'));
$input.select();
$input[0].setSelectionRange(0, 99999);
document.execCommand("copy");
if (navigator.clipboard) {
navigator.clipboard.writeText($input.val());
}
});
});

View File

@ -159,10 +159,8 @@
.find('ul') // hide every ul
.hide()
.end()
.find('.explr-expand') // unless explicitly set to expand
.show()
.siblings('.explr-toggler')
.addClass('explr-minus '+opts.classesMinus);
.find('.explr-toggler')
.addClass('explr-plus '+opts.classesPlus);
} else {
$tree
.find('.explr-collapse') // hide every element set to collapse

1
data/www/js/lib/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,76 +1,24 @@
jQuery(document).ready(function ($) {
const $tableActive = $('table.active-playlists');
const $tableInactive = $('table.inactive-playlists');
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 main = function () {
const qrcodeElement = document.getElementById('qrcode');
};
$(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',
if (qrcodeElement) {
new QRCode(qrcodeElement, {
text: qrcodeElement.attributes['data-qrcode-payload'].value,
width: 128,
height: 128,
colorDark: '#222',
colorLight: '#fff',
correctLevel: QRCode.CorrectLevel.H
});
const $tr = $(this).parents('tr:eq(0)').remove().clone();
if ($(this).is(':checked')) {
$tableActive.append($tr);
} else {
$tableInactive.append($tr);
}
updateTable();
});
};
$(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-time-sync').val(playlist.time_sync ? '1' : '0');
$('#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');
}
});
}
});
main();
});

View File

@ -36,7 +36,7 @@ main {
margin-right: 20px;
.trigger {
color: white;
color: $white;
.avatar {
width: 32px;

View File

@ -28,6 +28,15 @@ body, html {
align-self: stretch;
}
.vertical {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
flex: 1;
align-self: stretch;
}
main {
flex: 1;
display: flex;
@ -51,7 +60,7 @@ main {
border-bottom: $layoutBorder;
h1 {
color: white;
color: $white;
font-weight: 600;
font-size: 24px;
}
@ -63,16 +72,13 @@ main {
justify-content: flex-end;
align-items: center;
.btn,
button {
margin-left: 10px;
}
}
}
.alert {
}
.bottom-content {
display: flex;
flex-direction: row;

View File

@ -103,7 +103,7 @@ menu {
padding-left: 10px;
i {
color: white;
color: $white;
opacity: .2;
background: transparent;
display: flex;
@ -119,7 +119,7 @@ menu {
}
&:after {
background: white;
background: $white;
content: "";
height: 195px;
left: -200px;
@ -156,11 +156,11 @@ menu {
}
a {
color: white;
color: $white;
font-weight: bold;
i {
color: white;
color: $white;
opacity: 1;
}
}

View File

@ -6,6 +6,13 @@
justify-content: center;
align-items: center;
border-radius: 4px;
a {
color: inherit;
margin-left: 4px;
margin-right: 4px;
text-decoration: underline;
}
}
.alert-info {

View File

@ -38,7 +38,7 @@
flex-direction: row;
justify-content: center;
align-items: center;
color: white;
color: $white;
text-align: center;
padding: 0 3px;

View File

@ -4,7 +4,7 @@ button,
$shadowOffset: 2px;
position: relative;
padding: 10px 13px 8px 10px;
padding: 10px 10px 8px 10px;
font-size: 14px;
color: #fff;
cursor: pointer;
@ -35,15 +35,48 @@ button,
color: #AAA;
background: $neutralGrey;
box-shadow: 0 $shadowOffset 0 0 darken($neutralGrey, 10%);
&:hover { box-shadow: 0 $shadowOffset 0 1px #222 inset; background: darken($neutralGrey, 10%); }
&:focus { background: darken($neutralGrey, 20%); }
&:hover {
box-shadow: 0 $shadowOffset 0 1px #222 inset;
background: darken($neutralGrey, 10%);
}
&:focus {
background: darken($neutralGrey, 20%);
}
}
.btn-wire-neutral {
background: transparent;
border: 2px solid $neutralGrey;
color: rgba($white, .8);
box-shadow: none;
&:hover {
background: rgba($neutralGrey, 0.05);
border-color: darken($neutralGrey, 10%);
color: darken($neutralGrey, 10%);
box-shadow: none;
}
&:focus {
border-color: darken($neutralGrey, 20%);
background: transparent;
}
}
&.btn-naked {
background: transparent;
box-shadow: none;
&:hover { box-shadow: 0 $shadowOffset 0 1px #222 inset; background: darken($neutralGrey, 10%); }
&:focus { background: darken($neutralGrey, 20%); }
&:hover {
box-shadow: 0 $shadowOffset 0 1px #222 inset;
background: darken($neutralGrey, 10%);
}
&:focus {
background: darken($neutralGrey, 20%);
}
}
}

View File

@ -52,7 +52,7 @@
a {
padding: 8px 16px 8px 8px;
color: white;
color: $white;
display: flex;
flex-direction: row;
justify-content: flex-start;

View File

@ -7,3 +7,18 @@ span.empty {
padding: 2px 4px;
font-weight: bold;
}
.inner-empty {
display: flex;
flex: 1;
align-self: stretch;
justify-content: center;
align-items: center;
i {
font-size: 90px;
opacity: 0.3;
text-shadow: 0 -1px #333, 0 0px .5px #444;
}
}

View File

@ -19,11 +19,11 @@ ul.explr-tree {
}
a {
color: white;
color: $white;
padding-right: 80px;
&:hover {
color: white;
color: $white;
}
&.active {

View File

@ -36,9 +36,21 @@
h3 {
align-self: stretch;
border-bottom: 1px solid $lightGrey;
padding: 15px 15px;
margin: 0;
font-size: 14px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
color: white;
padding-bottom: 10px;
text-decoration: none;
&.divide {
border-top: 1px solid #222;
margin-top: 20px;
padding-top: 20px;
}
}
}
}

View File

@ -118,16 +118,16 @@ table.panes {
td {
font-weight: bold;
color: white;
color: $white;
i.icon-legend {
color: white;
color: $white;
}
span,
i.icon-value {
background-color: rgba($white, .3);
color: white;
color: $white;
}
&.description {

View File

@ -31,7 +31,7 @@ ul.pills {
flex-direction: row;
justify-content: center;
align-items: center;
color: white;
color: $white;
overflow: hidden;
padding-right: 30px;
text-align: center;

View File

@ -1,116 +1,66 @@
.pure-material-switch {
z-index: 0;
$toggleActiveColor: $limeGreen;
$containerWidth: 42px;
$containerHeight: 26px;
$containerRadius: 15px;
$containerShadowActive: 0 2px 2px #222 inset;
$containerShadowInactive: 0 2px 2px #111 inset;
$backgroundColorInactive: #222;
$backgroundColorActive: darken($toggleActiveColor, 30%);
$thumbColorActive: $toggleActiveColor;
$thumbColorInactive: #777;
$thumbWidth: 18px;
$thumbHeight: $thumbWidth;
$borderSize: 1px;
$animationSpeed: 0.2s;
.toggle {
position: relative;
display: inline-block;
}
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.pure-material-switch > input {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
z-index: -1;
position: absolute;
right: 6px;
top: -8px;
input {
display: none;
&:checked + label {
background: darken($thumbColorActive, 30%);
border: $borderSize solid rgba($white, .1);
box-shadow: $containerShadowActive;
&::after {
content: "";
display: block;
margin: 0;
border-radius: 50%;
width: 40px;
height: 40px;
background-color: rgba($black, 0.38);
outline: none;
opacity: 0;
transform: scale(1);
pointer-events: none;
transition: opacity 0.3s 0.1s, transform 0.2s 0.1s;
margin-left: calc($containerWidth - 21px);
width: $thumbWidth;
height: $thumbHeight;
transition: $animationSpeed;
background: $thumbColorActive;
box-shadow: 0 2px darken($thumbColorActive, 40%);
}
}
}
.pure-material-switch > span {
display: inline-block;
width: 100%;
label {
width: $containerWidth + ($borderSize * 2);
height: $containerHeight;
border-radius: $containerRadius;
background: $backgroundColorInactive;
cursor: pointer;
}
border: $borderSize solid rgba($white, .1);
box-shadow: $containerShadowInactive;
.pure-material-switch > span::before {
&::after {
content: "";
float: right;
display: inline-block;
margin: 5px 0 5px 10px;
border-radius: 7px;
width: 36px;
height: 14px;
background-color: rgba($black, 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;
display: block;
border-radius: 50%;
width: 20px;
height: 20px;
background-color: $white;
box-shadow: 0 3px 1px -2px rgba($black, 0.2), 0 2px 2px 0 rgba($black, 0.14), 0 1px 5px 0 rgba($black, 0.12);
transition: background-color 0.2s, transform 0.2s;
width: $thumbWidth;
height: $thumbHeight;
margin: 3px;
background: $thumbColorInactive;
box-shadow: 0 2px rgba(0,0,0,0.9);
transition: $animationSpeed;
}
.pure-material-switch > input:checked {
right: -10px;
background-color: $limeGreen;
}
.pure-material-switch > input:checked + span::before {
background-color: rgba($limeGreen, 0.6);
}
.pure-material-switch > input:checked + span::after {
background-color: $limeGreen;
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($limeGreen, 0.6);
}
.pure-material-switch > input:checked:active + span::before {
background-color: rgba($black, 0.38);
}
.pure-material-switch > input:disabled {
opacity: 0;
}
.pure-material-switch > input:disabled + span {
color: $black;
opacity: 0.38;
cursor: default;
}
.pure-material-switch > input:disabled + span::before {
background-color: rgba($black, 0.38);
}
.pure-material-switch > input:checked:disabled + span::before {
background-color: rgba($limeGreen, 0.6);
}

View File

@ -0,0 +1,117 @@
.tiles {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
align-self: stretch;
.tiles-inner {
display: flex;
flex: 1;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-self: stretch;
padding: 2px;
.tiles-empty {
}
.tile-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
background: #222;
align-self: stretch;
color: $white;
margin: 1px;
padding: 15px;
&:hover,
&.active {
background: #111;
&:hover {
opacity: 1;
}
}
&.disabled {
.tile-header {
.head-icon {
i {
color: #222;
}
}
}
}
.tile-header {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
align-self: stretch;
.head-icon {
flex: 1;
i {
font-size: 6px;
color: white;
opacity: .8;
display: flex;
}
}
.status-icons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
i {
font-size: 16px;
margin-left: 10px;
}
}
}
.tile-body {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-size: 15px;
font-weight: normal;
letter-spacing: 0.8px;
line-height: 22px;
margin: 0 0 0 10px;
flex-wrap: nowrap;
}
.tile-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
margin: 0;
.foot-span {
opacity: .8;
font-size: 13px;
font-family: "Courier New";
}
}
}
}
}

View File

@ -1,10 +1,10 @@
.form-holder {
min-width: 686px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
align-self: stretch;
flex: 1;
form {
max-width: 434px;
@ -24,6 +24,7 @@ form {
font-size: 14px;
margin: 0 0 25px 0;
}
}
.form-group {
display: flex;
@ -47,7 +48,7 @@ form {
color: #666666;
&.btn-upload {
color: white;
color: $white;
font-size: 14px;
flex: 0;
flex-basis: auto;
@ -169,6 +170,21 @@ form {
}
}
}
&.form-group-horizontal {
margin: 10px 0 20px 0;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.widget {
margin: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
}
}
.actions {
@ -191,5 +207,13 @@ form {
margin-right: 25px;
}
}
&.actions-center {
justify-content: center;
button {
margin-left: 0;
margin-right: 0;
}
}
}

View File

@ -84,5 +84,5 @@ header nav ul li a {
header nav ul li a:hover,
header nav ul li.active a {
color: white;
color: $white;
}

View File

@ -23,8 +23,9 @@
// Legacy
@import 'components/panes';
@import 'components/tiles';
@import 'components/empty';
//@import 'components/switches';
@import 'components/switches';
//@import 'components/cards';
//@import 'components/badges';

View File

@ -19,7 +19,7 @@
}
.page-panel.right-panel {
flex: 1;
flex: 2;
align-self: stretch;
display: flex;
flex-direction: column;
@ -29,7 +29,7 @@
padding: 20px;
h3 {
color: white;
color: $white;
padding: 10px 10px 10px 0;
margin-bottom: 20px;
font-size: 16px;

View File

@ -1,28 +1,155 @@
.view-playlist-list main .main-container {
.modal-playlist-qrcode {
h2 {
text-align: center;
}
.view-playlist-edit main .main-container {
.qrcode-pic {
text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
img {
border: 4px solid #555;
border-radius: $baseRadius;
}
}
}
.bottom-content {
.page-content {
flex: 1;
&.with-right-panel {
flex: 0.5;
}
.inner {
padding: 0;
h3 {
font-size: 16px;
font-weight: 500;
color: #DDD;
text-decoration: none;
margin: 0 0 20px 0;
}
.form-holder {
margin: 20px 10px 20px 20px;
border-right: 1px solid #222;
padding-right: 20px;
flex: 1.3;
form {
max-width: initial;
}
.form-group {
flex-grow: 0;
}
}
.preview-holder {
margin: 20px 20px 20px 10px;
flex: 1;
.form-group {
flex-grow: 0;
margin-bottom: 0;
.widget {
a,
.btn {
margin-left: 10px;
}
input[type=text] {
border: none;
background: #000;
border-radius: $baseRadius;
}
}
}
.page-panel.right-panel {
h4 {
font-size: 14px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
align-self: stretch;
color: white;
padding-bottom: 10px;
text-decoration: none;
&.divide {
border-top: 1px solid #222;
margin-top: 20px;
padding-top: 20px;
}
}
p {
font-size: 12px;
line-height: 18px;
display: flex;
margin-bottom: 5px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
align-self: stretch;
color: #666666;
}
.qrcode-pic {
margin-top: 10px;
img {
border: 1px dashed #555;
padding: 5px;
border-radius: $baseRadius;
}
}
.preview {
background: black;
border: 1px solid rgba($white, .3);
border-radius: $baseRadius;
justify-content: center;
align-items: center;
align-self: stretch;
display: flex;
margin: 10px 0 20px 0;
height: 300px;
iframe {
flex: 1;
align-self: stretch;
}
}
}
.slides-holder {
margin-top: 40px;
border-top: 1px solid #222;
align-self: stretch;
padding-top: 20px;
}
}
}
.page-panel.left-panel {
flex: 0.3;
max-width: initial;
justify-content: center;
align-items: center;
display: flex;
}
}
}

View File

@ -58,6 +58,29 @@
background: darken($color, 20%);
}
}
&.btn-wire-#{"#{$name}"} {
background: transparent;
box-shadow: none;
border: 2px solid $color;
color: rgba($white, .8);
i.btn-match {
color: $color;
}
&:hover {
background: rgba($color, 0.05);
border-color: darken($color, 10%);
color: darken($color, 10%);
box-shadow: none;
}
&:focus {
border-color: darken($color, 20%);
background: transparent;
}
}
}
}
}

View File

@ -68,8 +68,12 @@
"js_slideshow_content_delete_confirmation": "Are you sure?",
"playlist_page_title": "Playlists",
"playlist_button_add": "Add a playlist",
"playlist_panel_active": "Active playlists",
"playlist_button_add": "Add Playlist",
"playlist_button_delete": "Delete Playlist",
"playlist_panel_about_playlist": "About playlist",
"playlist_panel_content_management": "Content management",
"playlist_panel_preview": "Playlist preview",
"playlist_panel_preview_action": "Preview",
"playlist_panel_inactive": "Inactive playlists",
"playlist_panel_empty": "Currently, there are no playlists. %link% now.",
"playlist_panel_th_name": "Name",
@ -78,10 +82,12 @@
"playlist_panel_th_activity": "Options",
"playlist_form_add_title": "Add Playlist",
"playlist_form_add_submit": "Add",
"playlist_form_edit_title": "Edit Playlist",
"playlist_form_edit_submit": "Save",
"playlist_form_preview_url_desc": "You can use this link to play this playlist on any browser you want. Use copy button to get that in your clipboard.",
"playlist_form_preview_qrcode_desc": "You can easily access your playlist using a tablet or phone. Just scan the QR code to begin.",
"playlist_form_preview_iframe_desc": "You can view the playlist without leaving this screen by starting the preview from here.",
"playlist_form_label_name": "Enter playlist name",
"playlist_form_label_time_sync": "Sync slides across players",
"playlist_form_label_enabled": "Enable/Disable playlist",
"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",
@ -165,7 +171,7 @@
"settings_variable_desc_edition_auth_enabled": "Default user credentials will be admin/admin",
"settings_variable_desc_external_url": "External url (i.e: https://studio-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Slide upload limit (in megabytes)",
"settings_variable_desc_default_slide_duration": "Introduction slide duration (in seconds)",
"settings_variable_desc_intro_slide_duration": "Introduction slide duration (in seconds)",
"settings_variable_desc_default_slide_time_with_seconds": "Show the seconds on the clock in the introduction slide",
"settings_variable_desc_polling_interval": "Refresh interval applied for settings to the player (in seconds)",
"settings_variable_desc_playlist_default_time_sync": "Sync slides across players for default playlist",
@ -175,6 +181,7 @@
"settings_variable_desc_slide_animation_exit_effect": "Slide animation exit effect (generally better off without it)",
"settings_variable_desc_slide_animation_speed": "Slide animation speed",
"settings_variable_desc_ro_start_counter": "Start counter",
"settings_variable_desc_ro_last_folder_content": "Current folder in content explorer",
"settings_variable_desc_ro_last_folder_node_player": "Current folder in player explorer",
"settings_variable_desc_ro_editable": "Last application reboot datetime",
@ -214,6 +221,7 @@
"basic_month_11": "November",
"basic_month_12": "December",
"common_untitled": "<untitled>",
"common_loading": "Loading...",
"common_default_node_player_group": "Default Playgroup",
"common_default_playlist": "Default Playlist",

View File

@ -1,6 +1,5 @@
{
"dynmenu_content": "Contenido",
"slideshow_slide_page_title": "Descripción General del Programa",
"slideshow_slide_goto_player": "Ir al reproductor",
"slideshow_slide_refresh_player": "Actualizar reproductor",
@ -46,7 +45,6 @@
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Usar formato crontab: * * * * *",
"slideshow_slide_form_button_cancel": "Cancelar",
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
"slideshow_content_page_title": "Biblioteca de contenidos",
"slideshow_content_button_add": "Nuevo Contenido",
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido en una diapositiva; elimine la diapositiva primero",
@ -66,10 +64,13 @@
"slideshow_content_form_button_upload": "Subir un archivo",
"slideshow_content_form_button_upload_choosen": "No hay archivos seleccionados",
"js_slideshow_content_delete_confirmation": "¿Estás seguro?",
"playlist_page_title": "Playlist",
"playlist_button_add": "Agregar una lista de reproducción",
"playlist_panel_active": "Playlist activas",
"playlist_page_title": "Playlists",
"playlist_button_add": "Agregar Playlist",
"playlist_button_delete": "Eliminar Playlist",
"playlist_panel_about_playlist": "Acerca de la playlist",
"playlist_panel_content_management": "Gestión de contenido",
"playlist_panel_preview": "Vista previa de la playlist",
"playlist_panel_preview_action": "Avance",
"playlist_panel_inactive": "Playlist inactivas",
"playlist_panel_empty": "Actualmente, no hay playlist. %link% ahora.",
"playlist_panel_th_name": "Nombre",
@ -78,15 +79,16 @@
"playlist_panel_th_activity": "Opciones",
"playlist_form_add_title": "Agregar Playlist",
"playlist_form_add_submit": "Agregar",
"playlist_form_edit_title": "Editar Playlist",
"playlist_form_edit_submit": "Guardar",
"playlist_form_preview_url_desc": "Puedes usar este enlace para reproducir esta playlist en cualquier navegador que desees. Usa el botón Copiar para guardarla en tu portapapeles.",
"playlist_form_preview_qrcode_desc": "Puedes acceder fácilmente a tu playlist usando una tableta o un teléfono. Simplemente escanea el código QR para comenzar.",
"playlist_form_preview_iframe_desc": "Puedes ver la playlist sin salir de esta pantalla iniciando la vista previa desde aquí.",
"playlist_form_label_name": "Introduce el nombre de la playlist",
"playlist_form_label_time_sync": "Sincronizar diapositivas entre reproductores",
"playlist_form_label_enabled": "Activar/Desactivar playlist",
"playlist_form_button_cancel": "Cancelar",
"js_playlist_delete_confirmation": "¿Estás seguro?",
"playlist_delete_has_slides": "La lista de reproducción tiene diapositivas, por favor elimínelas antes y reintente",
"playlist_delete_has_node_player_groups": "La lista de reproducción está asignada a un playgroup",
"playlist_delete_has_slides": "La playlist tiene diapositivas, por favor elimínelas antes y reintente",
"playlist_delete_has_node_player_groups": "La playlist está asignada a un playgroup",
"fleet_node_player_page_title": "Reproductores",
"fleet_node_player_button_add": "Agregar un reproductor",
"fleet_node_player_panel_active": "Reproductores activos",
@ -107,7 +109,6 @@
"fleet_node_player_form_label_operating_system": "OS",
"fleet_node_player_form_button_cancel": "Cancelar",
"js_fleet_node_player_delete_confirmation": "¿Estás seguro?",
"fleet_node_player_group_page_title": "Playgroups",
"fleet_node_player_group_button_add": "Agregar Playgroup",
"fleet_node_player_group_panel_active": "Playgroup activos",
@ -124,7 +125,6 @@
"fleet_node_player_group_form_button_cancel": "Cancelar",
"js_fleet_node_player_group_delete_confirmation": "¿Estás seguro?",
"node_player_group_delete_has_node_player": "El playgroup tiene reproductores, por favor elimínelos o desasígnelos antes y reintente",
"login_page_title": "Iniciar Sesión",
"auth_page_title": "Usuarios",
"auth_user_button_add": "Agregar un usuario",
@ -143,7 +143,6 @@
"auth_user_form_button_cancel": "Cancelar",
"auth_user_delete_at_least_one_account": "Debe tener al menos un usuario activo mientras usa la función de autenticación",
"js_auth_user_delete_confirmation": "¿Estás seguro?",
"settings_page_title": "Configuración",
"settings_plugin_page_title": "Plugins",
"settings_variable_panel_system_variables": "Configuración general",
@ -165,22 +164,20 @@
"settings_variable_desc_edition_auth_enabled": "Las credenciales predeterminadas del usuario serán admin/admin",
"settings_variable_desc_external_url": "URL externa (ej.: https://studio-01.company.com o http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Límite de carga de diapositivas (en megabytes)",
"settings_variable_desc_default_slide_duration": "Duración de la diapositiva de introducción (en segundos)",
"settings_variable_desc_intro_slide_duration": "Duración de la diapositiva de introducción (en segundos)",
"settings_variable_desc_default_slide_time_with_seconds": "Mostrar los segundos en el reloj de la diapositiva de introducción",
"settings_variable_desc_polling_interval": "Intervalo de actualización aplicado para configuraciones del reproductor (en segundos)",
"settings_variable_desc_playlist_default_time_sync": "Sincronizar diapositivas entre reproductores para la lista de reproducción predeterminada",
"settings_variable_desc_playlist_default_time_sync": "Sincronizar diapositivas entre reproductores para la playlist predeterminada",
"settings_variable_desc_slide_animation_enabled": "Habilitar efecto de animación entre diapositivas",
"settings_variable_desc_slide_animation_entrance_effect": "Efecto de entrada de animación de diapositiva",
"settings_variable_desc_slide_animation_exit_effect": "Efecto de salida de animación de diapositiva (generalmente mejor sin él)",
"settings_variable_desc_slide_animation_speed": "Velocidad de animación de diapositiva",
"settings_variable_desc_ro_start_counter": "Contador de inicio",
"settings_variable_desc_ro_last_folder_content": "Carpeta actual en el explorador de contenidos",
"settings_variable_desc_ro_last_folder_node_player": "Carpeta actual en el explorador del reproductor",
"settings_variable_desc_ro_editable": "Fecha y hora del último reinicio de la aplicación",
"settings_variable_desc_ro_last_slide_update": "Fecha y hora de la última actualización de diapositiva",
"settings_variable_desc_ro_refresh_player_request": "Fecha y hora de la última solicitud de actualización del reproductor",
"sysinfo_page_title": "Información del sistema",
"sysinfo_panel_button_restart": "Reiniciar",
"sysinfo_panel_table_section_system": "Sistema",
@ -193,7 +190,6 @@
"logs_panel_last_logs": "Registros (últimas 100 líneas)",
"js_sysinfo_restart_confirmation": "¿Estás seguro?",
"js_sysinfo_restart_loading": "Reiniciando, por favor espera...",
"basic_day_1": "Lunes",
"basic_day_2": "Martes",
"basic_day_3": "Miércoles",
@ -213,7 +209,7 @@
"basic_month_10": "Octubre",
"basic_month_11": "Noviembre",
"basic_month_12": "Diciembre",
"common_untitled": "<sin-título>",
"common_loading": "Cargando...",
"common_default_node_player_group": "Playgroup predeterminado",
"common_default_playlist": "Lista de reproducción predeterminada",
@ -245,7 +241,6 @@
"updated_by": "Última actualización por",
"close": "Cerrar",
"anonymous": "Anónimo",
"enum_animation_speed_slower": "Más lento",
"enum_animation_speed_slow": "Lento",
"enum_animation_speed_normal": "Normal",
@ -279,7 +274,6 @@
"enum_operating_system_redhat": "RedHat",
"enum_operating_system_centos": "CentOS",
"enum_operating_system_other": "Otro",
"sysinfo_rpi_model": "Modelo de Raspberry Pi",
"sysinfo_rpi_model_unknown": "No es una Raspberry Pi o la información del modelo no está disponible",
"sysinfo_storage_free_space": "Espacio de almacenamiento libre",
@ -289,6 +283,5 @@
"sysinfo_network_interface": "Interfaz de red",
"sysinfo_mac_address": "Dirección MAC",
"sysinfo_ip_address": "Dirección IP",
"player_default_welcome_message": "Para gestionar este reproductor, ve a un navegador en %link%"
}

View File

@ -1,6 +1,5 @@
{
"dynmenu_content": "Contenu",
"slideshow_slide_page_title": "Vue Planning",
"slideshow_slide_goto_player": "Voir le lecteur",
"slideshow_slide_refresh_player": "Rafraîchir le lecteur",
@ -46,7 +45,6 @@
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Utiliser le format crontab: * * * * *",
"slideshow_slide_form_button_cancel": "Annuler",
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
"slideshow_content_page_title": "Bibliothèque de contenus",
"slideshow_content_button_add": "Nouveau Contenu",
"slideshow_content_referenced_in_slide_error": "Le contenu est référencé dans une slide, supprimez d'abord la slide",
@ -66,10 +64,13 @@
"slideshow_content_form_button_upload": "Uploader un fichier",
"slideshow_content_form_button_upload_choosen": "Aucun fichier sélectionné",
"js_slideshow_content_delete_confirmation": "Êtes-vous sûr ?",
"playlist_page_title": "Playlist",
"playlist_button_add": "Ajouter une playlist",
"playlist_panel_active": "Playlist actives",
"playlist_page_title": "Playlists",
"playlist_button_add": "Ajouter Playlist",
"playlist_button_delete": "Supprimer Playlist",
"playlist_panel_about_playlist": "À propos de la playlist",
"playlist_panel_content_management": "Elements de la playlist",
"playlist_panel_preview": "Playlist preview",
"playlist_panel_preview_action": "Prévisualiser",
"playlist_panel_inactive": "Playlist inactives",
"playlist_panel_empty": "Actuellement, il n'y a pas de playlist. %link% maintenant.",
"playlist_panel_th_name": "Nom",
@ -78,15 +79,17 @@
"playlist_panel_th_activity": "Options",
"playlist_form_add_title": "Ajout d'une Playlist",
"playlist_form_add_submit": "Ajouter",
"playlist_form_edit_title": "Modification d'une Playlist",
"playlist_form_preview_url_desc": "Vous pouvez utiliser ce lien pour lire cette playlist sur n'importe quel navigateur de votre choix. Utilisez le bouton Copier pour l'obtenir dans votre presse-papiers.",
"playlist_form_preview_qrcode_desc": "Vous pouvez facilement accéder à votre playlist à l'aide d'une tablette ou d'un téléphone. Scannez simplement le code QR pour commencer.",
"playlist_form_preview_iframe_desc": "Vous pouvez visualiser la playlist sans quitter cet écran en démarrant l'aperçu à partir d'ici.",
"playlist_form_edit_submit": "Enregistrer",
"playlist_form_label_name": "Entrez le nom de la playlist",
"playlist_form_label_time_sync": "Synchroniser les slides des lecteurs",
"playlist_form_label_enabled": "Activer/Désactiver la playlist",
"playlist_form_button_cancel": "Annuler",
"js_playlist_delete_confirmation": "Êtes-vous sûr ?",
"playlist_delete_has_slides": "La playlist contient des slides, supprimez-les avant et réessayez",
"playlist_delete_has_node_player_groups": "La playlist est attribuée à un playgroup",
"fleet_node_player_page_title": "Lecteurs",
"fleet_node_player_button_add": "Ajouter un lecteur",
"fleet_node_player_panel_active": "Players actifs",
@ -107,7 +110,6 @@
"fleet_node_player_form_label_operating_system": "OS",
"fleet_node_player_form_button_cancel": "Annuler",
"js_fleet_node_player_delete_confirmation": "Êtes-vous sûr ?",
"fleet_node_player_group_page_title": "Playgroups",
"fleet_node_player_group_button_add": "Ajouter un Playgroup",
"fleet_node_player_group_panel_active": "Playgroups",
@ -124,7 +126,6 @@
"fleet_node_player_group_form_button_cancel": "Annuler",
"js_fleet_node_player_group_delete_confirmation": "Êtes-vous sûr ?",
"node_player_group_delete_has_node_player": "Le playgroup a des lecteurs, supprimez-les ou réassignez-les avant de le supprimer",
"login_page_title": "Connexion",
"auth_page_title": "Utilisateurs",
"auth_user_button_add": "Ajouter un utilisateur",
@ -143,7 +144,6 @@
"auth_user_form_button_cancel": "Annuler",
"auth_user_delete_at_least_one_account": "Vous devez avoir au moins un utilisateur actif lorsque vous activez la gestion de l'authentification",
"js_auth_user_delete_confirmation": "Êtes-vous sûr ?",
"settings_page_title": "Paramètres",
"settings_plugin_page_title": "Plugins",
"settings_variable_panel_system_variables": "Paramètres généraux",
@ -165,22 +165,20 @@
"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://studio-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Limite d'upload du fichier d'une slide (en mégaoctets)",
"settings_variable_desc_default_slide_duration": "Durée de la slide d'introduction (en secondes)",
"settings_variable_desc_intro_slide_duration": "Durée de la slide d'introduction (en secondes)",
"settings_variable_desc_default_slide_time_with_seconds": "Afficher les secondes de l'horloge de la slide d'introduction",
"settings_variable_desc_polling_interval": "Intervalle de rafraîchissement des paramètres à appliquer au lecteur (en secondes)",
"settings_variable_desc_playlist_default_time_sync": "Synchroniser les slides des lecteurs pour la playlist par défaut",
"settings_variable_desc_slide_animation_enabled": "Activer les effets d'animation entre les slides",
"settings_variable_desc_slide_animation_entrance_effect": "Effet d'animation d'arrivée de la slide",
"settings_variable_desc_slide_animation_exit_effect": "Effet d'animation de sortie de la slide (généralement mieux sans)",
"settings_variable_desc_slide_animation_speed": "Vitesse de l'animation de la slide",
"settings_variable_desc_ro_start_counter": "Compteur de démarrage",
"settings_variable_desc_ro_last_folder_content": "Dossier courant dans l'explorateur de contenu",
"settings_variable_desc_ro_last_folder_node_player": "Dossier courant dans l'explorateur du lecteur",
"settings_variable_desc_ro_editable": "Date de dernier redémarrage de l'application",
"settings_variable_desc_ro_last_slide_update": "Date de dernière modification d'une slide",
"settings_variable_desc_ro_refresh_player_request": "Date de dernière demande de rafraîchissement du lecteur",
"sysinfo_page_title": "Système",
"sysinfo_panel_button_restart": "Redémarrer",
"sysinfo_panel_table_section_system": "Système",
@ -193,7 +191,6 @@
"logs_panel_last_logs": "Journaux (100 dernières lignes)",
"js_sysinfo_restart_confirmation": "Êtes-vous sûr ?",
"js_sysinfo_restart_loading": "Redémarrage en cours, veuillez patienter...",
"basic_day_1": "Lundi",
"basic_day_2": "Mardi",
"basic_day_3": "Mercredi",
@ -213,7 +210,7 @@
"basic_month_10": "Octobre",
"basic_month_11": "Novembre",
"basic_month_12": "Décembre",
"common_untitled": "<sans-titre>",
"common_loading": "Chargement...",
"common_default_node_player_group": "Playgroup par défaut",
"common_default_playlist": "Playlist par défaut",
@ -245,7 +242,6 @@
"updated_by": "Dernière modification par",
"close": "Fermer",
"anonymous": "Anon",
"enum_animation_speed_slower": "Très lent",
"enum_animation_speed_slow": "Lent",
"enum_animation_speed_normal": "Normal",
@ -279,7 +275,6 @@
"enum_operating_system_redhat": "RedHat",
"enum_operating_system_centos": "CentOS",
"enum_operating_system_other": "Autre",
"sysinfo_rpi_model": "Modèle du Raspberry Pi",
"sysinfo_rpi_model_unknown": "Le modèle n'est pas un Raspberry Pi",
"sysinfo_storage_free_space": "Stockage Disponible",
@ -289,6 +284,5 @@
"sysinfo_network_interface": "Interface Réseau",
"sysinfo_mac_address": "Addresse MAC",
"sysinfo_ip_address": "Addresse IP",
"player_default_welcome_message": "Pour gérer ce lecteur, allez sur un navigateur à l'adresse %link%"
}

View File

@ -1,6 +1,5 @@
{
"dynmenu_content": "Contenuti",
"slideshow_slide_page_title": "Programmazione",
"slideshow_slide_goto_player": "Vai al player",
"slideshow_slide_refresh_player": "Aggiorna player",
@ -46,7 +45,6 @@
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Utilizza formato crontab: * * * * *",
"slideshow_slide_form_button_cancel": "Annulla",
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
"slideshow_content_page_title": "Libreria dei contenuti",
"slideshow_content_button_add": "Nuovo Contenuto",
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto in una diapositiva, rimuovere prima la diapositiva",
@ -66,10 +64,13 @@
"slideshow_content_form_button_upload": "Carica un file",
"slideshow_content_form_button_upload_choosen": "Nessun file selezionato",
"js_slideshow_content_delete_confirmation": "Sei sicuro?",
"playlist_page_title": "Playlist",
"playlist_button_add": "Aggiungi alla playlist",
"playlist_panel_active": "Attiva playlist",
"playlist_page_title": "Playlists",
"playlist_button_add": "Aggiungi Playlist",
"playlist_button_delete": "Elimina Playlist",
"playlist_panel_about_playlist": "Informazioni sulla playlist",
"playlist_panel_content_management": "Gestione dei contenuti",
"playlist_panel_preview": "Anteprima della playlist",
"playlist_panel_preview_action": "Anteprima",
"playlist_panel_inactive": "Playlist inattive",
"playlist_panel_empty": "Attualmente, non ci sono playlist. %link% adesso.",
"playlist_panel_th_name": "Nome",
@ -78,15 +79,16 @@
"playlist_panel_th_activity": "Opzioni",
"playlist_form_add_title": "Aggiungi Playlist",
"playlist_form_add_submit": "Aggiungi",
"playlist_form_edit_title": "Modifica Playlist",
"playlist_form_edit_submit": "Salva",
"playlist_form_preview_url_desc": "Puoi utilizzare questo collegamento per riprodurre questa playlist su qualsiasi browser desideri. Utilizza il pulsante Copia per inserirla negli appunti.",
"playlist_form_preview_qrcode_desc": "Puoi accedere facilmente alla tua playlist utilizzando un tablet o un telefono. Basta scansionare il codice QR per iniziare.",
"playlist_form_preview_iframe_desc": "Puoi visualizzare la playlist senza uscire da questa schermata avviando l'anteprima da qui.",
"playlist_form_label_name": "Inserisci il nome della playlist",
"playlist_form_label_time_sync": "Sincronizza le slide tra gli schermi",
"playlist_form_label_enabled": "Abilita/Disabilita playlist",
"playlist_form_button_cancel": "Cancella",
"js_playlist_delete_confirmation": "Sei sicuro?",
"playlist_delete_has_slides": "Sono presenti slide nella playlist, annullale e riprova",
"playlist_delete_has_node_player_groups": "La playlist è collegata ad un playgroup",
"fleet_node_player_page_title": "Schermi",
"fleet_node_player_button_add": "Aggiungi allo schermo",
"fleet_node_player_panel_active": "Schermi attivi",
@ -107,7 +109,6 @@
"fleet_node_player_form_label_operating_system": "OS",
"fleet_node_player_form_button_cancel": "Cancella",
"js_fleet_node_player_delete_confirmation": "Sei sicuro?",
"fleet_node_player_group_page_title": "Playgroups",
"fleet_node_player_group_button_add": "Aggiungi Playgroup",
"fleet_node_player_group_panel_active": "Playgroup attivi",
@ -124,7 +125,6 @@
"fleet_node_player_group_form_button_cancel": "Cancella",
"js_fleet_node_player_group_delete_confirmation": "Sei sicuro?",
"node_player_group_delete_has_node_player": "Lo playgroup ha una playlist, rumuovila o riassegnala e riprova",
"login_page_title": "Login",
"auth_page_title": "Utente",
"auth_user_button_add": "Aggiungi utente",
@ -143,7 +143,6 @@
"auth_user_form_button_cancel": "Cancella",
"auth_user_delete_at_least_one_account": "È necessario avere almeno un utente attivo durante l'utilizzo della funzione di autenticazione",
"js_auth_user_delete_confirmation": "Sei sicuro?",
"settings_page_title": "Impostazioni",
"settings_plugin_page_title": "Plugins",
"settings_variable_panel_system_variables": "Impostazioni generali",
@ -165,22 +164,20 @@
"settings_variable_desc_edition_auth_enabled": "Le credenziali utente predefinite sono admin/admin",
"settings_variable_desc_external_url": "Url esterno (esempio: https://studio-01.company.com or http://10.10.3.100)",
"settings_variable_desc_slide_upload_limit": "Limite upload slide (in megabytes)",
"settings_variable_desc_default_slide_duration": "Durata introduzione slide (in secondi)",
"settings_variable_desc_intro_slide_duration": "Durata introduzione slide (in secondi)",
"settings_variable_desc_default_slide_time_with_seconds": "Mostra secondi introduzione slide",
"settings_variable_desc_polling_interval": "Intervallo di aggiornamento applicato per le impostazioni del monitor (in secondi)",
"settings_variable_desc_playlist_default_time_sync": "Sincronizza le diapositive tra i lettori per la playlist predefinita",
"settings_variable_desc_slide_animation_enabled": "Abilita l'effetto di animazione tra le diapositive",
"settings_variable_desc_slide_animation_entrance_effect": "Effetto ingresso diapositiva",
"settings_variable_desc_slide_animation_exit_effect": "Effetto di uscita della diapositiva (meglio senza)",
"settings_variable_desc_slide_animation_speed": "Velicita animazione slide",
"settings_variable_desc_ro_start_counter": "Avvia contatore",
"settings_variable_desc_ro_last_folder_content": "Cartella corrente in Esplora contenuti",
"settings_variable_desc_ro_last_folder_node_player": "Cartella corrente in Player Explorer",
"settings_variable_desc_ro_editable": "Data/ora dell'ultimo riavvio dell'applicazione",
"settings_variable_desc_ro_last_slide_update": "Data e ora dell'ultimo aggiornamento della diapositiva",
"settings_variable_desc_ro_refresh_player_request": "Data e ora della richiesta di aggiornamento dell monitor",
"sysinfo_page_title": "Informazione sistema",
"sysinfo_panel_button_restart": "Riavvia",
"sysinfo_panel_table_section_system": "Sistema",
@ -193,7 +190,6 @@
"logs_panel_last_logs": "Logs (ultime 100 righe)",
"js_sysinfo_restart_confirmation": "Sei sicuro?",
"js_sysinfo_restart_loading": "Riavvio in corso, attendi...",
"basic_day_1": "Lunedi",
"basic_day_2": "Martedi",
"basic_day_3": "Mercoledi",
@ -213,7 +209,7 @@
"basic_month_10": "Ottobre",
"basic_month_11": "Novembre",
"basic_month_12": "Dicembre",
"common_untitled": "<senza-titolo>",
"common_loading": "Caricamento...",
"common_default_node_player_group": "Playgroup di default",
"common_default_playlist": "Default playlist",
@ -245,7 +241,6 @@
"updated_by": "Aggiornato da",
"close": "Chiuso",
"anonymous": "Anonimo",
"enum_animation_speed_slower": "Lentamente",
"enum_animation_speed_slow": "Lento",
"enum_animation_speed_normal": "Normale",
@ -279,7 +274,6 @@
"enum_operating_system_redhat": "RedHat",
"enum_operating_system_centos": "CentOS",
"enum_operating_system_other": "Altro",
"sysinfo_rpi_model": "Raspberry Pi Model",
"sysinfo_rpi_model_unknown": "Informazioni Raspberry Pi non disponibili",
"sysinfo_storage_free_space": "Spazio libero",
@ -289,6 +283,5 @@
"sysinfo_network_interface": "interfaccia di rete",
"sysinfo_mac_address": "Indirizzo MAC",
"sysinfo_ip_address": "indirizzo IP",
"player_default_welcome_message": "Per gestire questo lettore, vai al browser all'indirizzo %link%"
}

View File

@ -24,6 +24,11 @@ class Application:
signal.signal(signal.SIGINT, self.signal_handler)
def start(self) -> None:
variable = self._model_store.variable().get_one_by_name('start_counter')
if variable:
self._model_store.variable().update_by_name(variable.name, variable.as_int() + 1)
self._web_server.run()
def signal_handler(self, signal, frame) -> None:

View File

@ -30,17 +30,17 @@ class AuthController(ObController):
login_error = None
if current_user.is_authenticated:
return redirect(url_for('slideshow_slide_list'))
return redirect(url_for('playlist'))
if not self._model_store.variable().map().get('auth_enabled').as_bool():
return redirect(url_for('slideshow_slide_list'))
return redirect(url_for('playlist'))
if len(request.form):
user = self._model_store.user().get_one_by_username(request.form['username'], enabled=True)
if user:
if user.password == self._model_store.user().encode_password(request.form['password']):
login_user(user)
return redirect(url_for('slideshow_slide_list'))
return redirect(url_for('playlist'))
else:
login_error = 'bad_credentials'
else:

View File

@ -37,13 +37,22 @@ class PlayerController(ObController):
playlist_id = current_playlist.id if current_playlist else None
items = self._get_playlist(playlist_id=playlist_id, preview_content_id=preview_content_id)
intro_slide_duration = self._model_store.variable().get_one_by_name('intro_slide_duration').eval()
if items['preview_mode'] or request.args.get('intro', '1') == '0':
intro_slide_duration = 0
animation_enabled = self._model_store.variable().get_one_by_name('slide_animation_enabled').eval()
if request.args.get('animation', '1') == '0':
animation_enabled = False
return render_template(
'player/player.jinja.html',
items=items,
default_slide_duration=0 if items['preview_mode'] else self._model_store.variable().get_one_by_name('default_slide_duration').eval(),
intro_slide_duration=intro_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'),
slide_animation_enabled=animation_enabled,
slide_animation_entrance_effect=self._model_store.variable().get_one_by_name('slide_animation_entrance_effect'),
slide_animation_exit_effect=self._model_store.variable().get_one_by_name('slide_animation_exit_effect'),
slide_animation_speed=self._model_store.variable().get_one_by_name('slide_animation_speed'),

View File

@ -1,6 +1,7 @@
import json
from flask import Flask, render_template, redirect, request, url_for, jsonify
from flask import Flask, render_template, redirect, request, url_for, jsonify, abort
from src.exceptions.PlaylistSlugAlreadyExist import PlaylistSlugAlreadyExist
from src.service.ModelStore import ModelStore
from src.model.entity.Playlist import Playlist
from src.interface.ObController import ObController
@ -17,20 +18,31 @@ class PlaylistController(ObController):
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', 'playlist', self.guard_playlist(self._auth(self.playlist)), methods=['GET'])
self._app.add_url_rule('/playlist/list/<playlist_id>', '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'])
self._app.add_url_rule('/playlist/save', 'playlist_save', self.guard_playlist(self._auth(self.playlist_save)), methods=['POST'])
self._app.add_url_rule('/playlist/delete/<playlist_id>', 'playlist_delete', self.guard_playlist(self._auth(self.playlist_delete)), methods=['GET'])
def playlist_list(self):
def playlist(self):
return redirect(url_for('playlist_list', playlist_id=0))
def playlist_list(self, playlist_id: int = 0):
current_playlist = self._model_store.playlist().get(playlist_id)
playlists = self._model_store.playlist().get_all(sort="created_at", ascending=False)
durations = self._model_store.playlist().get_durations_by_playlists()
if not current_playlist and len(playlists) > 0:
current_playlist = playlists[0]
return render_template(
'playlist/list.jinja.html',
playlists=self._model_store.playlist().get_all(ascending=True),
enabled_playlists=self._model_store.playlist().get_enabled_playlists(with_default=True),
disabled_playlists=self._model_store.playlist().get_disabled_playlists(),
durations=durations
error=request.args.get('error', None),
current_playlist=current_playlist,
playlists=playlists,
durations=durations,
slides=self._model_store.slide().get_slides(playlist_id=current_playlist.id),
contents={content.id: content for content in self._model_store.content().get_contents()},
)
def playlist_add(self):
@ -40,32 +52,28 @@ class PlaylistController(ObController):
time_sync=False
)
self._model_store.playlist().add_form(playlist)
try:
playlist = self._model_store.playlist().add_form(playlist)
except PlaylistSlugAlreadyExist as e:
abort(409)
return redirect(url_for('playlist_list'))
return redirect(url_for('playlist_list', playlist_id=playlist.id))
def playlist_edit(self):
def playlist_save(self):
self._model_store.playlist().update_form(
id=request.form['id'],
name=request.form['name'],
time_sync=request.form['time_sync'],
time_sync=True if 'time_sync' in request.form else False,
enabled=True if 'enabled' in request.form else False
)
return redirect(url_for('playlist_list'))
return redirect(url_for('playlist_list', playlist_id=request.form['id']))
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, playlist_id: int):
if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0:
return redirect(url_for('playlist_list', playlist_id=playlist_id, error='playlist_delete_has_slides'))
def playlist_delete(self):
data = request.get_json()
id = data.get('id')
if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0:
return redirect(url_for('playlist_list', playlist_id=playlist_id, error='playlist_delete_has_node_player_groups'))
if self._model_store.slide().count_slides_for_playlist(id) > 0:
return jsonify({'status': 'error', 'message': self.t('playlist_delete_has_slides')}), 400
if self._model_store.node_player_group().count_node_player_groups_for_playlist(id) > 0:
return jsonify({'status': 'error', 'message': self.t('playlist_delete_has_node_player_groups')}), 400
self._model_store.playlist().delete(id)
return jsonify({'status': 'ok'})
self._model_store.playlist().delete(playlist_id)
return redirect(url_for('playlist'))

View File

@ -16,31 +16,14 @@ class SlideController(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'])
self._app.add_url_rule('/slideshow/slide/delete', 'slideshow_slide_delete', self._auth(self.slideshow_slide_delete), methods=['DELETE'])
self._app.add_url_rule('/slideshow/slide/position', 'slideshow_slide_position', self._auth(self.slideshow_slide_position), methods=['POST'])
self._app.add_url_rule('/slideshow/player-refresh', 'slideshow_player_refresh', self._auth(self.slideshow_player_refresh), methods=['GET'])
self._app.add_url_rule('/slideshow/player-refresh', 'slideshow_player_refresh/<playlist_id>', self._auth(self.slideshow_player_refresh), methods=['GET'])
def manage(self):
return redirect(url_for('slideshow_slide_list'))
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/slides/list.jinja.html',
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'),
contents={content.id: content.name for content in self._model_store.content().get_contents()},
enum_content_type=ContentType
)
return redirect(url_for('playlist'))
def slideshow_slide_add(self):
content = None
@ -67,9 +50,9 @@ class SlideController(ObController):
self._post_update()
if slide.playlist_id:
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist_id))
return redirect(url_for('playlist_list', playlist_id=slide.playlist_id))
return redirect(url_for('slideshow_slide_list'))
return redirect(url_for('playlist'))
def slideshow_slide_edit(self):
slide = self._model_store.slide().update_form(
@ -83,15 +66,9 @@ class SlideController(ObController):
self._post_update()
if slide.playlist_id:
return redirect(url_for('slideshow_slide_list_playlist_use', playlist_id=slide.playlist_id))
return redirect(url_for('playlist_list', playlist_id=slide.playlist_id))
return redirect(url_for('slideshow_slide_list'))
def slideshow_slide_toggle(self):
data = request.get_json()
self._model_store.slide().update_enabled(data.get('id'), data.get('enabled'))
self._post_update()
return jsonify({'status': 'ok'})
return redirect(url_for('playlist'))
def slideshow_slide_delete(self):
data = request.get_json()
@ -105,11 +82,12 @@ class SlideController(ObController):
self._post_update()
return jsonify({'status': 'ok'})
def slideshow_player_refresh(self):
def slideshow_player_refresh(self, playlist_id: int):
self._model_store.variable().update_by_name("refresh_player_request", time.time())
return redirect(
url_for(
'slideshow_slide_list',
'playlist_list',
playlist_id=playlist_id,
refresh_player=self._model_store.variable().get_one_by_name('polling_interval').as_int()
)
)

View File

@ -0,0 +1,2 @@
class PlaylistSlugAlreadyExist(Exception):
pass

View File

@ -120,8 +120,13 @@ class DatabaseManager:
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, ascending=True, values: dict = {}) -> list:
query = "select * from {} where {} {}".format(table_name, query, "ORDER BY {} {}".format(sort, "ASC" if ascending else "DESC") if sort else "")
def get_one_by_query(self, table_name: str, query: str = "1=1", values: dict = {}, sort: Optional[str] = None, ascending=True, limit: Optional[int] = None) -> list:
query = "select * from {} where {} {} {}".format(
table_name,
query,
"ORDER BY {} {}".format(sort, "ASC" if ascending else "DESC") if sort else "",
"LIMIT {}".format(limit) if limit else ""
)
lines = self.execute_read_query(query=query, params=tuple(v for v in values.values()))
count = len(lines)
@ -216,6 +221,7 @@ class DatabaseManager:
"DROP TABLE IF EXISTS fleet_studio",
"ALTER TABLE slideshow RENAME TO slides",
"DELETE FROM settings WHERE name = 'fleet_studio_enabled'",
"DELETE FROM settings WHERE name = 'default_slide_duration'",
"UPDATE content SET uuid = id WHERE uuid = '' or uuid is null",
]

View File

@ -72,10 +72,9 @@ class NodePlayerGroupManager(ModelManager):
def get_node_players_groups(self, playlist_id: Optional[int] = None) -> List[NodePlayerGroup]:
query = " 1=1 "
if playlist_id:
query = "{} {}".format(query, "AND playlist_id = {}".format(playlist_id))
else:
query = "{} {}".format(query, "AND playlist_id is NULL")
return self.get_by(query=query, sort="name")

View File

@ -3,7 +3,8 @@ import os
from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Playlist import Playlist
from src.util.utils import get_optional_string, get_yt_video_id, slugify
from src.util.utils import get_optional_string, get_yt_video_id, slugify, slugify_next
from src.exceptions.PlaylistSlugAlreadyExist import PlaylistSlugAlreadyExist
from src.manager.DatabaseManager import DatabaseManager
from src.manager.SlideManager import SlideManager
from src.manager.LangManager import LangManager
@ -54,8 +55,8 @@ class PlaylistManager(ModelManager):
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)
def get_one_by(self, query, values: dict = {}, sort: Optional[str] = None, ascending=True, limit: Optional[int] = None) -> Optional[Playlist]:
object = self._db.get_one_by_query(self.TABLE_NAME, query=query, values=values, sort=sort, ascending=ascending, limit=limit)
if not object:
return None
@ -105,8 +106,25 @@ class PlaylistManager(ModelManager):
for playlist_id, edits in edits_playlists.items():
self._db.update_by_id(self.TABLE_NAME, playlist_id, edits)
def get_available_slug(self, slug) -> str:
known_playlist = {"slug": slug}
next_slug = slug
while known_playlist is not None:
next_slug = slugify_next(next_slug)
known_playlist = self.get_one_by(query="slug = ?", values={"slug": next_slug}, sort="created_at", ascending=False, limit=1)
return next_slug
def pre_add(self, playlist: Dict) -> Dict:
playlist["slug"] = slugify(playlist["name"])
known_playlist = self.get_one_by(query="slug = ?", values={
"slug": playlist["slug"]
}, sort="created_at", ascending=False, limit=1)
if known_playlist:
playlist["slug"] = self.get_available_slug(playlist["slug"])
self.user_manager.track_user_on_create(playlist)
self.user_manager.track_user_on_update(playlist)
return playlist
@ -131,7 +149,7 @@ class PlaylistManager(ModelManager):
def post_delete(self, playlist_id: str) -> str:
return playlist_id
def update_form(self, id: int, name: str, time_sync: bool) -> None:
def update_form(self, id: int, name: str, time_sync: bool, enabled: bool) -> None:
playlist = self.get(id)
if not playlist:
@ -139,13 +157,14 @@ class PlaylistManager(ModelManager):
form = {
"name": name,
"time_sync": time_sync
"time_sync": time_sync,
"enabled": enabled
}
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:
def add_form(self, playlist: Union[Playlist, Dict]) -> Playlist:
form = playlist
if not isinstance(playlist, dict):
@ -153,7 +172,11 @@ class PlaylistManager(ModelManager):
del form['id']
self._db.add(self.TABLE_NAME, self.pre_add(form))
playlist = self.get_one_by(query="slug = ?", values={
"slug": form["slug"]
})
self.post_add(playlist.id)
return playlist
def delete(self, id: int) -> None:
playlist = self.get(id)

View File

@ -73,13 +73,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_slides(self, playlist_id: Optional[int] = None, content_id: Optional[int] = None, enabled: bool = True) -> List[Slide]:
query = "enabled = {}".format("1" if enabled else "0")
def get_slides(self, playlist_id: Optional[int] = None, content_id: Optional[int] = None, enabled: Optional[bool] = None) -> List[Slide]:
query = " 1=1 "
if enabled is not None:
query = "{} AND enabled = {} ".format(query, "1" if enabled else "0")
if playlist_id:
query = "{} {}".format(query, "AND playlist_id = {}".format(playlist_id))
else:
query = "{} {}".format(query, "AND playlist_id is NULL")
if content_id:
query = "{} {}".format(query, "AND content_id = {}".format(content_id))

View File

@ -1,5 +1,6 @@
import json
import time
import math
from typing import Dict, Optional, List, Tuple, Union
from src.manager.DatabaseManager import DatabaseManager
@ -111,7 +112,7 @@ class VariableManager:
{"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},
### Player Options
{"name": "default_slide_duration", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 3, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_default_slide_duration'), "refresh_player": False},
{"name": "intro_slide_duration", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 3, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_intro_slide_duration'), "refresh_player": False},
{"name": "default_slide_time_with_seconds", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": False, "type": VariableType.BOOL, "editable": True, "description": self.t('settings_variable_desc_default_slide_time_with_seconds'), "refresh_player": False},
{"name": "polling_interval", "section": self.t(VariableSection.PLAYER_OPTIONS), "value": 5, "unit": VariableUnit.SECOND, "type": VariableType.INT, "editable": True, "description": self.t('settings_variable_desc_polling_interval'), "refresh_player": True},
@ -132,6 +133,7 @@ class VariableManager:
{"name": "auth_enabled", "section": self.t(VariableSection.SECURITY), "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},
# Not editable (System information)
{"name": "start_counter", "value": 0, "type": VariableType.INT, "editable": False, "description": self.t('settings_variable_desc_ro_start_counter')},
{"name": "last_folder_content", "value": FOLDER_ROOT_PATH, "type": VariableType.STRING, "editable": False, "description": self.t('settings_variable_desc_ro_last_folder_content')},
{"name": "last_folder_node_player", "value": FOLDER_ROOT_PATH, "type": VariableType.STRING, "editable": False, "description": self.t('settings_variable_desc_ro_last_folder_node_player')},
{"name": "last_restart", "value": time.time(), "type": VariableType.TIMESTAMP, "editable": False, "description": self.t('settings_variable_desc_ro_editable')},

View File

@ -89,7 +89,7 @@ class Playlist:
return f"Playlist(" \
f"id='{self.id}',\n" \
f"name='{self.name}',\n" \
f"nameslug='{self.slug}',\n" \
f"slug='{self.slug}',\n" \
f"enabled='{self.enabled}',\n" \
f"time_sync='{self.time_sync}',\n" \
f"created_by='{self.created_by}',\n" \

View File

@ -3,11 +3,6 @@ from enum import Enum
class HookType(Enum):
H_SLIDESHOW_SLIDES_TOOLBAR_ACTIONS_START = 'h_slideshow_slides_toolbar_actions_start'
H_SLIDESHOW_SLIDES_TOOLBAR_ACTIONS_END = 'h_slideshow_slides_toolbar_actions_end'
H_SLIDESHOW_SLIDES_CSS = 'h_slideshow_slides_css'
H_SLIDESHOW_SLIDES_JAVASCRIPT = 'h_slideshow_slides_javascript'
H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START = 'h_slideshow_toolbar_actions_start'
H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_END = 'h_slideshow_toolbar_actions_end'
H_SLIDESHOW_CONTENT_CSS = 'h_slideshow_css'

View File

@ -255,3 +255,13 @@ def merge_dicts(dict1, dict2):
def dictsort(dict1, attribute="position"):
return dict(sorted(dict1.items(), key=lambda item: item[1][attribute]))
def slugify_next(slug: str) -> str:
parts = slug.rsplit('-', 1)
if len(parts) > 1 and parts[-1].isdigit():
next_number = int(parts[-1]) + 1
return f"{parts[0]}-{next_number}"
else:
return f"{slug}-1"

View File

@ -59,7 +59,7 @@
"position": 0,
"pills": [
{"name": "Bibliothèque", "route": "slideshow_content_list", "icon": "fa-image"},
{"name": "Playlists", "route": "playlist_list", "icon": "fa-play"},
{"name": "Playlists", "route": "playlist", "icon": "fa-play"},
]
},
"configuration": {
@ -115,7 +115,7 @@
{% block header %}
<menu>
<h1 class="logo">
<a href="{{ url_for('slideshow_slide_list') }}">
<a href="{{ url_for('playlist') }}">
<img src="{{ STATIC_PREFIX }}img/logo2.png" class="before"/>
<img src="{{ STATIC_PREFIX }}img/logo2white.png" class="after"/>
Obscreen
@ -164,7 +164,8 @@
<ul class="pills">
{% for menu in current_dynmenu.pills %}
<li class="{{ 'active' if active_route == menu.route }}">
<a href="{{ url_for(menu.route) }}">
{% set href = menu.url_for if menu.url_for else url_for(menu.route) %}
<a href="{{ href }}">
<span class="icon">
<i class="fa {{ menu.icon }}"></i>
</span>

View File

@ -6,7 +6,7 @@
{% block add_css %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/jquery-explr-1.4.css"/>
{{ HOOK(H_SLIDESHOW_CONTENT_CSS) }}
{{ HOOK(H_FLEET_NODE_PLAYER_CSS) }}
{% endblock %}
{% block add_js %}
@ -14,8 +14,7 @@
<script src="{{ STATIC_PREFIX }}js/explorer.js"></script>
<script src="{{ STATIC_PREFIX }}js/fleet/node-players.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
{{ HOOK(H_FLEET_NODE_PLAYER_JAVASCRIPT) }}
{% endblock %}
{% block body_class %}view-node-player-list{% endblock %}
@ -27,7 +26,7 @@
</h1>
<div class="top-actions">
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_START) }}
<div class="explr-selection-actions">
<button class="explr-item-edit explr-selection-entity btn-info" data-entity-route="{{ url_for('fleet_node_player_edit', node_player_id='!c!') }}">
@ -49,7 +48,7 @@
<i class="fa fa-folder-plus icon-left"></i>
{{ l.common_new_folder }}
</button>
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_END) }}
{{ HOOK(H_FLEET_NODE_PLAYER_TOOLBAR_ACTIONS_END) }}
</div>
</div>
@ -74,7 +73,7 @@
{% set node_player_children = node_players[folder.id]|default([]) %}
{% set has_children = folder.children or node_player_children %}
<li class="icon-folder li-explr-folder li-explr-folder-{{ folder.id }}">
<li class="icon-folder li-explr-folder li-explr-folder-{{ folder.id if folder.id else 0 }}">
<a href="{{ url_for('fleet_node_player_cd') }}?path={{ folder.path }}" class="{% if folder.path == working_folder_path %}active{% endif %}">
{{ folder.name }}
</a>

View File

@ -5,7 +5,7 @@
<meta name="robots" content="noindex, nofollow">
<meta name="google" content="notranslate">
<link rel="shortcut icon" href="{{ STATIC_PREFIX }}/favicon.ico">
{% if slide_animation_enabled.eval() %}
{% if slide_animation_enabled %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/animate.min.css" />
{% endif %}
<style>
@ -20,7 +20,7 @@
</head>
<body>
<div id="IntroSlide" class="slide" style="z-index: 10000;">
{% if default_slide_duration > 0 %}
{% if intro_slide_duration > 0 %}
<iframe src="/player/default"></iframe>
{% endif %}
</div>
@ -37,7 +37,7 @@
<script type="text/javascript">
// Backend config
let items = {{ json_dumps(items) | safe}};
const introDuration = {{ default_slide_duration * 1000 }};
const introDuration = {{ intro_slide_duration * 1000 }};
const playlistCheckResolutionMs = {{ polling_interval.eval() * 1000 }};
// Backend flag updates
@ -59,9 +59,9 @@
let pauseClockValue = null;
// Animations
const animate = {{ 'true' if slide_animation_enabled.eval() and not items.preview_mode else 'false' }};
const animate = {{ 'true' if slide_animation_enabled and not items.preview_mode else 'false' }};
const animate_speed = "animate__{{ slide_animation_speed.eval()|default("normal") }}";
const animation_speed_duration = {{ animation_speed_duration[slide_animation_speed.eval()] if slide_animation_enabled.eval() else 0 }};
const animation_speed_duration = {{ animation_speed_duration[slide_animation_speed.eval()] if slide_animation_enabled else 0 }};
const animate_transitions = [
"animate__{{ slide_animation_entrance_effect.eval()|default("fadeIn") }}",
"animate__{{ slide_animation_exit_effect.eval()|default("none") }}"

View File

@ -0,0 +1,113 @@
<div class="horizontal">
<div class="form-holder vertical">
<h3>
{{ l.playlist_panel_about_playlist }}
</h3>
<form class="form" action="{{ url_for('playlist_save') }}" method="POST">
<input type="hidden" name="id" id="playlist-edit-id" value="{{ current_playlist.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"
value="{{ current_playlist.name }}"/>
</div>
</div>
<div class="form-group form-group-horizontal">
<label for="playlist-edit-enabled">{{ l.playlist_form_label_enabled }}</label>
<div class="widget">
<div class="toggle">
<input type="checkbox" id="playlist-edit-enabled" name="enabled"
{% if current_playlist.enabled %}checked="checked"{% endif %}>
<label for="playlist-edit-enabled"></label>
</div>
</div>
</div>
<div class="form-group form-group-horizontal">
<label for="playlist-edit-time-sync">{{ l.playlist_form_label_time_sync }}</label>
<div class="widget">
<div class="toggle">
<input type="checkbox" id="playlist-edit-time-sync" name="time_sync"
{% if current_playlist.time_sync %}checked="checked"{% endif %}>
<label for="playlist-edit-time-sync"></label>
</div>
</div>
</div>
<div class="actions actions-right">
<button type="submit" class="btn-success-alt">
<i class="fa fa-save icon-left"></i>
{{ l.common_save }}
</button>
</div>
</form>
<div class="slides-holder">
<h3>
{{ l.playlist_panel_content_management }}
<div>
<button class="btn btn-info slide-add">
<i class="fa fa-plus"></i>
</button>
</div>
</h3>
{% with slides=slides %}
{% include 'slideshow/slides/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="preview-holder vertical">
<div class="form-group">
<h4>
URL
</h4>
<p>
{{ l.playlist_form_preview_url_desc }}
</p>
{% set preview_url = request.scheme ~ '://' ~ request.headers.get('host') ~ url_for('player_use', playlist_slug_or_id=current_playlist.slug) %}
<div class="widget">
<input type="text" name="name" id="playlist-preview-url" required="required" value="{{ preview_url }}"
class="disabled" disabled="disabled"/>
<button class="btn btn-naked copy-link" data-target-id="playlist-preview-url">
<i class="fa fa-copy"></i>
</button>
<a href="{{ preview_url }}" class="btn btn-neutral" target="_blank">
<i class="fa-solid fa-up-right-from-square"></i>
</a>
</div>
<h4 class="divide">
QR Code
</h4>
<p>
{{ l.playlist_form_preview_qrcode_desc }}
</p>
<div id="qrcode" class="qrcode-pic" data-qrcode-payload="{{ preview_url }}"></div>
</div>
<h4 class="divide">
Iframe
</h4>
<p>
{{ l.playlist_form_preview_iframe_desc }}
</p>
<div class="preview">
<button class="btn btn-info btn-naked"
onclick="$(this).parent().find('iframe').removeClass('hidden');$(this).addClass('hidden');">
<i class="fa fa-eye icon-left"></i> {{ l.playlist_panel_preview_action }}
</button>
<iframe src="{{ preview_url }}?intro=0&animation=0" frameborder="0" class="hidden"></iframe>
</div>
</div>
</div>

View File

@ -1,86 +1,46 @@
<table class="{{ tclass }}-playlists">
<thead>
<tr>
<th>{{ l.playlist_panel_th_name }}</th>
{% if AUTH_ENABLED %}
<th class="tac">
<i class="fa fa-user"></i>
</th>
{% endif %}
<th class="tac"><i class="fa fa-compass"></i></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>
<div class="tiles playlists">
<div class="tiles-inner">
{% for playlist in playlists %}
<tr class="playlist-item" data-level="{{ playlist.id }}" data-entity="{{ playlist.to_json({"created_by": track_created(playlist).username, "updated_by": track_updated(playlist).username}) }}">
<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 }}
{% set active = current_playlist and ''~playlist.id == ''~current_playlist.id %}
<a href="{{ url_for('playlist_list', playlist_id=playlist.id) }}"
class="{% if active %}active{% endif %} tile-item {% if not playlist.enabled %}disabled{% endif %} playlist-item"
data-level="{{ playlist.id }}"
data-entity="{{ playlist.to_json({"created_by": track_created(playlist).username, "updated_by": track_updated(playlist).username}) }}">
<div class="tile-header">
<div class="head-icon">
<i class="fa {{ 'fa fa-circle' if active else 'fa fa-circle' }}"></i>
</div>
</td>
{% if AUTH_ENABLED %}
<td class="tac">
{% if playlist.id %}
{% set creator = track_created(playlist) %}
{% if creator.username %}
<a href="javascript:void(0);" class="badge item-utrack playlist-utrack {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</a>
{% endif %}
{% endif %}
</td>
{% endif %}
<td class="tac">
{% if playlist.time_sync %}
<i class="fa fa-check"></i>
{% else %}
<i class="fa fa-times"></i>
{% endif %}
</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">
</div>
<div class="tile-body">
{% set title = playlist.name|trim %}
{% set title = title if title|length > 0 %}
{{ truncate((title)|default(l.common_untitled), 35, '...') }}
</div>
<div class="tile-footer">
<div class="foot-span">
{% set total_duration = seconds_to_hhmmss(durations[playlist.id]) %}
{% if total_duration %}
{{ total_duration }}
{% else %}
{{ l.common_empty }}
{% endif %}
</td>
<td class="actions tac">
</div>
{% if AUTH_ENABLED %}
{% 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>
{% set creator = track_created(playlist) %}
{% if creator.username %}
<div class="foot-span {% if not creator.enabled %}anonymous{% endif %}">
{{ creator.username }}
</div>
{% endif %}
</td>
</tr>
{% endif %}
{% endif %}
</div>
</a>
{% endfor %}
</tbody>
</table>
<div class="inner-empty empty-flag {% if playlists|length != 0 %}hidden{% endif %}">
<i class="fa fa-play"></i>
</div>
</div>
</div>

View File

@ -6,13 +6,34 @@
{% block add_css %}
{{ HOOK(H_PLAYLIST_CSS) }}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css" />
{% endblock %}
{% block add_js %}
<script type="text/javascript">
var schedule_start_choices = {
'loop': '{{ l.slideshow_slide_form_label_cron_scheduled_loop }}',
'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}',
'cron': '{{ l.slideshow_slide_form_label_cron_scheduled_cron }}',
};
var schedule_end_choices = {
'stayloop': '{{ l.slideshow_slide_form_label_cron_scheduled_stayloop }}',
'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}',
'duration': '{{ l.slideshow_slide_form_label_cron_scheduled_duration }}',
};
var contents = {{ json_dumps(contents) | safe }}
</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/lib/qrcode.min.js"></script>
<script src="{{ STATIC_PREFIX }}js/slideshow/slides.js"></script>
<script src="{{ STATIC_PREFIX }}js/playlist/playlists.js"></script>
<script src="{{ STATIC_PREFIX }}js/restart.js"></script>
{{ HOOK(H_PLAYLIST_JAVASCRIPT) }}
{% endblock %}
{% block body_class %}view-playlist-list{% endblock %}
{% block page %}
<div class="top-content">
@ -26,21 +47,49 @@
<i class="fa fa-play icon-left"></i>
{{ l.playlist_button_add }}
</button>
{% if current_playlist %}
<a href="{{ url_for('playlist_delete', playlist_id=current_playlist.id) }}" class="btn btn-danger-alt protected">
<i class="fa fa-trash"></i>
</a>
{% endif %}
{{ HOOK(H_PLAYLIST_TOOLBAR_ACTIONS_END) }}
</div>
</div>
<div class="alert alert-info tiles-empty empty-flag {% if playlists|length != 0 %}hidden{% endif %}">
{{ l.playlist_panel_empty|replace(
'%link%',
('<a href="javascript:void(0);" class="item-add playlist-add">'~l.playlist_button_add~'</a>')|safe
) }}
</div>
{% if request.args.get('refresh_player') %}
<div class="alert alert-success">
<i class="fa fa-refresh icon-left"></i>
{{ l.slideshow_slide_refresh_player_success|replace('%time%', request.args.get('refresh_player')) }}
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger">
{{ l[error] }}
</div>
{% endif %}
<div class="bottom-content">
<div class="page-content">
<div class="inner">
<div class="panel">
<div class="panel-body">
<h3>{{ l.sysinfo_panel_title }}</h3>
<div class="page-panel left-panel">
{% with playlists=playlists %}
{% include 'playlist/component/table.jinja.html' %}
{% endwith %}
</div>
</div>
<div class="page-content">
<div class="inner">
{% with playlists=playlists %}
{% include 'playlist/component/edit.jinja.html' %}
{% endwith %}
</div>
</div>
</div>
@ -49,6 +98,9 @@
<div class="modals-outer">
<div class="modals-inner">
{% include 'playlist/modal/add.jinja.html' %}
{% include 'playlist/modal/qrcode.jinja.html' %}
{% include 'slideshow/slides/modal/add.jinja.html' %}
{% include 'slideshow/slides/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>

View File

@ -1,37 +0,0 @@
<div class="modal modal-playlist-edit modal-playlist hidden">
<h2>
{{ l.playlist_form_edit_title }}
</h2>
<form class="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="form-group">
<label for="playlist-edit-time-sync">{{ l.playlist_form_label_time_sync }}</label>
<div class="widget">
<select name="time_sync" type="text" id="playlist-edit-time-sync" required="required">
<option value="1"></option>
<option value="0"></option>
</select>
</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>

View File

@ -0,0 +1,13 @@
<div class="modal modal-playlist-qrcode modal-playlist">
<h2>
{{ l.playlist_form_show_qrcode }}
</h2>
<div id="qrcode" class="qrcode-pic"></div>
<div class="actions actions-center">
<button type="button" class="btn btn-naked modal-close">
<i class="fa fa-close icon-left"></i>{{ l.common_close }}
</button>
</div>
</div>

View File

@ -1,77 +1,15 @@
{% extends 'base.jinja.html' %}
{% block page_title %}
{{ l.slideshow_slide_page_title }}
{% endblock %}
{% block add_css %}
<link rel="stylesheet" href="{{ STATIC_PREFIX }}css/lib/flatpickr.min.css" />
{{ HOOK(H_SLIDESHOW_SLIDES_CSS) }}
{% endblock %}
{% block add_js %}
<script type="text/javascript">
var schedule_start_choices = {
'loop': '{{ l.slideshow_slide_form_label_cron_scheduled_loop }}',
'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}',
'cron': '{{ l.slideshow_slide_form_label_cron_scheduled_cron }}',
};
var schedule_end_choices = {
'stayloop': '{{ l.slideshow_slide_form_label_cron_scheduled_stayloop }}',
'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}',
'duration': '{{ l.slideshow_slide_form_label_cron_scheduled_duration }}',
};
var contents = {{ json_dumps(contents) | safe }}
</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_SLIDES_JAVASCRIPT) }}
{% endblock %}
{% block page %}
<div class="toolbar">
<h2><i class="fa-regular fa-clock icon-left"></i>{{ l.slideshow_slide_page_title }}</h2>
<div class="toolbar-actions">
{{ HOOK(H_SLIDESHOW_SLIDES_TOOLBAR_ACTIONS_START) }}
<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_slide_goto_player }}">
<i class="fa fa-eye"></i>
</a>
<a href="{{ url_for('slideshow_player_refresh') }}" class="btn" title="{{ l.slideshow_slide_refresh_player }}">
<i class="fa fa-refresh"></i>
</a>
<button class="purple slide-add item-add"><i class="fa fa-plus icon-left"></i>{{ l.slideshow_slide_button_add }}</button>
{% if PLAYLIST_ENABLED %}
<select class="select-item-picker playlist-picker">
<option value="{{ url_for('slideshow_slide_list') }}" {% if not current_playlist %}selected="selected"{% endif %}>
{{ l.common_default_playlist }}
</option>
{% for playlist in playlists %}
{% set is_active_playlist = str(current_playlist.id) == str(playlist.id) %}
<option value="{{ url_for('slideshow_slide_list_playlist_use', playlist_id=playlist.id) }}" {% if is_active_playlist %}selected="selected"{% endif %}>
{{ playlist.name }}
</option>
{% endfor %}
</select>
{% endif %}
{{ HOOK(H_SLIDESHOW_SLIDES_TOOLBAR_ACTIONS_END) }}
</div>
</div>
{% if request.args.get('refresh_player') %}
<div class="alert alert-success">
<i class="fa fa-refresh icon-left"></i>
{{ l.slideshow_slide_refresh_player_success|replace('%time%', request.args.get('refresh_player')) }}
</div>
{% endif %}
<div class="panel {% if PLAYLIST_ENABLED %}panel-active{% endif %}">
<div class="panel-body">
@ -82,26 +20,5 @@
{% 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/slides/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 'slideshow/slides/modal/add.jinja.html' %}
{% include 'slideshow/slides/modal/edit.jinja.html' %}
{% include 'core/utrack.jinja.html' %}
</div>
</div>
</div>
{% endblock %}

View File

@ -3,7 +3,7 @@
{{ l.slideshow_slide_form_add_title }}
</h2>
<form class="form" action="/slideshow/slide/add" method="POST" enctype="multipart/form-data">
<form class="form" action="{{ url_for('slideshow_slide_add') }}" method="POST" enctype="multipart/form-data">
<h3>
{{ l.slideshow_slide_form_section_content }}
@ -56,25 +56,27 @@
<div class="form-group object-input">
<label for="slide-add-duration">{{ l.slideshow_slide_form_label_object }}</label>
<div class="widget">
<input type="text" name="object" id="slide-add-object-input-text" class="slide-add-object-input" disabled="disabled" />
<input type="file" name="object" id="slide-add-object-input-upload" class="slide-add-object-input hidden" disabled="disabled" />
<input type="text" name="object" id="slide-add-object-input-text" class="slide-add-object-input"
disabled="disabled"/>
<input type="file" name="object" id="slide-add-object-input-upload"
class="slide-add-object-input hidden" disabled="disabled"/>
</div>
</div>
</div>
<h3>
<h3 class="divide">
{{ l.slideshow_slide_form_section_scheduling }}
</h3>
<div class="form-group slide-notification-group">
<label for="slide-add-is-notification">{{ l.slideshow_slide_form_label_is_notification }}</label>
<div class="widget">
<input type="checkbox" class="trigger slide-is-notification" name="is_notification" id="slide-add-is-notification" />
<input type="checkbox" class="trigger slide-is-notification" name="is_notification"
id="slide-add-is-notification"/>
</div>
</div>
<div class="form-group slide-schedule-group">
<label for="slide-add-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
<div class="widget">
@ -83,8 +85,10 @@
<option value="datetime">{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}</option>
<option value="cron">{{ l.slideshow_slide_form_label_cron_scheduled_cron }}</option>
</select>
<input type="text" id="slide-add-cron-schedule-datetimepicker" class="datetimepicker" value="" placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}" />
<input type="text" name="cron_schedule" id="slide-add-cron-schedule" class="target hidden" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" />
<input type="text" id="slide-add-cron-schedule-datetimepicker" class="datetimepicker" value=""
placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}"/>
<input type="text" name="cron_schedule" id="slide-add-cron-schedule" class="target hidden"
placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}"/>
</div>
</div>
@ -96,8 +100,10 @@
<option value="datetime">{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}</option>
<option value="cron">{{ l.slideshow_slide_form_label_cron_scheduled_cron }}</option>
</select>
<input type="text" id="slide-add-cron-schedule-end-datetimepicker" class="datetimepicker" value="" placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}" />
<input type="text" name="cron_schedule_end" id="slide-add-cron-schedule-end" class="target hidden" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" />
<input type="text" id="slide-add-cron-schedule-end-datetimepicker" class="datetimepicker" value=""
placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}"/>
<input type="text" name="cron_schedule_end" id="slide-add-cron-schedule-end" class="target hidden"
placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}"/>
</div>
</div>
@ -110,15 +116,16 @@
</div>
<div class="actions">
<button type="button" class="btn-normal modal-close">
{{ l.slideshow_slide_form_button_cancel }}
<button type="button" class="btn btn-naked modal-close">
{{ l.common_close }}
</button>
<button type="submit" class="green">
<i class="fa fa-plus icon-left"></i> {{ l.slideshow_slide_form_add_submit }}
<button type="submit" class="btn btn-info">
<i class="fa fa-save icon-left"></i>{{ l.common_save }}
</button>
<button type="button" disabled="disabled" class="green hidden btn-loading">
<button type="button" disabled="disabled" class="btn btn-naked hidden btn-loading">
{{ l.common_loading }}
</button>
</div>
</form>
</div>

View File

@ -4,7 +4,7 @@
</h2>
<form class="form" action="/slideshow/slide/edit" method="POST">
<form class="form" action="{{ url_for('slideshow_slide_edit') }}" method="POST">
<h3>
{{ l.slideshow_slide_form_section_content }}
@ -22,14 +22,15 @@
</div>
<h3>
<h3 class="divide">
{{ l.slideshow_slide_form_section_scheduling }}
</h3>
<div class="form-group slide-notification-group">
<label for="slide-edit-is-notification">{{ l.slideshow_slide_form_label_is_notification }}</label>
<div class="widget">
<input type="checkbox" class="trigger slide-is-notification" name="is_notification" id="slide-edit-is-notification" />
<input type="checkbox" class="trigger slide-is-notification" name="is_notification"
id="slide-edit-is-notification"/>
</div>
</div>
@ -41,8 +42,10 @@
<option value="datetime">{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}</option>
<option value="cron">{{ l.slideshow_slide_form_label_cron_scheduled_cron }}</option>
</select>
<input type="text" id="slide-edit-cron-schedule-datetimepicker" class="datetimepicker" value="" placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}" />
<input type="text" name="cron_schedule" id="slide-edit-cron-schedule" class="target hidden" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" />
<input type="text" id="slide-edit-cron-schedule-datetimepicker" class="datetimepicker" value=""
placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}"/>
<input type="text" name="cron_schedule" id="slide-edit-cron-schedule" class="target hidden"
placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}"/>
</div>
</div>
@ -53,8 +56,10 @@
<option value="duration">{{ l.slideshow_slide_form_label_cron_scheduled_duration }}</option>
<option value="datetime">{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}</option>
</select>
<input type="text" id="slide-edit-cron-schedule-end-datetimepicker" class="datetimepicker" value="" placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}" />
<input type="text" name="cron_schedule_end" id="slide-edit-cron-schedule-end" class="target hidden" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" />
<input type="text" id="slide-edit-cron-schedule-end-datetimepicker" class="datetimepicker" value=""
placeholder="{{ l.slideshow_slide_form_label_cron_scheduled_datetime_placeholder }}"/>
<input type="text" name="cron_schedule_end" id="slide-edit-cron-schedule-end" class="target hidden"
placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}"/>
</div>
</div>
@ -67,11 +72,14 @@
</div>
<div class="actions">
<button type="button" class="btn-normal modal-close">
{{ l.slideshow_slide_form_button_cancel }}
<button type="button" class="btn btn-naked modal-close">
{{ l.common_close }}
</button>
<button type="submit" class="green">
<i class="fa fa-save icon-left"></i>{{ l.slideshow_slide_form_edit_submit }}
<button type="submit" class="btn btn-info">
<i class="fa fa-save icon-left"></i>{{ l.common_save }}
</button>
<button type="button" disabled="disabled" class="btn btn-naked hidden btn-loading">
{{ l.common_loading }}
</button>
</div>
</form>