Merge pull request #12 from jr-k/develop

Release v1.6
This commit is contained in:
JRK 2024-05-03 03:14:01 +02:00 committed by GitHub
commit 0c9c02751f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 347 additions and 115 deletions

View File

@ -5,6 +5,8 @@
## About
Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
[![Docker Pulls](https://badgen.net/docker/pulls/jierka/obscreen?icon=docker&label=docker%20pulls)](https://hub.docker.com/r/jierka/obscreen/)
### Features:
- Dead simple chromium webview
- Clear GUI
@ -18,7 +20,7 @@ Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
![Obscreen Screenshot](https://github.com/jr-k/obscreen/blob/master/docs/screenshot.png "Obscreen Screenshot")
## Installation and configure (docker)
## Installation and configuration (docker)
```bash
git clone https://github.com/jr-k/obscreen.git
cd obscreen

View File

@ -6,7 +6,8 @@
"location",
"name",
"position",
"type"
"type",
"cron_schedule"
],
"data": {
"0": {
@ -15,7 +16,8 @@
"type": "picture",
"enabled": true,
"name": "Picture Sample",
"position": 0
"position": 0,
"cron_schedule": null
},
"1": {
"location": "https://unix.org",
@ -23,7 +25,8 @@
"type": "url",
"enabled": true,
"name": "URL Sample",
"position": 1
"position": 1,
"cron_schedule": null
}
}
}

View File

@ -512,6 +512,14 @@ form .form-group textarea {
width: auto;
}
form .form-group input[type=checkbox] {
flex: 0;
}
form .form-group input[type=checkbox].trigger {
margin-right: 10px;
}
form .form-group span {
margin-left: 10px;
}

101
data/www/js/is-cron-now.js Executable file
View 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
};
})();

View File

@ -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({
url: '/slideshow/slide/toggle',
headers: {'Content-Type': 'application/json'},
@ -68,6 +68,16 @@ jQuery(document).ready(function ($) {
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 () {
const value = $(this).val();
const inputType = $(this).find('option').filter(function (i, el) {
@ -95,11 +105,14 @@ jQuery(document).ready(function ($) {
$(document).on('click', '.slide-edit', function () {
const slide = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-slide-edit');
const hasCron = slide.cron_schedule && slide.cron_schedule.length > 0;
$('.modal-slide-edit input:visible:eq(0)').focus().select();
$('#slide-edit-name').val(slide.name);
$('#slide-edit-type').val(slide.type);
$('#slide-edit-location').val(slide.location);
$('#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);
});

BIN
docs/screenshot.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 267 KiB

View File

@ -8,7 +8,10 @@
"slideshow_slide_panel_th_duration": "Duration",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_enabled": "Enabled",
"slideshow_slide_panel_th_cron_scheduled": "Scheduled",
"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_submit": "Add",
"slideshow_slide_form_edit_title": "Edit Slide",
@ -22,6 +25,8 @@
"slideshow_slide_form_label_object": "Object",
"slideshow_slide_form_label_duration": "Duration",
"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",
"js_slideshow_slide_delete_confirmation": "Are you sure?",

View File

@ -8,7 +8,10 @@
"slideshow_slide_panel_th_duration": "Durée",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_enabled": "Activé",
"slideshow_slide_panel_th_cron_scheduled": "Programmation",
"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_submit": "Ajouter",
"slideshow_slide_form_edit_title": "Modification d'une slide",
@ -22,6 +25,8 @@
"slideshow_slide_form_label_object": "Objet",
"slideshow_slide_form_label_duration": "Durée",
"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",
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",

View File

@ -1,3 +1,5 @@
flask==2.3.3
pysondb-v2==2.1.0
python-dotenv
cron-descriptor

View File

@ -3,18 +3,31 @@ import json
from flask import Flask, render_template, redirect, request, url_for, send_from_directory, jsonify
from src.service.ModelStore import ModelStore
from src.interface.ObController import ObController
from src.utils import get_ip_address
from src.utils import get_ip_address, get_safe_cron_descriptor
class PlayerController(ObController):
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:
return [slides[0], slides[0]]
playlist_loop = []
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):
self._app.add_url_rule('/', 'player', self.player, methods=['GET'])

View File

@ -8,7 +8,7 @@ from src.service.ModelStore import ModelStore
from src.model.entity.Slide import Slide
from src.model.enum.SlideType import SlideType
from src.interface.ObController import ObController
from src.utils import str_to_enum
from src.utils import str_to_enum, get_optional_string
class SlideshowController(ObController):
@ -40,6 +40,7 @@ class SlideshowController(ObController):
name=request.form['name'],
type=str_to_enum(request.form['type'], SlideType),
duration=request.form['duration'],
cron_schedule=get_optional_string(request.form['cron_schedule']),
)
if slide.has_file():
@ -65,7 +66,7 @@ class SlideshowController(ObController):
return redirect(url_for('slideshow_slide_list'))
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()
return redirect(url_for('slideshow_slide_list'))

View File

@ -8,7 +8,7 @@ class LangManager:
def __init__(self, lang: str):
self._map = {}
self._lang = lang
self._lang = lang.lower()
self.load()
def load(self, directory: str = "", prefix: str = ""):
@ -23,3 +23,6 @@ class LangManager:
def map(self) -> dict:
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

View File

@ -2,7 +2,7 @@ import os
from typing import Dict, Optional, List, Tuple, Union
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.errors import IdDoesNotExistError
@ -69,8 +69,12 @@ class SlideManager:
for slide_id, slide_position in positions.items():
self._db.update_by_id(slide_id, {"position": slide_position})
def update_form(self, id: str, name: str, duration: int) -> None:
self._db.update_by_id(id, {"name": name, "duration": duration})
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,
"cron_schedule": get_optional_string(cron_schedule)
})
def add_form(self, slide: Union[Slide, Dict]) -> None:
db_slide = slide

View File

@ -7,7 +7,7 @@ from src.utils import str_to_enum
class Slide:
def __init__(self, location: str = '', duration: int = 3, type: Union[SlideType, str] = SlideType.URL, enabled: bool = False, name: str = 'Untitled', position: int = 999, id: Optional[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._location = location
self._duration = duration
@ -15,6 +15,7 @@ class Slide:
self._enabled = enabled
self._name = name
self._position = position
self._cron_schedule = cron_schedule
@property
def id(self) -> Optional[str]:
@ -36,6 +37,14 @@ class Slide:
def type(self, value: SlideType):
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
def duration(self) -> int:
return self._duration
@ -77,6 +86,7 @@ class Slide:
f"duration='{self.duration}',\n" \
f"position='{self.position}',\n" \
f"location='{self.location}',\n" \
f"cron_schedule='{self.cron_schedule}',\n" \
f")"
def to_json(self) -> str:
@ -91,9 +101,11 @@ class Slide:
"type": self.type.value,
"duration": self.duration,
"location": self.location,
"cron_schedule": self.cron_schedule,
}
def has_file(self) -> bool:
return (self.type == SlideType.VIDEO
return (
self.type == SlideType.VIDEO
or self.type == SlideType.PICTURE
)

View File

@ -9,6 +9,7 @@ from src.model.hook.HookRegistration import HookRegistration
from src.model.hook.StaticHookRegistration import StaticHookRegistration
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
from src.constant.WebDirConstant import WebDirConstant
from src.utils import get_safe_cron_descriptor
class TemplateRenderer:
@ -18,6 +19,9 @@ class TemplateRenderer:
self._model_store = model_store
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:
globals = dict(
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'),
LANG=self._model_store.variable().map().get('lang').as_string(),
HOOK=self._render_hook,
cron_descriptor=self.cron_descriptor
)
for hook in HookType:

View File

@ -33,7 +33,6 @@ class WebServer:
def setup(self) -> None:
self._setup_flask_app()
self._setup_web_globals()
self._setup_web_extensions()
self._setup_web_errors()
self._setup_web_controllers()
@ -73,12 +72,8 @@ class WebServer:
def inject_global_vars() -> dict:
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:
@self._app.errorhandler(404)
def not_found(e):
return send_from_directory(self._get_template_folder(), 'core/error404.html'), 404

View File

@ -4,6 +4,38 @@ import platform
from typing import Optional, List
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]:
@ -27,12 +59,14 @@ def get_keys(dict_or_object, key_list_name: str, key_attr_name: str = 'key') ->
return None
def str_to_enum(str_val: str, enum_class) -> Enum:
for enum_item in enum_class:
if enum_item.value == str_val:
return enum_item
raise ValueError(f"{str_val} is not a valid {enum_class.__name__} item")
def get_ip_address() -> Optional[str]:
try:
os_name = platform.system().lower()

View File

@ -1 +1 @@
1.5
1.6

View File

@ -12,19 +12,20 @@
.slide iframe { background: white; }
.slide img { height: 100%; }
</style>
<script type="application/javascript" src="{{ STATIC_PREFIX }}js/is-cron-now.js"></script>
</head>
<body>
<div id="FirstSlide" class="slide" style="z-index: 1000;">
<iframe src="/player/default"></iframe>
</div>
<div id="SecondSlide" class="slide" style="z-index: 500">
<div id="SecondSlide" class="slide" style="z-index: 500;">
<iframe src="/player/default"></iframe>
</div>
<script type="text/javascript">
var items = {{items | safe}};
var duration = 3000 / 1;
var playlistCheck = 10 * 1000; // 10 seconds check
var curUrl = 0;
var curItemIndex = 0;
var nextReady = true;
var itemCheck = setInterval(function () {
fetch('player/playlist').then(function(response) {
@ -37,53 +38,94 @@
console.error(err);
});
}, playlistCheck);
var animate = {{ 'true' if slide_animation_enabled.eval() else 'false' }};
var animate_transitions = [
"animate__{{ slide_animation_entrance_effect.eval()|default("fadeIn") }}",
"animate__{{ slide_animation_exit_effect.eval()|default("none") }}"
];
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
};
var cronTick = function() {
if ((new Date()).getSeconds() != 0) {
return;
}
// console.log('Cron Tick');
for (var i = 0; i < items.cron.length; i++) {
var item = items.cron[i];
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 main() {
preloadIntoSecondSlide();
preloadSlide('SecondSlide', items.loop[curItemIndex])
cronState.interval = setInterval(cronTick, 1000);
}
function loadContent(element, callbackReady) {
switch (items[curUrl].type) {
function loadContent(element, callbackReady, item) {
switch (item.type) {
case 'url':
loadUrl(element, callbackReady);
loadUrl(element, callbackReady, item);
break;
case 'picture':
loadPicture(element, callbackReady);
loadPicture(element, callbackReady, item);
break;
case 'video':
loadVideo(element, callbackReady);
loadVideo(element, callbackReady, item);
break;
default:
loadUrl(element, callbackReady);
loadUrl(element, callbackReady, item);
break;
}
}
function loadUrl(element, callbackReady) {
element.innerHTML = `<iframe src="${items[curUrl].location}"></iframe>`;
function loadUrl(element, callbackReady, item) {
element.innerHTML = `<iframe src="${item.location}"></iframe>`;
callbackReady(function () {});
}
function loadPicture(element, callbackReady) {
element.innerHTML = `<img src="${items[curUrl].location}" alt="" />`;
function loadPicture(element, callbackReady, item) {
element.innerHTML = `<img src="${item.location}" alt="" />`;
callbackReady(function () {});
}
function loadVideo(element, callbackReady) {
element.innerHTML = `<video><source src=${items[curUrl].location} type="video/mp4" /></video>`;
function loadVideo(element, callbackReady, item) {
element.innerHTML = `<video><source src=${item.location} type="video/mp4" /></video>`;
var video = element.querySelector('video');
video.addEventListener('loadedmetadata', function () {
items[curUrl].duration = Math.ceil(video.duration);
item.duration = Math.ceil(video.duration);
});
callbackReady(function () {
var onSlideStart = function () {
nextReady = false;
video.play();
video.addEventListener('ended', function () {
@ -91,17 +133,17 @@
});
setTimeout(function () {
nextReady = true;
}, Math.ceil(items[curUrl].duration * 1.5));
});
}, Math.ceil(item.duration * 1.5));
};
callbackReady(onSlideStart);
}
function preloadIntoFirstSlide() {
//console.log('preloadIntoFirstSlide', items[curUrl]);
var element = document.getElementById('FirstSlide');
function preloadSlide(slide, item) {
var element = document.getElementById(slide);
var callbackReady = function (onSlideStart) {
var move = function () {
if (nextReady) {
moveToFirstSlide();
if (nextReady && !cronState.active) {
moveToSlide(slide, item);
onSlideStart();
} else {
setTimeout(move, 1000);
@ -111,76 +153,31 @@
setTimeout(move, duration);
};
loadContent(element, callbackReady);
loadContent(element, callbackReady, item);
}
function moveToFirstSlide() {
//console.log('moveToFirstSlide', items[curUrl]);
var firstSlide = document.querySelector('#FirstSlide');
var secondSlide = document.querySelector('#SecondSlide');
function moveToSlide(slide, item) {
curSlide = document.getElementById(slide);
previousSlide = curSlide == firstSlide ? secondSlide : firstSlide;
duration = items[curUrl].duration * 1000;
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
duration = item.duration * 1000;
curItemIndex = (curItemIndex + 1) === items.loop.length ? 0 : curItemIndex + 1;
firstSlide.style.zIndex = 1000;
secondSlide.style.zIndex = 500;
curSlide.style.zIndex = 1000;
previousSlide.style.zIndex = 500;
if (animate) {
firstSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
firstSlide.onanimationend = () => {
firstSlide.classList.remove(animate_transitions[0], animate_speed);
preloadIntoSecondSlide();
curSlide.classList.add('animate__animated', animate_transitions[0], animate_speed);
curSlide.onanimationend = () => {
curSlide.classList.remove(animate_transitions[0], animate_speed);
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
};
secondSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
secondSlide.onanimationend = () => {
secondSlide.classList.remove(animate_transitions[1], animate_speed);
previousSlide.classList.add('animate__animated', animate_transitions[1], animate_speed);
previousSlide.onanimationend = () => {
previousSlide.classList.remove(animate_transitions[1], animate_speed);
};
} else {
preloadIntoSecondSlide();
}
}
function preloadIntoSecondSlide() {
//console.log('preloadIntoSecondSlide', items[curUrl]);
var element = document.getElementById('SecondSlide');
var callbackReady = function (onSlideStart) {
var move = function () {
if (nextReady) {
moveToSecondSlide();
onSlideStart();
} else {
setTimeout(move, 1000);
}
}
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();
preloadSlide(previousSlide.attributes['id'].value, items.loop[curItemIndex]);
}
}

View File

@ -4,6 +4,7 @@
<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_enabled }}</th>
<th class="">{{ l.slideshow_slide_panel_th_cron_scheduled }}</th>
<th class="tac">{{ l.slideshow_slide_panel_th_activity }}</th>
</tr>
</thead>
@ -42,6 +43,18 @@
<input type="checkbox" {% if slide.enabled %}checked="checked"{% endif %}><span></span>
</label>
</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">
<a href="javascript:void(0);" class="item-edit slide-edit">
<i class="fa fa-pencil"></i>

View File

@ -1,4 +1,4 @@
<div class="modal modal-slide-add">
<div class="modal modal-slide-add modal-slide">
<h2>
{{ l.slideshow_slide_form_add_title }}
</h2>
@ -38,6 +38,14 @@
</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">
<button type="button" class="modal-close">
{{ l.slideshow_slide_form_button_cancel }}

View File

@ -1,4 +1,4 @@
<div class="modal modal-slide-edit hidden">
<div class="modal modal-slide-edit modal-slide hidden">
<h2>
{{ l.slideshow_slide_form_edit_submit }}
</h2>
@ -38,6 +38,14 @@
</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">
<button type="button" class="modal-close">
{{ l.slideshow_slide_form_button_cancel }}