commit
0c9c02751f
@ -5,6 +5,8 @@
|
||||
## About
|
||||
Use a RaspberryPi to show a full-screen slideshow (Kiosk-mode)
|
||||
|
||||
[](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)
|
||||
|
||||

|
||||
|
||||
## Installation and configure (docker)
|
||||
## Installation and configuration (docker)
|
||||
```bash
|
||||
git clone https://github.com/jr-k/obscreen.git
|
||||
cd obscreen
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
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({
|
||||
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
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_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?",
|
||||
|
||||
|
||||
@ -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 ?",
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
flask==2.3.3
|
||||
pysondb-v2==2.1.0
|
||||
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 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'])
|
||||
|
||||
@ -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'))
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
34
src/utils.py
34
src/utils.py
@ -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()
|
||||
|
||||
@ -1 +1 @@
|
||||
1.5
|
||||
1.6
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
function main() {
|
||||
preloadIntoSecondSlide();
|
||||
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 loadContent(element, callbackReady) {
|
||||
switch (items[curUrl].type) {
|
||||
function main() {
|
||||
preloadSlide('SecondSlide', items.loop[curItemIndex])
|
||||
cronState.interval = setInterval(cronTick, 1000);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user