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 ## About
Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode) 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: ### 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)
![Obscreen Screenshot](https://github.com/jr-k/obscreen/blob/master/docs/screenshot.png "Obscreen Screenshot") ![Obscreen Screenshot](https://github.com/jr-k/obscreen/blob/master/docs/screenshot.png "Obscreen Screenshot")
## 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

View File

@ -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
} }
} }
} }

View File

@ -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
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({ $.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

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": "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?",

View File

@ -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 ?",

View File

@ -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

View File

@ -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'])

View File

@ -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'))

View File

@ -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

View File

@ -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

View File

@ -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
) )

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -1 +1 @@
1.5 1.6

View File

@ -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();
} }
} }

View File

@ -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>

View File

@ -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 }}

View File

@ -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 }}