commit
0c9c02751f
@ -5,6 +5,8 @@
|
|||||||
## About
|
## About
|
||||||
Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
|
Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
|
||||||
|
|
||||||
|
[](https://hub.docker.com/r/jierka/obscreen/)
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
- Dead simple chromium webview
|
- Dead simple chromium webview
|
||||||
- Clear GUI
|
- Clear GUI
|
||||||
@ -18,7 +20,7 @@ Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation and configure (docker)
|
## Installation and configuration (docker)
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/jr-k/obscreen.git
|
git clone https://github.com/jr-k/obscreen.git
|
||||||
cd obscreen
|
cd obscreen
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
"location",
|
"location",
|
||||||
"name",
|
"name",
|
||||||
"position",
|
"position",
|
||||||
"type"
|
"type",
|
||||||
|
"cron_schedule"
|
||||||
],
|
],
|
||||||
"data": {
|
"data": {
|
||||||
"0": {
|
"0": {
|
||||||
@ -15,7 +16,8 @@
|
|||||||
"type": "picture",
|
"type": "picture",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "Picture Sample",
|
"name": "Picture Sample",
|
||||||
"position": 0
|
"position": 0,
|
||||||
|
"cron_schedule": null
|
||||||
},
|
},
|
||||||
"1": {
|
"1": {
|
||||||
"location": "https://unix.org",
|
"location": "https://unix.org",
|
||||||
@ -23,7 +25,8 @@
|
|||||||
"type": "url",
|
"type": "url",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "URL Sample",
|
"name": "URL Sample",
|
||||||
"position": 1
|
"position": 1,
|
||||||
|
"cron_schedule": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -512,6 +512,14 @@ form .form-group textarea {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form .form-group input[type=checkbox] {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group input[type=checkbox].trigger {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
form .form-group span {
|
form .form-group span {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
101
data/www/js/is-cron-now.js
Executable file
101
data/www/js/is-cron-now.js
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
const cron = (() => {
|
||||||
|
const DAY_NAMES = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||||||
|
const DAY_ABBRS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||||
|
const MINUTE_CHOICES = Array.from({length: 60}, (_, i) => [`${i}`, `${i}`]);
|
||||||
|
const HOUR_CHOICES = Array.from({length: 24}, (_, i) => [`${i}`, `${i}`]);
|
||||||
|
const DOM_CHOICES = Array.from({length: 31}, (_, i) => [`${i + 1}`, `${i + 1}`]);
|
||||||
|
const MONTH_CHOICES = Array.from({length: 12}, (_, i) => [`${i + 1}`, new Date(0, i).toLocaleString('en', { month: 'long' })]);
|
||||||
|
const DOW_CHOICES = DAY_NAMES.map((day, index) => [`${index}`, day]);
|
||||||
|
|
||||||
|
function toInt(value, allowDaynames = false) {
|
||||||
|
if (typeof value === 'number' || (typeof value === 'string' && !isNaN(value))) {
|
||||||
|
return parseInt(value, 10);
|
||||||
|
} else if (allowDaynames && typeof value === 'string') {
|
||||||
|
value = value.toLowerCase();
|
||||||
|
const dayIndex = DAY_NAMES.indexOf(value);
|
||||||
|
if (dayIndex !== -1) return dayIndex;
|
||||||
|
const abbrIndex = DAY_ABBRS.indexOf(value);
|
||||||
|
if (abbrIndex !== -1) return abbrIndex;
|
||||||
|
}
|
||||||
|
throw new Error('Failed to parse string to integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArg(value, target, allowDaynames = false) {
|
||||||
|
value = value.trim();
|
||||||
|
if (value === '*') return true;
|
||||||
|
let values = value.split(',').map(v => v.trim()).filter(v => v);
|
||||||
|
|
||||||
|
for (let val of values) {
|
||||||
|
try {
|
||||||
|
if (toInt(val, allowDaynames) === target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
if (val.includes('-')) {
|
||||||
|
let step = 1;
|
||||||
|
let start, end;
|
||||||
|
if (val.includes('/')) {
|
||||||
|
[start, end] = val.split('-').map(part => part.trim());
|
||||||
|
[end, step] = end.split('/').map(part => toInt(part.trim(), allowDaynames));
|
||||||
|
start = toInt(start, allowDaynames);
|
||||||
|
} else {
|
||||||
|
[start, end] = val.split('-').map(part => toInt(part.trim(), allowDaynames));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i += step) {
|
||||||
|
if (i === target) return true;
|
||||||
|
}
|
||||||
|
if (allowDaynames && start > end) {
|
||||||
|
for (let i = start; i < start + 7; i += step) {
|
||||||
|
if (i % 7 === target) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.includes('/')) {
|
||||||
|
let [v, interval] = val.split('/').map(part => part.trim());
|
||||||
|
if (v !== '*') continue;
|
||||||
|
if (target % toInt(interval, allowDaynames) === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBeen(s, since, dt = new Date()) {
|
||||||
|
since = new Date(since);
|
||||||
|
dt = new Date(dt);
|
||||||
|
|
||||||
|
if (dt < since) {
|
||||||
|
throw new Error("The 'since' datetime must be before the current datetime.");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (since <= dt) {
|
||||||
|
if (isActive(s, since)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
since = new Date(since.getTime() + 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(s, dt = new Date()) {
|
||||||
|
let [minute, hour, dom, month, dow, year] = s.split(' ');
|
||||||
|
let weekday = dt.getDay();
|
||||||
|
|
||||||
|
return parseArg(minute, dt.getMinutes()) &&
|
||||||
|
parseArg(hour, dt.getHours()) &&
|
||||||
|
parseArg(dom, dt.getDate()) &&
|
||||||
|
parseArg(month, dt.getMonth() + 1) &&
|
||||||
|
parseArg(dow, weekday === 0 ? 6 : weekday - 1, true) &&
|
||||||
|
(!year || (year && parseArg(year, dt.getFullYear())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isActive
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -49,7 +49,7 @@ jQuery(document).ready(function ($) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$(document).on('change', 'input[type=checkbox]', function () {
|
$(document).on('change', '.slide-item input[type=checkbox]', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/slideshow/slide/toggle',
|
url: '/slideshow/slide/toggle',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
@ -68,6 +68,16 @@ jQuery(document).ready(function ($) {
|
|||||||
updateTable();
|
updateTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('change', '.modal-slide input[type=checkbox]', function () {
|
||||||
|
const $target = $('#'+ $(this).attr('id').replace('-trigger', ''));
|
||||||
|
const hide = !$(this).is(':checked');
|
||||||
|
$target.toggleClass('hidden', hide);
|
||||||
|
|
||||||
|
if (hide) {
|
||||||
|
$target.val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('change', '#slide-add-type', function () {
|
$(document).on('change', '#slide-add-type', function () {
|
||||||
const value = $(this).val();
|
const value = $(this).val();
|
||||||
const inputType = $(this).find('option').filter(function (i, el) {
|
const inputType = $(this).find('option').filter(function (i, el) {
|
||||||
@ -95,11 +105,14 @@ jQuery(document).ready(function ($) {
|
|||||||
$(document).on('click', '.slide-edit', function () {
|
$(document).on('click', '.slide-edit', function () {
|
||||||
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
|
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
|
||||||
showModal('modal-slide-edit');
|
showModal('modal-slide-edit');
|
||||||
|
const hasCron = slide.cron_schedule && slide.cron_schedule.length > 0;
|
||||||
$('.modal-slide-edit input:visible:eq(0)').focus().select();
|
$('.modal-slide-edit input:visible:eq(0)').focus().select();
|
||||||
$('#slide-edit-name').val(slide.name);
|
$('#slide-edit-name').val(slide.name);
|
||||||
$('#slide-edit-type').val(slide.type);
|
$('#slide-edit-type').val(slide.type);
|
||||||
$('#slide-edit-location').val(slide.location);
|
$('#slide-edit-location').val(slide.location);
|
||||||
$('#slide-edit-duration').val(slide.duration);
|
$('#slide-edit-duration').val(slide.duration);
|
||||||
|
$('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron);
|
||||||
|
$('#slide-edit-cron-schedule-trigger').prop('checked', hasCron);
|
||||||
$('#slide-edit-id').val(slide.id);
|
$('#slide-edit-id').val(slide.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
BIN
docs/screenshot.png
Normal file → Executable file
BIN
docs/screenshot.png
Normal file → Executable file
Binary file not shown.
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 267 KiB |
@ -8,7 +8,10 @@
|
|||||||
"slideshow_slide_panel_th_duration": "Duration",
|
"slideshow_slide_panel_th_duration": "Duration",
|
||||||
"slideshow_slide_panel_th_duration_unit": "sec",
|
"slideshow_slide_panel_th_duration_unit": "sec",
|
||||||
"slideshow_slide_panel_th_enabled": "Enabled",
|
"slideshow_slide_panel_th_enabled": "Enabled",
|
||||||
|
"slideshow_slide_panel_th_cron_scheduled": "Scheduled",
|
||||||
"slideshow_slide_panel_th_activity": "Activity",
|
"slideshow_slide_panel_th_activity": "Activity",
|
||||||
|
"slideshow_slide_panel_td_cron_scheduled_loop": "Loop",
|
||||||
|
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Bad cron value",
|
||||||
"slideshow_slide_form_add_title": "Add Slide",
|
"slideshow_slide_form_add_title": "Add Slide",
|
||||||
"slideshow_slide_form_add_submit": "Add",
|
"slideshow_slide_form_add_submit": "Add",
|
||||||
"slideshow_slide_form_edit_title": "Edit Slide",
|
"slideshow_slide_form_edit_title": "Edit Slide",
|
||||||
@ -22,6 +25,8 @@
|
|||||||
"slideshow_slide_form_label_object": "Object",
|
"slideshow_slide_form_label_object": "Object",
|
||||||
"slideshow_slide_form_label_duration": "Duration",
|
"slideshow_slide_form_label_duration": "Duration",
|
||||||
"slideshow_slide_form_label_duration_unit": "seconds",
|
"slideshow_slide_form_label_duration_unit": "seconds",
|
||||||
|
"slideshow_slide_form_label_cron_scheduled": "Scheduled",
|
||||||
|
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Use crontab format: * * * * *",
|
||||||
"slideshow_slide_form_button_cancel": "Cancel",
|
"slideshow_slide_form_button_cancel": "Cancel",
|
||||||
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,10 @@
|
|||||||
"slideshow_slide_panel_th_duration": "Durée",
|
"slideshow_slide_panel_th_duration": "Durée",
|
||||||
"slideshow_slide_panel_th_duration_unit": "sec",
|
"slideshow_slide_panel_th_duration_unit": "sec",
|
||||||
"slideshow_slide_panel_th_enabled": "Activé",
|
"slideshow_slide_panel_th_enabled": "Activé",
|
||||||
|
"slideshow_slide_panel_th_cron_scheduled": "Programmation",
|
||||||
"slideshow_slide_panel_th_activity": "Options",
|
"slideshow_slide_panel_th_activity": "Options",
|
||||||
|
"slideshow_slide_panel_td_cron_scheduled_loop": "En boucle",
|
||||||
|
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Mauvaise valeur cron",
|
||||||
"slideshow_slide_form_add_title": "Ajouter d'une slide",
|
"slideshow_slide_form_add_title": "Ajouter d'une slide",
|
||||||
"slideshow_slide_form_add_submit": "Ajouter",
|
"slideshow_slide_form_add_submit": "Ajouter",
|
||||||
"slideshow_slide_form_edit_title": "Modification d'une slide",
|
"slideshow_slide_form_edit_title": "Modification d'une slide",
|
||||||
@ -22,6 +25,8 @@
|
|||||||
"slideshow_slide_form_label_object": "Objet",
|
"slideshow_slide_form_label_object": "Objet",
|
||||||
"slideshow_slide_form_label_duration": "Durée",
|
"slideshow_slide_form_label_duration": "Durée",
|
||||||
"slideshow_slide_form_label_duration_unit": "secondes",
|
"slideshow_slide_form_label_duration_unit": "secondes",
|
||||||
|
"slideshow_slide_form_label_cron_scheduled": "Programmer",
|
||||||
|
"slideshow_slide_form_widget_cron_scheduled_placeholder": "Utiliser le format crontab: * * * * *",
|
||||||
"slideshow_slide_form_button_cancel": "Annuler",
|
"slideshow_slide_form_button_cancel": "Annuler",
|
||||||
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
flask==2.3.3
|
flask==2.3.3
|
||||||
pysondb-v2==2.1.0
|
pysondb-v2==2.1.0
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
cron-descriptor
|
||||||
|
|
||||||
|
|||||||
@ -3,18 +3,31 @@ import json
|
|||||||
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
|
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
|
||||||
from src.service.ModelStore import ModelStore
|
from src.service.ModelStore import ModelStore
|
||||||
from src.interface.ObController import ObController
|
from src.interface.ObController import ObController
|
||||||
from src.utils import get_ip_address
|
from src.utils import get_ip_address, get_safe_cron_descriptor
|
||||||
|
|
||||||
|
|
||||||
class PlayerController(ObController):
|
class PlayerController(ObController):
|
||||||
|
|
||||||
def _get_playlist(self) -> dict:
|
def _get_playlist(self) -> dict:
|
||||||
slides = self._model_store.slide().to_dict(self._model_store.slide().get_enabled_slides())
|
enabled_slides = self._model_store.slide().get_enabled_slides()
|
||||||
|
slides = self._model_store.slide().to_dict(enabled_slides)
|
||||||
|
|
||||||
if len(slides) == 1:
|
playlist_loop = []
|
||||||
return [slides[0], slides[0]]
|
playlist_cron = []
|
||||||
|
|
||||||
return slides
|
for slide in slides:
|
||||||
|
if 'cron_schedule' in slide and slide['cron_schedule']:
|
||||||
|
if get_safe_cron_descriptor(slide['cron_schedule']):
|
||||||
|
playlist_cron.append(slide)
|
||||||
|
else:
|
||||||
|
playlist_loop.append(slide)
|
||||||
|
|
||||||
|
playlists = {
|
||||||
|
'loop': playlist_loop,
|
||||||
|
'cron': playlist_cron
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlists
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
self._app.add_url_rule('/', 'player', self.player, methods=['GET'])
|
self._app.add_url_rule('/', 'player', self.player, methods=['GET'])
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from src.service.ModelStore import ModelStore
|
|||||||
from src.model.entity.Slide import Slide
|
from src.model.entity.Slide import Slide
|
||||||
from src.model.enum.SlideType import SlideType
|
from src.model.enum.SlideType import SlideType
|
||||||
from src.interface.ObController import ObController
|
from src.interface.ObController import ObController
|
||||||
from src.utils import str_to_enum
|
from src.utils import str_to_enum, get_optional_string
|
||||||
|
|
||||||
|
|
||||||
class SlideshowController(ObController):
|
class SlideshowController(ObController):
|
||||||
@ -40,6 +40,7 @@ class SlideshowController(ObController):
|
|||||||
name=request.form['name'],
|
name=request.form['name'],
|
||||||
type=str_to_enum(request.form['type'], SlideType),
|
type=str_to_enum(request.form['type'], SlideType),
|
||||||
duration=request.form['duration'],
|
duration=request.form['duration'],
|
||||||
|
cron_schedule=get_optional_string(request.form['cron_schedule']),
|
||||||
)
|
)
|
||||||
|
|
||||||
if slide.has_file():
|
if slide.has_file():
|
||||||
@ -65,7 +66,7 @@ class SlideshowController(ObController):
|
|||||||
return redirect(url_for('slideshow_slide_list'))
|
return redirect(url_for('slideshow_slide_list'))
|
||||||
|
|
||||||
def slideshow_slide_edit(self):
|
def slideshow_slide_edit(self):
|
||||||
self._model_store.slide().update_form(request.form['id'], request.form['name'], request.form['duration'])
|
self._model_store.slide().update_form(request.form['id'], request.form['name'], request.form['duration'], request.form['cron_schedule'])
|
||||||
self._post_update()
|
self._post_update()
|
||||||
return redirect(url_for('slideshow_slide_list'))
|
return redirect(url_for('slideshow_slide_list'))
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class LangManager:
|
|||||||
|
|
||||||
def __init__(self, lang: str):
|
def __init__(self, lang: str):
|
||||||
self._map = {}
|
self._map = {}
|
||||||
self._lang = lang
|
self._lang = lang.lower()
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
def load(self, directory: str = "", prefix: str = ""):
|
def load(self, directory: str = "", prefix: str = ""):
|
||||||
@ -23,3 +23,6 @@ class LangManager:
|
|||||||
|
|
||||||
def map(self) -> dict:
|
def map(self) -> dict:
|
||||||
return self._map
|
return self._map
|
||||||
|
|
||||||
|
def get_locale(self, local_with_country: bool = False) -> str:
|
||||||
|
return "{}_{}".format(self._lang, self._lang.upper()) if local_with_country else self._lang
|
||||||
@ -2,7 +2,7 @@ import os
|
|||||||
|
|
||||||
from typing import Dict, Optional, List, Tuple, Union
|
from typing import Dict, Optional, List, Tuple, Union
|
||||||
from src.model.entity.Slide import Slide
|
from src.model.entity.Slide import Slide
|
||||||
from src.utils import str_to_enum
|
from src.utils import str_to_enum, get_optional_string
|
||||||
from pysondb import PysonDB
|
from pysondb import PysonDB
|
||||||
from pysondb.errors import IdDoesNotExistError
|
from pysondb.errors import IdDoesNotExistError
|
||||||
|
|
||||||
@ -69,8 +69,12 @@ class SlideManager:
|
|||||||
for slide_id, slide_position in positions.items():
|
for slide_id, slide_position in positions.items():
|
||||||
self._db.update_by_id(slide_id, {"position": slide_position})
|
self._db.update_by_id(slide_id, {"position": slide_position})
|
||||||
|
|
||||||
def update_form(self, id: str, name: str, duration: int) -> None:
|
def update_form(self, id: str, name: str, duration: int, cron_schedule: Optional[str] = '') -> None:
|
||||||
self._db.update_by_id(id, {"name": name, "duration": duration})
|
self._db.update_by_id(id, {
|
||||||
|
"name": name,
|
||||||
|
"duration": duration,
|
||||||
|
"cron_schedule": get_optional_string(cron_schedule)
|
||||||
|
})
|
||||||
|
|
||||||
def add_form(self, slide: Union[Slide, Dict]) -> None:
|
def add_form(self, slide: Union[Slide, Dict]) -> None:
|
||||||
db_slide = slide
|
db_slide = slide
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from src.utils import str_to_enum
|
|||||||
|
|
||||||
class Slide:
|
class Slide:
|
||||||
|
|
||||||
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None):
|
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[str] = None, cron_schedule: Optional[str] = None):
|
||||||
self._id = id if id else None
|
self._id = id if id else None
|
||||||
self._location = location
|
self._location = location
|
||||||
self._duration = duration
|
self._duration = duration
|
||||||
@ -15,6 +15,7 @@ class Slide:
|
|||||||
self._enabled = enabled
|
self._enabled = enabled
|
||||||
self._name = name
|
self._name = name
|
||||||
self._position = position
|
self._position = position
|
||||||
|
self._cron_schedule = cron_schedule
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> Optional[str]:
|
def id(self) -> Optional[str]:
|
||||||
@ -36,6 +37,14 @@ class Slide:
|
|||||||
def type(self, value: SlideType):
|
def type(self, value: SlideType):
|
||||||
self._type = value
|
self._type = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cron_schedule(self) -> Optional[str]:
|
||||||
|
return self._cron_schedule
|
||||||
|
|
||||||
|
@cron_schedule.setter
|
||||||
|
def cron_schedule(self, value: Optional[str]):
|
||||||
|
self._cron_schedule = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration(self) -> int:
|
def duration(self) -> int:
|
||||||
return self._duration
|
return self._duration
|
||||||
@ -77,6 +86,7 @@ class Slide:
|
|||||||
f"duration='{self.duration}',\n" \
|
f"duration='{self.duration}',\n" \
|
||||||
f"position='{self.position}',\n" \
|
f"position='{self.position}',\n" \
|
||||||
f"location='{self.location}',\n" \
|
f"location='{self.location}',\n" \
|
||||||
|
f"cron_schedule='{self.cron_schedule}',\n" \
|
||||||
f")"
|
f")"
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
@ -91,9 +101,11 @@ class Slide:
|
|||||||
"type": self.type.value,
|
"type": self.type.value,
|
||||||
"duration": self.duration,
|
"duration": self.duration,
|
||||||
"location": self.location,
|
"location": self.location,
|
||||||
|
"cron_schedule": self.cron_schedule,
|
||||||
}
|
}
|
||||||
|
|
||||||
def has_file(self) -> bool:
|
def has_file(self) -> bool:
|
||||||
return (self.type == SlideType.VIDEO
|
return (
|
||||||
|
self.type == SlideType.VIDEO
|
||||||
or self.type == SlideType.PICTURE
|
or self.type == SlideType.PICTURE
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from src.model.hook.HookRegistration import HookRegistration
|
|||||||
from src.model.hook.StaticHookRegistration import StaticHookRegistration
|
from src.model.hook.StaticHookRegistration import StaticHookRegistration
|
||||||
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
|
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
|
||||||
from src.constant.WebDirConstant import WebDirConstant
|
from src.constant.WebDirConstant import WebDirConstant
|
||||||
|
from src.utils import get_safe_cron_descriptor
|
||||||
|
|
||||||
|
|
||||||
class TemplateRenderer:
|
class TemplateRenderer:
|
||||||
@ -18,6 +19,9 @@ class TemplateRenderer:
|
|||||||
self._model_store = model_store
|
self._model_store = model_store
|
||||||
self._render_hook = render_hook
|
self._render_hook = render_hook
|
||||||
|
|
||||||
|
def cron_descriptor(self, expression: str, use_24hour_time_format=True) -> str:
|
||||||
|
return get_safe_cron_descriptor(expression, use_24hour_time_format, self._model_store.lang().get_locale(local_with_country=True))
|
||||||
|
|
||||||
def get_view_globals(self) -> dict:
|
def get_view_globals(self) -> dict:
|
||||||
globals = dict(
|
globals = dict(
|
||||||
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
|
STATIC_PREFIX="/{}/{}/".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_ASSETS),
|
||||||
@ -25,6 +29,7 @@ class TemplateRenderer:
|
|||||||
VERSION=self._model_store.config().map().get('version'),
|
VERSION=self._model_store.config().map().get('version'),
|
||||||
LANG=self._model_store.variable().map().get('lang').as_string(),
|
LANG=self._model_store.variable().map().get('lang').as_string(),
|
||||||
HOOK=self._render_hook,
|
HOOK=self._render_hook,
|
||||||
|
cron_descriptor=self.cron_descriptor
|
||||||
)
|
)
|
||||||
|
|
||||||
for hook in HookType:
|
for hook in HookType:
|
||||||
|
|||||||
@ -33,7 +33,6 @@ class WebServer:
|
|||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
self._setup_flask_app()
|
self._setup_flask_app()
|
||||||
self._setup_web_globals()
|
self._setup_web_globals()
|
||||||
self._setup_web_extensions()
|
|
||||||
self._setup_web_errors()
|
self._setup_web_errors()
|
||||||
self._setup_web_controllers()
|
self._setup_web_controllers()
|
||||||
|
|
||||||
@ -73,12 +72,8 @@ class WebServer:
|
|||||||
def inject_global_vars() -> dict:
|
def inject_global_vars() -> dict:
|
||||||
return self._template_renderer.get_view_globals()
|
return self._template_renderer.get_view_globals()
|
||||||
|
|
||||||
def _setup_web_extensions(self) -> None:
|
|
||||||
@self._app.template_filter('ctime')
|
|
||||||
def time_ctime(s):
|
|
||||||
return time.ctime(s)
|
|
||||||
|
|
||||||
def _setup_web_errors(self) -> None:
|
def _setup_web_errors(self) -> None:
|
||||||
@self._app.errorhandler(404)
|
@self._app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return send_from_directory(self._get_template_folder(), 'core/error404.html'), 404
|
return send_from_directory(self._get_template_folder(), 'core/error404.html'), 404
|
||||||
|
|
||||||
|
|||||||
34
src/utils.py
34
src/utils.py
@ -4,6 +4,38 @@ import platform
|
|||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from cron_descriptor import ExpressionDescriptor
|
||||||
|
from cron_descriptor.Exception import FormatException, WrongArgumentException, MissingFieldException
|
||||||
|
|
||||||
|
|
||||||
|
def get_safe_cron_descriptor(expression: str, use_24hour_time_format=True, locale_code: Optional[str] = None) -> str:
|
||||||
|
options = {
|
||||||
|
"expression": expression,
|
||||||
|
"use_24hour_time_format": use_24hour_time_format
|
||||||
|
}
|
||||||
|
|
||||||
|
if locale_code:
|
||||||
|
options["locale_code"] = locale_code
|
||||||
|
try:
|
||||||
|
return str(ExpressionDescriptor(**options))
|
||||||
|
except FormatException:
|
||||||
|
return ''
|
||||||
|
except WrongArgumentException:
|
||||||
|
return ''
|
||||||
|
except MissingFieldException:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def get_optional_string(var: Optional[str]) -> Optional[str]:
|
||||||
|
if var is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
var = var.strip()
|
||||||
|
|
||||||
|
if var:
|
||||||
|
return var
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') -> Optional[List]:
|
def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') -> Optional[List]:
|
||||||
@ -27,12 +59,14 @@ def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') ->
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def str_to_enum(str_val: str, enum_class) -> Enum:
|
def str_to_enum(str_val: str, enum_class) -> Enum:
|
||||||
for enum_item in enum_class:
|
for enum_item in enum_class:
|
||||||
if enum_item.value == str_val:
|
if enum_item.value == str_val:
|
||||||
return enum_item
|
return enum_item
|
||||||
raise ValueError(f"{str_val} is not a valid {enum_class.__name__} item")
|
raise ValueError(f"{str_val} is not a valid {enum_class.__name__} item")
|
||||||
|
|
||||||
|
|
||||||
def get_ip_address() -> Optional[str]:
|
def get_ip_address() -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
os_name = platform.system().lower()
|
os_name = platform.system().lower()
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
1.5
|
1.6
|
||||||
|
|||||||
@ -12,19 +12,20 @@
|
|||||||
.slide iframe { background: white; }
|
.slide iframe { background: white; }
|
||||||
.slide img { height: 100%; }
|
.slide img { height: 100%; }
|
||||||
</style>
|
</style>
|
||||||
|
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/is-cron-now.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="FirstSlide" class="slide" style="z-index: 1000;">
|
<div id="FirstSlide" class="slide" style="z-index: 1000;">
|
||||||
<iframe src="/player/default"></iframe>
|
<iframe src="/player/default"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div id="SecondSlide" class="slide" style="z-index: 500">
|
<div id="SecondSlide" class="slide" style="z-index: 500;">
|
||||||
<iframe src="/player/default"></iframe>
|
<iframe src="/player/default"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var items = {{items | safe}};
|
var items = {{items | safe}};
|
||||||
var duration = 3000 / 1;
|
var duration = 3000 / 1;
|
||||||
var playlistCheck = 10 * 1000; // 10 seconds check
|
var playlistCheck = 10 * 1000; // 10 seconds check
|
||||||
var curUrl = 0;
|
var curItemIndex = 0;
|
||||||
var nextReady = true;
|
var nextReady = true;
|
||||||
var itemCheck = setInterval(function () {
|
var itemCheck = setInterval(function () {
|
||||||
fetch('player/playlist').then(function(response) {
|
fetch('player/playlist').then(function(response) {
|
||||||
@ -37,53 +38,94 @@
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
}, playlistCheck);
|
}, playlistCheck);
|
||||||
|
|
||||||
var animate = {{ 'true' if slide_animation_enabled.eval() else 'false' }};
|
var animate = {{ 'true' if slide_animation_enabled.eval() else 'false' }};
|
||||||
var animate_transitions = [
|
var animate_transitions = [
|
||||||
"animate__{{ slide_animation_entrance_effect.eval()|default("fadeIn") }}",
|
"animate__{{ slide_animation_entrance_effect.eval()|default("fadeIn") }}",
|
||||||
"animate__{{ slide_animation_exit_effect.eval()|default("none") }}"
|
"animate__{{ slide_animation_exit_effect.eval()|default("none") }}"
|
||||||
];
|
];
|
||||||
var animate_speed = "animate__{{ slide_animation_speed.eval()|default("normal") }}";
|
var animate_speed = "animate__{{ slide_animation_speed.eval()|default("normal") }}";
|
||||||
|
var firstSlide = document.getElementById('FirstSlide');
|
||||||
|
var secondSlide = document.getElementById('SecondSlide');
|
||||||
|
var previousSlide = secondSlide;
|
||||||
|
var curSlide = firstSlide;
|
||||||
|
var cronState = {
|
||||||
|
active: false,
|
||||||
|
itemIndex: null,
|
||||||
|
interval: null
|
||||||
|
};
|
||||||
|
|
||||||
function main() {
|
var cronTick = function() {
|
||||||
preloadIntoSecondSlide();
|
if ((new Date()).getSeconds() != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('Cron Tick');
|
||||||
|
|
||||||
|
for (var i = 0; i < items.cron.length; i++) {
|
||||||
|
var item = items.cron[i];
|
||||||
|
|
||||||
|
if (cron.isActive(item.cron_schedule) && cronState.itemIndex != i) {
|
||||||
|
cronState.active = true;
|
||||||
|
cronState.itemIndex = i;
|
||||||
|
var callbackReady = function (onSlideStart) {
|
||||||
|
onSlideStart();
|
||||||
|
moveToSlide(curSlide.attributes['id'].value, item);
|
||||||
|
var move = function () {
|
||||||
|
if (nextReady) {
|
||||||
|
curItemIndex = (curItemIndex + 1) === items.loop.length ? 0 : curItemIndex + 1;
|
||||||
|
cronState.active = false;
|
||||||
|
cronState.itemIndex = null;
|
||||||
|
} else {
|
||||||
|
setTimeout(move, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(move, item.duration * 1000);
|
||||||
|
};
|
||||||
|
loadContent(curSlide, callbackReady, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadContent(element, callbackReady) {
|
function main() {
|
||||||
switch (items[curUrl].type) {
|
preloadSlide('SecondSlide', items.loop[curItemIndex])
|
||||||
|
cronState.interval = setInterval(cronTick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadContent(element, callbackReady, item) {
|
||||||
|
switch (item.type) {
|
||||||
case 'url':
|
case 'url':
|
||||||
loadUrl(element, callbackReady);
|
loadUrl(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
case 'picture':
|
case 'picture':
|
||||||
loadPicture(element, callbackReady);
|
loadPicture(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
case 'video':
|
case 'video':
|
||||||
loadVideo(element, callbackReady);
|
loadVideo(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
loadUrl(element, callbackReady);
|
loadUrl(element, callbackReady, item);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUrl(element, callbackReady) {
|
function loadUrl(element, callbackReady, item) {
|
||||||
element.innerHTML = `<iframe src="${items[curUrl].location}"></iframe>`;
|
element.innerHTML = `<iframe src="${item.location}"></iframe>`;
|
||||||
callbackReady(function () {});
|
callbackReady(function () {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPicture(element, callbackReady) {
|
function loadPicture(element, callbackReady, item) {
|
||||||
element.innerHTML = `<img src="${items[curUrl].location}" alt="" />`;
|
element.innerHTML = `<img src="${item.location}" alt="" />`;
|
||||||
callbackReady(function () {});
|
callbackReady(function () {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadVideo(element, callbackReady) {
|
function loadVideo(element, callbackReady, item) {
|
||||||
element.innerHTML = `<video><source src=${items[curUrl].location} type="video/mp4" /></video>`;
|
element.innerHTML = `<video><source src=${item.location} type="video/mp4" /></video>`;
|
||||||
var video = element.querySelector('video');
|
var video = element.querySelector('video');
|
||||||
video.addEventListener('loadedmetadata', function () {
|
video.addEventListener('loadedmetadata', function () {
|
||||||
items[curUrl].duration = Math.ceil(video.duration);
|
item.duration = Math.ceil(video.duration);
|
||||||
});
|
});
|
||||||
|
|
||||||
callbackReady(function () {
|
var onSlideStart = function () {
|
||||||
nextReady = false;
|
nextReady = false;
|
||||||
video.play();
|
video.play();
|
||||||
video.addEventListener('ended', function () {
|
video.addEventListener('ended', function () {
|
||||||
@ -91,17 +133,17 @@
|
|||||||
});
|
});
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
nextReady = true;
|
nextReady = true;
|
||||||
}, Math.ceil(items[curUrl].duration * 1.5));
|
}, Math.ceil(item.duration * 1.5));
|
||||||
});
|
};
|
||||||
|
callbackReady(onSlideStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadIntoFirstSlide() {
|
function preloadSlide(slide, item) {
|
||||||
//console.log('preloadIntoFirstSlide', items[curUrl]);
|
var element = document.getElementById(slide);
|
||||||
var element = document.getElementById('FirstSlide');
|
|
||||||
var callbackReady = function (onSlideStart) {
|
var callbackReady = function (onSlideStart) {
|
||||||
var move = function () {
|
var move = function () {
|
||||||
if (nextReady) {
|
if (nextReady && !cronState.active) {
|
||||||
moveToFirstSlide();
|
moveToSlide(slide, item);
|
||||||
onSlideStart();
|
onSlideStart();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(move, 1000);
|
setTimeout(move, 1000);
|
||||||
@ -111,76 +153,31 @@
|
|||||||
setTimeout(move, duration);
|
setTimeout(move, duration);
|
||||||
};
|
};
|
||||||
|
|
||||||
loadContent(element, callbackReady);
|
loadContent(element, callbackReady, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveToFirstSlide() {
|
function moveToSlide(slide, item) {
|
||||||
//console.log('moveToFirstSlide', items[curUrl]);
|
curSlide = document.getElementById(slide);
|
||||||
var firstSlide = document.querySelector('#FirstSlide');
|
previousSlide = curSlide == firstSlide ? secondSlide : firstSlide;
|
||||||
var secondSlide = document.querySelector('#SecondSlide');
|
|
||||||
|
|
||||||
duration = items[curUrl].duration * 1000;
|
duration = item.duration * 1000;
|
||||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
curItemIndex = (curItemIndex + 1) === items.loop.length ? 0 : curItemIndex + 1;
|
||||||
|
|
||||||
firstSlide.style.zIndex = 1000;
|
curSlide.style.zIndex = 1000;
|
||||||
secondSlide.style.zIndex = 500;
|
previousSlide.style.zIndex = 500;
|
||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
firstSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
|
curSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
|
||||||
firstSlide.onanimationend = () => {
|
curSlide.onanimationend = () => {
|
||||||
firstSlide.classList.remove(animate_transitions[0], animate_speed);
|
curSlide.classList.remove(animate_transitions[0], animate_speed);
|
||||||
preloadIntoSecondSlide();
|
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
|
||||||
};
|
};
|
||||||
secondSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
|
previousSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
|
||||||
secondSlide.onanimationend = () => {
|
previousSlide.onanimationend = () => {
|
||||||
secondSlide.classList.remove(animate_transitions[1], animate_speed);
|
previousSlide.classList.remove(animate_transitions[1], animate_speed);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
preloadIntoSecondSlide();
|
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, duration);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadContent(element, callbackReady);
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveToSecondSlide() {
|
|
||||||
//console.log('moveToSecondSlide', items[curUrl]);
|
|
||||||
var firstSlide = document.querySelector('#FirstSlide');
|
|
||||||
var secondSlide = document.querySelector('#SecondSlide');
|
|
||||||
duration = items[curUrl].duration * 1000;
|
|
||||||
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
|
|
||||||
|
|
||||||
secondSlide.style.zIndex = 1000;
|
|
||||||
firstSlide.style.zIndex = 500;
|
|
||||||
|
|
||||||
if (animate) {
|
|
||||||
secondSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
|
|
||||||
secondSlide.onanimationend = () => {
|
|
||||||
secondSlide.classList.remove(animate_transitions[0], animate_speed);
|
|
||||||
preloadIntoFirstSlide();
|
|
||||||
};
|
|
||||||
firstSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
|
|
||||||
firstSlide.onanimationend = () => {
|
|
||||||
firstSlide.classList.remove(animate_transitions[1], animate_speed);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
preloadIntoFirstSlide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<th>{{ l.slideshow_slide_panel_th_name }}</th>
|
<th>{{ l.slideshow_slide_panel_th_name }}</th>
|
||||||
<th class="tac">{{ l.slideshow_slide_panel_th_duration }}</th>
|
<th class="tac">{{ l.slideshow_slide_panel_th_duration }}</th>
|
||||||
<th class="tac">{{ l.slideshow_slide_panel_th_enabled }}</th>
|
<th class="tac">{{ l.slideshow_slide_panel_th_enabled }}</th>
|
||||||
|
<th class="">{{ l.slideshow_slide_panel_th_cron_scheduled }}</th>
|
||||||
<th class="tac">{{ l.slideshow_slide_panel_th_activity }}</th>
|
<th class="tac">{{ l.slideshow_slide_panel_th_activity }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -42,6 +43,18 @@
|
|||||||
<input type="checkbox" {% if slide.enabled %}checked="checked"{% endif %}><span></span>
|
<input type="checkbox" {% if slide.enabled %}checked="checked"{% endif %}><span></span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="">
|
||||||
|
{% if slide.cron_schedule %}
|
||||||
|
{% set cron_desc = cron_descriptor(slide.cron_schedule) %}
|
||||||
|
{% if cron_desc %}
|
||||||
|
⏳ {{ cron_desc }}
|
||||||
|
{% else %}
|
||||||
|
<span class="error">⚠️ {{ l.slideshow_slide_panel_td_cron_scheduled_bad_cron }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
🔄 {{ l.slideshow_slide_panel_td_cron_scheduled_loop }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="actions tac">
|
<td class="actions tac">
|
||||||
<a href="javascript:void(0);" class="item-edit slide-edit">
|
<a href="javascript:void(0);" class="item-edit slide-edit">
|
||||||
<i class="fa fa-pencil"></i>
|
<i class="fa fa-pencil"></i>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<div class="modal modal-slide-add">
|
<div class="modal modal-slide-add modal-slide">
|
||||||
<h2>
|
<h2>
|
||||||
{{ l.slideshow_slide_form_add_title }}
|
{{ l.slideshow_slide_form_add_title }}
|
||||||
</h2>
|
</h2>
|
||||||
@ -38,6 +38,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="slide-add-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
|
||||||
|
<div class="widget">
|
||||||
|
<input type="checkbox" id="slide-add-cron-schedule-trigger" class="trigger" />
|
||||||
|
<input type="text" name="cron_schedule" id="slide-add-cron-schedule" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" class="hidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="modal-close">
|
<button type="button" class="modal-close">
|
||||||
{{ l.slideshow_slide_form_button_cancel }}
|
{{ l.slideshow_slide_form_button_cancel }}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<div class="modal modal-slide-edit hidden">
|
<div class="modal modal-slide-edit modal-slide hidden">
|
||||||
<h2>
|
<h2>
|
||||||
{{ l.slideshow_slide_form_edit_submit }}
|
{{ l.slideshow_slide_form_edit_submit }}
|
||||||
</h2>
|
</h2>
|
||||||
@ -38,6 +38,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="slide-edit-cron-schedule">{{ l.slideshow_slide_form_label_cron_scheduled }}</label>
|
||||||
|
<div class="widget">
|
||||||
|
<input type="checkbox" id="slide-edit-cron-schedule-trigger" class="trigger" />
|
||||||
|
<input type="text" name="cron_schedule" id="slide-edit-cron-schedule" placeholder="{{ l.slideshow_slide_form_widget_cron_scheduled_placeholder }}" class="hidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="modal-close">
|
<button type="button" class="modal-close">
|
||||||
{{ l.slideshow_slide_form_button_cancel }}
|
{{ l.slideshow_slide_form_button_cancel }}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user